FileStream - Citirea și scrierea fișierelor
Clasa FileStream oferă posibilități pentru citirea și scrierea fișierelor. Aceasta permite lucrul atât cu fișiere text, cât și cu fișiere binare.
Crearea unui FileStream
Pentru a crea un obiect FileStream, se pot utiliza fie constructorii acestei clase, fie metodele statice ale clasei File. Constructorul FileStream are multe versiuni supraîncărcate, dintre care una dintre cele mai simple și utilizate este:
FileStream(string filename, FileMode mode)
Aici, constructorul primește doi parametri: calea către fișier și enumerarea FileMode. Această enumerare indică modul de acces la fișier și poate lua următoarele valori:
- Append: dacă fișierul există, textul este adăugat la sfârșitul fișierului. Dacă fișierul nu există, acesta este creat. Fișierul este deschis doar pentru scriere
- Create: se creează un fișier nou. Dacă un astfel de fișier deja există, acesta este suprascris
- CreateNew: se creează un fișier nou. Dacă un astfel de fișier deja există, aplicația va arunca o eroare
- Open: deschide fișierul. Dacă fișierul nu există, va arunca o excepție
- OpenOrCreate: dacă fișierul există, acesta este deschis; dacă nu, se creează un fișier nou
- Truncate: dacă fișierul există, acesta este suprascris. Fișierul este deschis doar pentru scriere
O altă modalitate de a crea un obiect FileStream este prin metodele statice ale clasei File:
FileStream File.Open(string file, FileMode mode);
FileStream File.OpenRead(string file);
FileStream File.OpenWrite(string file);
Prima metodă deschide fișierul ținând cont de obiectul FileMode și returnează un flux de fișier FileStream. Această metodă are și câteva versiuni supraîncărcate. A doua metodă deschide fluxul pentru citire, iar a treia deschide fluxul pentru scriere.
Închiderea fluxului
Clasa FileStream implementează interfața IDisposable pentru eliberarea tuturor resurselor asociate cu fișierul. După ce ați terminat de lucrat cu FileStream, trebuie să eliberați fișierul asociat prin apelarea metodei Dispose. Pentru închiderea corectă, puteți apela metoda Close(), care la rândul său apelează metoda Dispose:
FileStream? fstream = null;
try
{
fstream = new FileStream("note3.dat", FileMode.OpenOrCreate);
// operațiuni cu fstream
}
catch(Exception ex)
{ }
finally
{
fstream?.Close();
}
Sau puteți folosi structura using, care va elibera automat toate resursele asociate cu FileStream:
using (FileStream fstream = new FileStream("note3.dat", FileMode.OpenOrCreate))
{
// operațiuni cu fstream
}
Proprietăți și metode ale FileStream
Cele mai importante proprietăți ale clasei FileStream sunt:
- Length: returnează lungimea fluxului în bytes
- Position: returnează poziția curentă în flux
- Name: returnează calea absolută către fișierul deschis în FileStream
Pentru citirea/scrierea fișierelor, se pot utiliza următoarele metode ale clasei FileStream:
- void CopyTo(Stream destination): copie datele din fluxul curent în fluxul destination
- Task CopyToAsync(Stream destination): versiunea asincronă a metodei CopyTo
- void Flush(): golește conținutul bufferului în fișier
- Task FlushAsync(): versiunea asincronă a metodei Flush
- int Read(byte[] array, int offset, int count): citește datele din fișier într-un array de bytes și returnează numărul de bytes citiți
- int Read(byte[] array, int offset, int count) Primește trei parametri: array - array-ul de bytes în care vor fi plasate datele citite din fișier; offset - reprezintă deplasarea în bytes în array-ul array, în care bytes-ii citiți vor fi plasați; count - numărul maxim de bytes care urmează să fie citiți; Task<int> ReadAsync(byte[] array, int offset, int count): versiunea asincronă a metodei Read.
- long Seek(long offset, SeekOrigin origin): setează poziția în flux cu deplasare pe baza numărului de bytes indicat în parametrul offset.
- void Write(byte[] array, int offset, int count): scrie datele din array-ul de bytes în fișier. Primește trei parametri: array - array-ul de bytes din care datele vor fi scrise în fișier; offset - deplasarea în bytes în array-ul array, de unde începe scrierea bytes-ilor în flux; count - numărul maxim de bytes care urmează să fie scriși.
- Task WriteAsync(byte[] array, int offset, int count): versiunea asincronă a metodei Write.
Citirea și scrierea fișierelor
FileStream oferă acces la fișiere la nivel de bytes, așa că, de exemplu, dacă trebuie să citiți sau să scrieți una sau mai multe linii într-un fișier text, trebuie să transformați array-ul de bytes în stringuri folosind metode speciale. De aceea, pentru lucrul cu fișiere text sunt utilizate alte clase.
Totuși, când lucrați cu fișiere binare care au o anumită structură, FileStream poate fi foarte util pentru extragerea anumitor porțiuni de informații și procesarea lor.
Să vedem un exemplu de citire și scriere într-un fișier text:
using System.Text;
string path = @"C:\app\note.txt"; // calea către fișier
string text = "Hello FDC.COM"; // textul pentru scriere
// scrierea în fișier
using (FileStream fstream = new FileStream(path, FileMode.OpenOrCreate))
{
// transformăm string-ul în bytes
byte[] buffer = Encoding.Default.GetBytes(text);
// scrierea array-ului de bytes în fișier
await fstream.WriteAsync(buffer, 0, buffer.Length);
Console.WriteLine("Textul a fost scris în fișier");
}
// citirea din fișier
using (FileStream fstream = File.OpenRead(path))
{
// alocăm un array pentru citirea datelor din fișier
byte[] buffer = new byte[fstream.Length];
// citirea datelor
await fstream.ReadAsync(buffer, 0, buffer.Length);
// decodăm bytes în string
string textFromFile = Encoding.Default.GetString(buffer);
Console.WriteLine($"Textul din fișier: {textFromFile}");
}
În acest exemplu, definim calea către fișier și textul pentru scriere.
Atât la citire, cât și la scriere, pentru crearea și eliminarea obiectului FileStream se utilizează structura using, la sfârșitul căreia pentru obiectul FileStream creat se apelează automat metoda Dispose, iar astfel obiectul este distrus.
Deoarece operațiile cu fișiere pot dura mult timp și reprezintă un punct sensibil în funcționarea programului, se recomandă utilizarea versiunilor asincrone ale metodelor FileStream. Atât la scriere, cât și la citire, se folosește obiectul de codificare Encoding.Default din spațiul de nume System.Text.
În acest caz, folosim două metode ale acestuia: GetBytes pentru obținerea unui array de bytes dintr-un string și GetString pentru obținerea unui string dintr-un array de bytes.
În final, textul introdus de noi este scris în fișierul note.txt și vom obține următorul rezultat în consolă:
Textul a fost scris în fișier
Textul din fișier: Hello FDC.COM
Fișierul scris este, în esență, un fișier binar (nu text), deși dacă scriem doar un string în el, putem să-l vizualizăm într-un mod lizibil, deschizându-l într-un editor de text. Totuși, dacă scriem bytes aleatorii în el, de exemplu:
fstream.WriteByte(13);
fstream.WriteByte(103);
Atunci pot apărea probleme în interpretarea acestuia. De aceea, pentru lucrul cu fișiere text sunt destinate alte clase - StreamReader și StreamWriter.
Acces aleatoriu la fișiere
Adesea, fișierele binare au o anumită structură. Cunoscând această structură, putem extrage porțiunea necesară de informații din fișier sau, invers, putem scrie într-o anumită poziție a fișierului un set specific de bytes.
e exemplu, în fișierele .wav, datele audio încep de la byte-ul 44, iar până la byte-ul 44 se află diferite metadate - numărul de canale audio, frecvența de eșantionare etc.
Cu ajutorul metodei Seek() putem controla poziția cursorului fluxului, de la care începe citirea sau scrierea în fișier. Această metodă primește doi parametri: offset (deplasare) și poziția în fișier. Poziția în fișier este descrisă de trei valori:
- SeekOrigin.Begin: începutul fișierului
- SeekOrigin.End: sfârșitul fișierului
- SeekOrigin.Current: poziția curentă în fișier
Cursorul fluxului, de la care începe citirea sau scrierea, este deplasat înainte pe baza valorii offset în raport cu poziția specificată ca al doilea parametru. Deplasarea poate fi negativă, atunci cursorul se deplasează înapoi; dacă este pozitivă, se deplasează înainte.
Să vedem un exemplu simplu:
using System.Text;
string path = "note.dat";
string text = "hello world";
using (FileStream fstream = new FileStream(path, FileMode.OpenOrCreate))
{
// transformăm string-ul în bytes
byte[] input = Encoding.Default.GetBytes(text);
// scrierea array-ului de bytes în fișier
fstream.Write(input, 0, input.Length);
Console.WriteLine("Textul a fost scris în fișier");
}
// citirea unei părți din fișier
using (FileStream fstream = new FileStream(path, FileMode.OpenOrCreate))
{
// deplasăm cursorul la sfârșitul fișierului, cu cinci bytes înainte de sfârșit
fstream.Seek(-5, SeekOrigin.End); // minus 5 caractere de la sfârșitul fluxului
// citim cinci bytes de la poziția curentă
byte[] output = new byte[5];
await fstream.ReadAsync(output, 0, output.Length);
// decodăm bytes în string
string textFromFile = Encoding.Default.GetString(output);
Console.WriteLine($"Textul din fișier: {textFromFile}"); // world
}
Mai întâi, scriem în fișier textul "hello world". Apoi, accesăm din nou fișierul pentru citire. Inițial, deplasăm cursorul cu cinci caractere înapoi în raport cu sfârșitul fluxului de fișiere:
fstream.Seek(-5, SeekOrigin.End)

