# 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:

<div class="el-tabs__header is-top" id="bkmrk-uk%C3%A1zky"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-uk%C3%A1zky-1" role="tab" tabindex="0">Ukázky</div></div></div></div></div>```c#
string s = """; // Ukázka 1
string s = "\""; // Ukázka 2
```

<div class="el-tabs__content" id="bkmrk-"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--1" role="tabpanel"></div></div>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

<div class="el-tabs__header is-top" id="bkmrk-mo%C5%BEnosti"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-mo%C5%BEnosti-1" role="tab" tabindex="0">Možnosti</div></div></div></div></div>```c#
string cesta = "c:\\pepa\\aplikace\\data.txt"; // Možnost 1
string cesta = @"c:\pepa\aplikace\data.txt"; // Možnost 2
```

<div class="el-tabs__content" id="bkmrk--2"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--3" role="tabpanel"></div></div>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.

<p class="callout info">Pokud vyvíjíte na unixovém operačním systému (např. MacOS nebo Linux), musíte používat klasické lomítka (`/`)!</p>

 **# 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 (`.`):

<div class="el-tabs__header is-top" id="bkmrk-cesty-1"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-cesty-2" role="tab" tabindex="0">Cesty</div></div></div></div></div>```c#
string relativniCesta = @".\data"; // Relativní cesta
string absolutniCesta = @"c:\aplikace\mujProgram\data"; // Absolutní cesta
```

<div class="el-tabs__content" id="bkmrk--4"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--5" role="tabpanel"></div></div>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ě:

<div class="el-tabs__header is-top" id="bkmrk-notace-dvou-te%C4%8Dek"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-notace-dvou-te%C4%8Dek-1" role="tab" tabindex="0">Notace dvou teček</div></div></div></div></div>```c#
// 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

*/
```

<div class="el-tabs__content" id="bkmrk--6"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--7" role="tabpanel"></div></div>### 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`:

<div class="el-tabs__header is-top" id="bkmrk-namespace-import"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-namespace-import-1" role="tab" tabindex="0">Namespace import</div></div></div></div></div>```c#
using System.IO;
```

<div class="el-tabs__content" id="bkmrk--8"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--9" role="tabpanel"></div></div><p class="callout info">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.</p>

**\# Vytvoření souboru**

Začneme od nejjednoduššího a ukážeme si, jak vytvoříme nový `.txt` soubor. K tomuto účelu slouží metoda `Create()`:

<div class="el-tabs__header is-top" id="bkmrk-vytvo%C5%99en%C3%AD-souboru"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-vytvo%C5%99en%C3%AD-souboru-1" role="tab" tabindex="0">Vytvoření souboru</div></div></div></div></div>```c#
// Vytvoří textový soubor v kořenovém adresáři
File.Create("soubor.txt");
```

<div class="el-tabs__content" id="bkmrk--10"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--11" role="tabpanel"></div></div><p class="callout danger">Pokud vytvářený soubor již existuje, dojde k přepsání toho stávajícího!</p>

Třída dále nabízí metody `Move()` a `Delete()`, jejichž názvy jsou více méně sebepopisné:

<div class="el-tabs__header is-top" id="bkmrk-p%C5%99esunut%C3%AD-souboru"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-p%C5%99esunut%C3%AD-souboru-1" role="tab" tabindex="0">Přesunutí souboru</div></div></div></div></div>```c#
// Přesun souboru "soubor.txt" z kořenového adresáře
// do složky "data"
File.Move("soubor.txt", @".\data\soubor.txt");
```

<div class="el-tabs__content" id="bkmrk--12"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--13" role="tabpanel"></div></div><div class="el-tabs__header is-top" id="bkmrk-smaz%C3%A1n%C3%AD-souboru"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-smaz%C3%A1n%C3%AD-souboru-1" role="tab" tabindex="0">Smazání souboru</div></div></div></div></div>```c#
// Smaže soubor "soubor.txt" z
// kořenového adresáře
File.Delete("soubor.txt");
```

<div class="el-tabs__content" id="bkmrk--14"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--15" role="tabpanel"></div></div>**\# Č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:

<div class="el-tabs__header is-top" id="bkmrk-%C4%8Cten%C3%AD-ze-souboru"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-%C4%8Cten%C3%AD-ze-souboru-1" role="tab" tabindex="0">Čtení ze souboru</div></div></div></div></div>```c#
// 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");
```

<div class="el-tabs__content" id="bkmrk--16"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--17" role="tabpanel"></div></div><p class="callout warning">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á.</p>

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.

<p class="callout warning">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`</p>

**\# 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í:

<div class="el-tabs__header is-top" id="bkmrk-z%C3%A1pis-do-souboru"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-z%C3%A1pis-do-souboru-1" role="tab" tabindex="0">Zápis do souboru</div></div></div></div></div>```c#
// 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);
```

<div class="el-tabs__content" id="bkmrk--18"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--19" role="tabpanel"></div></div>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:

<div class="el-tabs__header is-top" id="bkmrk-z%C3%A1pis-do-souboru-2"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-z%C3%A1pis-do-souboru-3" role="tab" tabindex="0">Zápis do souboru</div></div></div></div></div>```c#
File.AppendAllText("soubor.txt", "Text, který se přidá na konec, bez přepsání původního obsahu souboru.");
```

<div class="el-tabs__content" id="bkmrk--20"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--21" role="tabpanel"></div></div>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()`:

<div class="el-tabs__header is-top" id="bkmrk-zalo%C5%BEen%C3%AD-streamreade"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-zalo%C5%BEen%C3%AD-streamreade-1" role="tab" tabindex="0">Založení StreamReaderu (1)</div></div></div></div></div>```c#
// Vytvoření nové instance třídy StreamReader
StreamReader reader = new StreamReader("soubor.txt");

// ... logika čtení

// Uzavření streamu
reader.Close();
```

<div class="el-tabs__content" id="bkmrk--22"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--23" role="tabpanel"></div></div>Druhou naší možností je samotné instancování třídy obalit do bloku `using`, jež sám zajistí uzavření streamu:

<p class="callout info">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é.</p>

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:

<div class="el-tabs__header is-top" id="bkmrk-%C4%8Cten%C3%AD-po-znaku"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-%C4%8Cten%C3%AD-po-znaku-1" role="tab" tabindex="0">Čtení po znaku</div></div></div></div></div>```c#
// 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());
    }
}
```

<div class="el-tabs__content" id="bkmrk--24"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--25" role="tabpanel"></div></div>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:

<div class="el-tabs__header is-top" id="bkmrk-%C4%8Cten%C3%AD-po-%C5%99%C3%A1dc%C3%ADch"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-%C4%8Cten%C3%AD-po-%C5%99%C3%A1dc%C3%ADch-1" role="tab" tabindex="0">Čtení po řádcích</div></div></div></div></div>```c#
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);
    }
}
```

<div class="el-tabs__content" id="bkmrk--26"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--27" role="tabpanel"></div></div>**\# 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

<div class="el-tabs__header is-top" id="bkmrk-z%C3%A1pis-do-souboru-4"><div class="el-tabs__nav-wrap is-top"><div class="el-tabs__nav-scroll"><div class="el-tabs__nav is-top" role="tablist"><div aria-controls="pane-0" aria-selected="true" class="el-tabs__item is-top is-active" id="bkmrk-z%C3%A1pis-do-souboru-5" role="tab" tabindex="0">Zápis do souboru</div></div></div></div></div>```c#
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!");
}
```

<div class="el-tabs__content" id="bkmrk--28"><div aria-labelledby="tab-0" class="el-tab-pane" id="bkmrk--29" role="tabpanel"></div></div>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`.