Fisiere, operatii I/O si stream-uri
Pentru multe aplicatii este necesara lucrul cu fisiere si I/O-ul. Cele mai multe primitive pentru lucrul cu fisiere exista deja implementate in biblioteca standard pentru C#.
Sistemul de fisiere
In C# exista trei clase statice pentru a lucra cu sistemul de fisiere, Path, Directory si File. Clasa Path are metode pentru a lucra in general cu sistemul de fisiere si a extrage informatii despre directoare si fisiere intr-un mod generic si indemendent de sistemul de operare. Path poate fi folosit pentru a face operatii peste caile in sistemul de fisiere cum ar fi concatenarea cailor care sunt concatenate prin "/" pe Linux si "\" pentru Windows sau pentru a extrage extensia fisierelor.
Directory si File sunt folosite pentru a crea sau sterge directorare, respectiv fisiere, si pentru a itera peste continutul unui director. Pentru a extrage mai multe informatii despre acestea se pot folosi clasele DirectoryInfo si FileInfo inclusiv legate de data crearii si a ultimei modificari. Mai jos sunt cateva exemple de cum se pot folosi.
private const string CurrentPath = ".";
private const string DirectoryName = "TestDirectory";
private const string FileName = "TestFile";
private static void RunDirectoryExample()
{
// Cu Path putem sa identificam o cale absoluta de la una relativa.
var fullPath = Path.GetFullPath(CurrentPath);
Console.WriteLine("Full path for current directory is \"{0}\"", fullPath);
// Putem cere informatiile despre directorul dat prin clasa DirectoryInfo.
var directoryInfo = new DirectoryInfo(fullPath);
Console.WriteLine("Current directory was created at: {0}", directoryInfo.CreationTimeUtc);
// Ca sa concatenam cai fara sa stim care e sistemul de operate folosim Path.Join.
var newDirectoryPath = Path.Join(CurrentPath, DirectoryName);
// Directory expune metode de a verifica daca exista direcotorul si ca sa cream unul.
if (!Directory.Exists(newDirectoryPath))
{
Console.WriteLine("Creating new directory \"{0}\"", newDirectoryPath);
Directory.CreateDirectory(newDirectoryPath);
}
else
{
Console.WriteLine("Directory \"{0}\" already exists", newDirectoryPath);
}
Console.WriteLine("The directories found are:");
// Putem itera prin ce directorare sunt continute intr-o cale.
foreach (var directory in Directory.GetDirectories(CurrentPath))
{
Console.WriteLine(directory);
}
Console.WriteLine("The files found are:");
// La fel si pentru fisiere.
foreach (var file in Directory.GetFiles(CurrentPath))
{
Console.WriteLine(file);
}
Console.WriteLine("-------------------");
}
private static void RunFileExample()
{
var newFilePath = Path.Join(CurrentPath, FileName);
/* Cand deschidem un fisier ii dam modul de deschidere, aici ca sa fie deschis sau
* sa fie creat daca nu exista si modul de acces, aici vrem sa si citi si scrie.
* Obiectul la Open este un FileStream care mosteneste clasa Stream.
*/
using var file = File.Open(newFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
/* Pentru a lucra mai simplu cu stream-uri se folosesc cu StreamReader si StreamWriter
* pentru ca adauga peste lucrul cu bytes a lui FileStream metode care lucreaza cu text.
*/
using var reader = new StreamReader(file);
using var writer = new StreamWriter(file);
// Exista mai multe metode de citire, aici citim fisierul pana la capat.
var fileContents = reader.ReadToEnd();
Console.WriteLine("The file contents are:");
Console.WriteLine("-------------------");
Console.WriteLine(fileContents);
Console.WriteLine("-------------------");
var random = new Random().Next();
Console.WriteLine("Writing new random number to file: {0}", random);
// Putem scrie exact ca la consola in fisier cu StreamWriter
writer.WriteLine(random);
// La fel cum preluam informatii despre directoare putem faca la fel cu FileInfo pentru fisiere.
var fileInfo = new FileInfo(newFilePath);
Console.WriteLine("Last file write was at: {0}", fileInfo.LastWriteTimeUtc);
Console.WriteLine("-------------------");
}
Clasa Stream
Cand lucram cu fisiere nu vom lucra direct pe fisier, vom folosi mai multe niveluri de abstractizare. Peste fisier se deschide un FileStream care mosteneste clasa abstracta Stream. Clasa Stream abstractizeaza idea de flux de date binare, iar orice fisier se poate considera un flux de bytes, si poate fi fie de citire, fie de scriere, fie de ambele.
Peste un Stream putem folosi StreamReader si StreamWriter pentru a avea mai multe facilitati deoarece clasa Stream este gandita doar pentru a extrage siruri de bytes cu metodele de Read si Write care primesc un buffer pentru a citi din el, respectiv a scrie in el. StreamReader si StreamWriter adauga operatii cu date de tip text peste un Stream pe care il incapsuleaza, se poate vedea in exemplul anterior cum se pot folosi.
Clasa Stream este intalnita si in lucrul cu alte tipuri de fluxuri de date, fisierele sunt una, apoi putem avea un stream de date in memorie prin MemoryStream care poate fi folosit pentru a copia alte fluxuri de date din exterion in memoria programului sub forma binara. Alte utilizari pentru clasa stream este preluarea de date venite din retea de la o conexiune TCP printr-un NetworkStream.
In general, clasa Stream va fi folosita predominant pentru procesare de IO, dar chiar daca este o clasa abstracta exceptiile emise si comportamentul la citire si scriere va diferii in functie de tipul stream-ului, deci trebuie consultata documentatia de cum se folosesc in fiecare caz particular.
Orice Stream implementeaza interfata IDisposable pentru ca in aceasta exista resurese ce trebuie eliberate cum sunt descriptori de fisier si socketi care sunt urmarite de sistemul de operare iar prin aceasta interfata se asigura ca exista o metoda de a elibera aceste resurse. Dupa ce este folosit un stream trebuie apelata metoda Dispose sau daca nu mai e folosit obiectul la iesire din scope-ul curent se poate folosi cuvantul cheie using la initializarea variabilei pentru a apela Dispose la iesire din scope automat chiar si in caz de exceptie.