După efectuarea acestei operațiuni, cursorul va fi poziționat pe caracterul "w".
După aceasta, citim cinci bytes începând cu caracterul "w". În codificarea implicită, 1 caracter reprezintă 1 byte. Prin urmare, citirea a 5 bytes este echivalentă cu citirea a cinci caractere: "world".
Astfel, obținem următorul rezultat în consolă:
Textul a fost scris în fișier
Textul din fișier: world
Să vedem un exemplu puțin mai complex - cu scrierea începând de la o anumită poziție:
using System.Text;
string path = "note2.dat";
string text = "hello world";
// scrierea în fișier
using (FileStream fstream = new FileStream(path, FileMode.OpenOrCreate))
{
// transformăm string-ul în bytes
byte[] input = Encoding.Default.GetBytes(text);
// scrierea array-ului de bytes în fișier
fstream.Write(input, 0, input.Length);
Console.WriteLine("Textul a fost scris în fișier");
}
using (FileStream fstream = new FileStream(path, FileMode.OpenOrCreate))
{
// înlocuim în fișier cuvântul "world" cu "house"
string replaceText = "house";
fstream.Seek(-5, SeekOrigin.End); // minus 5 caractere de la sfârșitul fluxului
byte[] input = Encoding.Default.GetBytes(replaceText);
await fstream.WriteAsync(input, 0, input.Length);
// citim întregul fișier
// readucem cursorul la începutul fișierului
fstream.Seek(0, SeekOrigin.Begin);
byte[] output = new byte[fstream.Length];
await fstream.ReadAsync(output, 0, output.Length);
// decodăm bytes în string
string textFromFile = Encoding.Default.GetString(output);
Console.WriteLine($"Textul din fișier: {textFromFile}"); // hello house
}
În acest exemplu, scriem inițial în fișier string-ul "hello world". Apoi deschidem din nou fișierul și ne deplasăm la sfârșitul fișierului, cu cinci caractere înainte de sfârșit (adică din nou de la poziția caracterului "w"), și scriem string-ul "house". Astfel, string-ul "house" înlocuiește string-ul "world".
Pentru a citi întregul fișier după această operațiune, mutăm cursorul la începutul fișierului:
fstream.Seek(0, SeekOrigin.Begin);
Rezultatul afișat în consolă va fi:
Textul a fost scris în fișier
Textul din fișier: hello house