Práce s textovými soubory
Úvod
V této kapitole si ukážeme, jak číst a zapisovat do textových souborů.
Cesty
Než si ukážeme konkrétní třídy a metody pro práci se soubory, vysvětlíme si některá fakta ohledně zápisu cest k souborům v jazyce C#. K přístupu do podadresářů používáme lomítka. V unixových systémech používáme klasické lomítko (/) a na platformě Windows lomítko zpětné (\). Jazyk C# umožňuje použití obou možností, ale z historických důvodů bychom důrazně doporučovali držet se lomítka zpětného.
# Zpětné lomítko
Kromě znázorňování hierarchické struktury adresářů slouží lomítko i k tzv. escapování znaků. Pro lepší vysvětlení tohoto pojmu si vše ukážeme na jednoduchém příkladě. Představme si situaci, kdy chceme do stringové proměnné s uložit jednu uvozovku:
string s = """; // Ukázka 1
string s = "\""; // Ukázka 2
Kompilátor by uvozovku ve prostřed vnímal jako validní znak a ukončil by tak textový řetězec na pravé straně výrazu. Třetí uvozovka by pak byla vnímána jako další neukončený řetězec. Zpětné lomítko je cesta, jak uvozovku escapnout a říct tím kompilátoru, aby ji vnímal jako obyčejný literál a nikoliv jako klíčový znak (viz ukázka 2). Pokud tedy budeme chtít uložit cestu k souboru do stringové proměnné, musíme nějakým způsobem kompilátoru říct, aby zpětné lomítko vnímal jako literál pro oddělení adresářové struktury a nikoliv jako pokus o escapování následujícího znaku. Máme hned dvě možnosti, jak tohoto chování docílit
string cesta = "c:\\pepa\\aplikace\\data.txt"; // Možnost 1
string cesta = @"c:\pepa\aplikace\data.txt"; // Možnost 2
První možností je escapnout samotný znak escapnutí a použít tedy dvě zpětná lomítka. Druhou, více elegantnější možností je prefixovat celý řetězec znakem zavináče (@), čímž řekneme kompilátoru, aby všechny znaky v řetězci vnímal jako literály.
Pokud vyvíjíte na unixovém operačním systému (např. MacOS nebo Linux), musíte používat klasické lomítka (/)!
# Relativní cesty
Jazyk C# nám dává možnost odvíjet cesty i relativně od rootovského adresáře. Root neboli kořenový adresář programu je taková složka, kde se nachází spustitelný .exe soubor (nebo jiný spouštěcí soubor) naší aplikace. Standardně je to složka název-projektu\bin\debug\. Cestu označíme jako relativní, pokud na její začátek uvedeme tečku (.):
string relativniCesta = @".\data"; // Relativní cesta
string absolutniCesta = @"c:\aplikace\mujProgram\data"; // Absolutní cesta
Vždy je lepší cesty odvíjet relativně od spouštěcího souboru, neboť absolutní cesty mohou přestat fungovat v momentě, kdy program spustíme na jiném počítači. V ukázce 2 je pak na prohlédnutí i cesta absolutní. Pro vstoupení do vyšší adresářové struktury (rozuměj pro opuštění současné složky) pak slouží tečky dvě:
// Začneme v kořenovém adresáři, vystoupíme o úroveň výše
// a poté vstoupíme do složky se jménem "data" a vybereme
// soubor se jménem "soubor.txt":
string cesta = @".\..\data\soubor.txt";
/*
aplikace/
├── root/ (kořenový adresář)
│ └── aplikace.exe
├── data/
│ └── soubor.txt (vybraný soubor)
└── src/
├── obrazek.png
└── hudba.mp3
*/
Třída File
Nejjednodušší způsob, jak manipulovat s textovým souborem je třída File. Ta obsahuje statické metody, které pro nás obstarají všechny základní funkčnosti, které bychom mohli při práci se soubory potřebovat. Třída File je šikovná i v tom, že soubor sama otevře i uzavře, takže tuto činnost nemusíme provádět ručně. Abychom mohli třídu File vůbec používat, musíme našemu projektu říct, aby využíval namespace System.IO, který třídu obsahuje. Uděláme to pomocí klíčového slova using:
using System.IO;
Tento namespace (i jakýkoliv jiný) si pochopitelně nemusíme pamatovat zpaměti. Každé dobré vývojové prostředí nám tento namespace sám nabídne k doplnění v momentě, kdy se na třídu File odkážeme.
# Vytvoření souboru
Začneme od nejjednoduššího a ukážeme si, jak vytvoříme nový .txt soubor. K tomuto účelu slouží metoda Create():
// Vytvoří textový soubor v kořenovém adresáři
File.Create("soubor.txt");
Pokud vytvářený soubor již existuje, dojde k přepsání toho stávajícího!
Třída dále nabízí metody Move() a Delete(), jejichž názvy jsou více méně sebepopisné:
// Přesun souboru "soubor.txt" z kořenového adresáře
// do složky "data"
File.Move("soubor.txt", @".\data\soubor.txt");
// Smaže soubor "soubor.txt" z
// kořenového adresáře
File.Delete("soubor.txt");
# Čtení ze souboru
Nyní se podíváme na metody určené ke čtení ze souborů. Dvě nejčastěji používané z nich jsou ReadAllText() a ReadAllLines(). Oběma stačí v závorce pouze cesta k souboru:
// Načte kompletně celý obsah souboru do stringové proměnné
string obsah = File.ReadAllText("soubor.txt");
// Načte obsah celého souboru po řádcích do pole
string[] radky = File.ReadAllLines("soubor.txt");
Tyto metody načítají obsah souboru do RAM paměti počítače. Pokud bychom pracovali s opravdu velkým textovým souborem v řádech statisíců znaků, máme pro tyto účely třídu StreamReader, která je na takovéto situace lépe uzpůsobená.
Obě metody dělají totéž, ale každá vrátí výstupní data v jiném datovém typu. Metoda ReadAllText() vrací celý obsah v jedné stringové proměnné a nové řádky odděluje symbolem pro to určeným, kdežto ReadAllLines() vrací stringové pole, kde každý záznam je právě jeden řádek.
Symbol(y) pro nový řádek je na platformě Windows \r\n a na unixových operačních systémech \n. Pokud si nejsme jistí, jaký symbol použít, máme pro tyto účely k dispozici i vlastnost Environment.NewLine
# Zápis do souboru
K jednoduchému zápisu využíváme metody WriteAllText() a WriteAllLines(). Fungují podobně, jako jejich protějšky na čtení:
// Pole s textem
string[] pole = new string[] {"Věta 1", "Věta 2"};
// Zápis do souboru
WriteAllText("soubor.txt", "Text, který chceme zapsat do souboru.");
WriteAllLines("soubor.txt", pole);
Opět platí, že pokud cílový soubor neexistuje, vytvoří se nový. Pokud textový soubor již obsahuje nějaký text, dojde k jeho přemazání. Pokud nechceme přepsat celý soubor a ztratit tím všechna data, co v něm předtím byla, můžeme použít metodu AppendAllText(). Tato metoda vezme obsah, který v souboru byl a na konec přidá námi zadaný text:
File.AppendAllText("soubor.txt", "Text, který se přidá na konec, bez přepsání původního obsahu souboru.");
Dalším způsobem jak pracovat s textovými soubory jsou třídy StreamReader a StreamWriter. Tyto třídy nenačítají soubory do své paměti celé najednou, ale umožňují z nich číst nebo do nich zapisovat řádek po řádku. Daní za tento fakt je zdlouhavější zápis, neboť musíme vytvářet jejich instance a poté i ručně uzavírat datový stream. Výhodou tohoto přístupu je však možnost pracovat s objemnými soubory v poměrně krátkém čase za rozumného vytížení paměti.
# Třída StreamReader
Když vytvoříme instanci třídy StreamReader, dojde k otevření souboru, který jsme mu dodali jako argument. V praxi to znamená, že od této doby k němu nedostane přístup žádný jiný program. Aby se nám nestalo, že necháme nějaký soubor "zamknutý" musíme ho vždy poté, co jsme s ním hotovi, zavřít pomocí metody Close():
// Vytvoření nové instance třídy StreamReader
StreamReader reader = new StreamReader("soubor.txt");
// ... logika čtení
// Uzavření streamu
reader.Close();
Druhou naší možností je samotné instancování třídy obalit do bloku using, jež sám zajistí uzavření streamu:
V prvním ročníku jsme zmínili existenci tzv. garbage collectoru, který automatizuje správu paměti. V praxi to znamená, že pokud založíme novou proměnnou, běhové prostředí automaticky rozpozná, kdy proměnná již nebude potřeba a alokovanou (rozuměj zabranou) paměť samo uvolní. Některé zdroje jako naše textové soubory jsou však mimo kompetence tohoto collectoru a o jejich uvolnění se musíme postarat sami. Třídy pracující s takovýmito zdroji implementují rozhraní IDisposable, které nutí danou třídu implementovat metodu Dispose(), jež se stará o uklizení "nepořádku" po volání takovýchto tříd. Mezi takovéto třídy spadají i naše třídy StreamReader a StreamWriter. Jejich použití v using bloku zajistí zavolání výše zmíněné Dispose() metody a vyhneme se tak tudíž potencionálním nepříjemnostem. Tato informace je spíše doplňující látkou pro pokročilé.
Díky tomu, že nám soubory zůstávají otevřené, nemusíme číst celý soubor najednou, ale můžeme s ním manipulovat jen po řádcích či znacích. Když používáme jednotlivé metody na čtení, náš reader si zapamatuje pozici, na které přestal znaky číst:
// Vytvoření nové instance třídy StreamReader
using (StreamReader reader = new StreamReader("soubor.txt"))
{
// Dokud nám zbývají nepřečtené znaky
while (reader.Peek() >= 0)
{
// Převede výstup metody Read() z intu
// (ASCCI hodnota znaku) na obyčejný znak (char)
// a poté vypíše výsledek
Console.Write((char)sr.Read());
}
}
Takovýto kód by obsah souboru přečetl po jednom znaku a každý zvlášť vypsal do konzole. Podobně si počínáme i v případě, kdy chceme souborem iterovat po řádcích:
using (StreamReader reader = new StreamReader("soubor.txt"))
{
// Pomocná proměnná pro uložení řádku
string line;
// Dokud nám zbývají další řádky
while ((line = reader.ReadLine()) != null)
{
// Vypíšeme získaný řádek do konzole
Console.WriteLine(line);
}
}
# Třída StreamWriter
Pro zapisování máme ve třídě StreamWriter jen dvě metody - Write() a WriteLine(). Jejich použití je téměř shodné. Obě varianty do souboru zapíší předaný řetězec, ale metoda WriteLine() ještě na konec přidá nový řádek
using (StreamWriter writer = new StreamWriter("soubor.txt"))
{
// Zapíše řetězec na aktuální řádek
writer.Write("Požadovaný řetězec");
// Zapíše řetězec na nový řádek
writer.WriteLine("Budu na novém řádku!");
}
Podobně jako u třídy StreamReader musíme opět uzavřít stream buď voláním metody writer.Close() nebo obalením celého výrazu using blokem tak, jako na ukázce výše.
Shrnutí
V této kapitole jsme se seznámili se zápisem a čtením z textových souborů. Měli bychom být obeznámeni se třídami File, StreamReader a StreamWriter.
No comments to display
No comments to display