1. Introduzione
Immagina un bollitore con l'acqua. Apri il rubinetto — l'acqua inizia a scorrere. Puoi riempire tanto e versare tutto in una volta, oppure riempire il bollitore poco a poco. Lo stesso vale per i file — non sempre è comodo o possibile caricare tutto il file in memoria subito. I file possono essere grandi, e a volte la fonte dei dati non è nemmeno un file, ma per esempio una connessione di rete, dove i dati arrivano pian piano.
Se provassimo sempre a lavorare solo con array di byte, con file grandi finiremmo subito la memoria, e per stream "infiniti" (tipo video o audio stream) questo approccio proprio non funziona. Ed è qui che arriva il concetto di stream!
In .NET uno stream è un'astrazione per accedere ai dati in modo sequenziale: non importa cosa c'è dietro — file, rete, memoria, o anche qualcosa di super strano tipo un archivio compresso. Lo stream ti permette di leggere e scrivere dati a pezzi, di solito a blocchi o byte.
Idea principale:
- Stream — è un canale per trasferire dati. È come un nastro trasportatore: puoi "mettere" (scrivere) o "prendere" (leggere) dati, senza preoccuparti troppo di dove e come sono salvati.
- I dati arrivano in sequenza: puoi leggere il prossimo pezzo solo dopo il precedente (o viceversa, se c'è il supporto per il seek).
- Nella maggior parte dei casi non tieni tutti i dati in memoria (e il computer ti ringrazierà per questo).
Questa astrazione è alla base praticamente di tutte le operazioni di input/output in .NET: lavoro con file, reti, archivi, persino con la console!
2. Stream System.IO.Stream
Ereditarietà e architettura: System.IO.Stream
Quasi tutti gli stream in .NET ereditano dalla classe astratta System.IO.Stream. Questa definisce i metodi base per leggere, scrivere, spostarsi nello stream e gestirlo.
classDiagram
class Stream {
+Read()
+Write()
+Seek()
+CanRead
+CanWrite
+CanSeek
+Length
+Position
}
class FileStream
class MemoryStream
class NetworkStream
class CryptoStream
Stream <|-- FileStream
Stream <|-- MemoryStream
Stream <|-- NetworkStream
Stream <|-- CryptoStream
- Stream — classe base astratta
- FileStream — per lavorare con i file
- MemoryStream — per lavorare con dati in memoria
- NetworkStream — per interazione di rete
- CryptoStream — per cifrare/decifrare
Breve panoramica delle proprietà e dei metodi chiave dello stream
| Proprietà / Metodo | Descrizione |
|---|---|
|
Si può leggere da questo stream |
|
Si può scrivere in questo stream |
|
Si può spostarsi nello stream (non tutti lo supportano) |
|
Lunghezza dello stream (se supportata — non tutti gli stream ce l'hanno) |
|
Posizione attuale nello stream |
|
Lettura dei dati |
|
Scrittura dei dati |
|
Spostamento nello stream |
|
Svuota il buffer (scrive tutto quello accumulato nello stream) |
/ |
Chiude lo stream e libera le risorse |
Vediamo come appare "nella pratica".
3. Esempio: lettura e scrittura di file tramite Stream
Ecco un esempio minimal per vedere uno stream "in azione":
// Apriamo un file per scrivere
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Supponiamo di voler scrivere i numeri da 1 a 10 nel file
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Scriviamo un byte alla volta
}
// Chiudiamo esplicitamente il file per poi riaprirlo in lettura
stream.Close();
// Ora proviamo a leggere questi numeri indietro
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Stampa 1, 2, ... 10
}
Qui usiamo FileStream, che è uno stream vero e proprio: leggi e scrivi dati a blocchi o a byte.
Tipi di stream: dove li puoi trovare?
Uno stream non è solo un file su disco. Ecco alcuni esempi dove si usa il concetto di stream:
- File su disco (per esempio, FileStream — il caso più comune)
- Stream in memoria RAM (MemoryStream — comodo per dati temporanei o intermedi)
- Connessione di rete (NetworkStream)
- Compressione/archiviazione (GZipStream, DeflateStream)
- Cifratura (CryptoStream)
- Input/output da console (sì, anche questi!) — tecnicamente, sono stream pure loro
Questo ti permette di scrivere codice senza preoccuparti della fonte/destinazione dei dati: se il tuo codice lavora con uno stream, allora è universale!
4. Dettagli utili
Lettura e scrittura sono operazioni di trasferimento dati a pezzi. Di solito tramite array di byte e i metodi Read, Write.
Esempio: lettura di un file a blocchi
byte[] buffer = new byte[1024]; // Buffer da 1024 byte (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Gestiamo solo bytesRead byte dentro buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Somma del blocco: {sum}");
}
Questo approccio si usa ovunque — dagli antivirus ai player musicali.
Posizionamento nello stream (Position, Seek)
Nella maggior parte delle implementazioni di stream (tipo quelli dei file) puoi spostarti nei dati — leggere non solo il "prossimo pezzo", ma andare a una posizione precisa e lavorare da lì.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Ci spostiamo al 6° byte (contando da 0)
int value = stream.ReadByte();
Console.WriteLine($"6° byte nel file: {value}");
Gli stream possono essere solo in lettura, solo in scrittura, o entrambi
Alcuni stream supportano solo una delle due modalità:
- File aperto in scrittura: solo Write()
- Stream per leggere dati di rete: solo Read()
- In alcuni casi strani (tipo stream per stampante) non è proprio possibile "tornare indietro" o fare seek nello stream.
Controlla le operazioni supportate tramite le proprietà CanRead, CanWrite, CanSeek:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Lettura supportata");
if (stream.CanWrite)
Console.WriteLine("Scrittura supportata");
if (stream.CanSeek)
Console.WriteLine("Si può spostarsi nel file");
Bufferizzazione negli stream
Quasi tutti gli stream usano buffer interni per migliorare le prestazioni. Il buffering risparmia accessi a disco/rete: i dati si accumulano internamente e poi vengono dati/scritti in blocco.
Il metodo Flush() permette di svuotare il buffer (per esempio, per essere sicuri che tutto sia stato scritto su disco):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Garantisce che la scrittura sia davvero fatta su disco
Se stai scrivendo dati importanti (tipo transazioni di pagamento!), chiamare Flush() è il tuo amico.
5. Errori tipici lavorando con gli stream
Molto spesso i principianti fanno questi errori:
Si dimenticano di chiudere lo stream (e si beccano memory leak, file "bloccati" e altre gioie).
Confondono stream di testo e binari — provano a scrivere una stringa con un metodo per byte, e poi ottengono "caratteri strani".
Usano un buffer troppo piccolo (o nessun buffer) — le operazioni diventano lente.
Pensano che Read() legga sempre esattamente il numero di byte richiesto — in realtà può restituire meno; bisogna sempre controllare il valore restituito.
Non considerano che non tutti gli stream supportano il seek (Seek), soprattutto quelli di rete.
Per esempio:
// Esempio sbagliato: leggere tutti i byte del file senza controllare quanti byte sono stati letti davvero
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead può essere meno di 1024 se il file è più piccolo!
}
GO TO FULL VERSION