Naučit se nějaký programovací jazyk, třeba Visual Basic .NET či C#, je snadné. Daleko větším problémem bývá ale často naučit se obrovskou škálu funkcí, které nám .NET Framework poskytuje. Znám spoustu vývojářů, kteří přejdou na C# a za týden v něm prý umí programovat, je to přece tak snadné. Ale otrocky píší funkce, které už .NET má naimplementované. Navíc, když si vše píší sami, nepokryjí třeba všechny případy, které mohou nastat, a pak se diví, když jim kód nedělá, co má.
Právě proto jsem se rozhodl napsat tento článek - jedna věc je znát jazyk a druhá věc je znát funkce a třídy .NET frameworku tak, abychom jej efektivně využívali a nepsali znovu to, co napsal někdo jiný a v mnoha případech ještě k tomu lépe. V tomto článku tedy najdete několik užitečných funkcí, které mě zrovna napadly. Tento seznam není rozhodně úplný, pokud budete mít nějaké nápady, co přidat, určitě napište do diskuse, ať můžu udělat pokračování článku. Budu se snažit také poukázat na některé chyby, které začátečníci obvykle dělají.
Zápis a čtení textových souborů
StreamReader a StreamWriter jsou dvě velmi často používané třídy, které čtou, resp. zapisují z/do textového souboru. Známe je skoro všichni, StreamReader má metody ReadLine a Read, první z nich přečte jeden řádek v souboru a druhá umí načíst ze souboru zvolený počet znaků (typ Char). StreamWriter má medoty WriteLine a Write, první z nich zapíše do souboru řádek textu a druhá má spoustu různých přetížení, takže umí do souboru zapsat čísla, pole znaků, datum atd. Velmi důležité je při ukončení práce se souborem zavolat Close. Pokud jej nezavoláme při čtení souboru, tak se toho zase tolik nestane, až na to, že soubor bude nepřístupný, dokud je aplikace spuštěná. Pokud jej ale nezavoláme při zápisu do souboru, mohou se dít zajímavé věci. Zavolání Write a WriteLine totiž nezapisuje římo na disk, ale do paměťového bufferu, který se ukládá až když je toho více. To vše se děje kvůli vyššímu výkonu, zapsání dat na disk je poměrně pomalé. Pokud vám ale aplikace spadne, může se stát, že i když do souboru již delší dobu nezapisujete, data jsou stále ještě v paměti a ne na disku.
Na těchto dvou třídách je zajímavý ještě jeden detail, a to volba kódování národních abeced. Kdyby Jan Hus kritizoval raději jen církev a nešťoural se i v jazyce českém, měli bychom o dost lehčí život. Ale vzhledem k tomu, že počítače vznikaly převážně v zemích, kde podobná individua nepůsobila, na háčky a čárky se tak nějak zapomnělo. Proto dnes máme několik různých kódování - Windows 1250, ISO 8859-2 a UTF8. Vřele doporučuji vždy a všude používat kódování UTF8, protože podporuje téměř všechny možné i nemožné znaky na světě. Abychom měli jistotu, že se bude ukládat a načítat v kódování, které opravdu chceme, je dobré jej nastavit při vytváření objektů.
Dim sw As New IO.StreamWriter("c:\soubor.txt", False, System.Text.Encoding.UTF8)
Při vytváření StreamReaderu lze nastavit, že si má objekt vyvěštit použité kódování sám. Můžeme ale kódování nastavit napevno sami, pokud máme jistotu, že dostaneme soubory jen v tomto kódování.
Načtení a zápis do souboru na jeden řádek
Pokud potřebujeme provést nějakou jednoduchou operaci se souborem, jako třeba načíst jej celý do proměnné, nebo naopak Stringovou proměnnou uložit do souboru, stačí nám na to funkce ReadAllText a WriteAllText. Mají dokonce i ekvivalenty pro pole bajtů, a to ReadAllBytes a WriteAllBytes. Použití je jednoduché:
Dim text As String = IO.File.ReadAllText("c:\soubor.txt")
IO.File.WriteAllText("c:\soubor.txt", text.ToUpper())
Hodí se často ještě funkce ReadAllLines, která načte textový soubor a vrátí pole Stringů, kde každá položka pole je jeden řádek v tomto souboru. Jen dejte pozor, abyste nenačítali příliš velké soubory přímo do paměti, pokud je velikost souboru v řádech megabajtů, použití těchto funkcí již většinou není na místě!
Práce s cestami
Velmi zajímavá je třída System.IO.Path. Ta obsahuje spoustu funkcí pro práci s cestou, které si spousta lidí často píše sama. Proč se ale namáhat? Většinu funkcí pro práci s cestou k souboru již máme připravenou.
Sloučení dvou částí cesty je operace velice častá, na to máme funkci Combine. Nemusíme tedy dávat pozor, aby první část končila nebo druhá začínala lomítkem, díky čemuž by se cesta složila špatně. Použití funkce Combine je snadné:
Dim cesta As String = IO.Path.Combine(Application.StartupPath, "templates\sablona.xml")
Občas potřebujeme z cesty vytáhnout pouze jméno souboru. Ano, šlo by to napsat velmi jednoduše pomocí metody Substring a LastIndexOf (stačí najít poslední lomítko v řetězci a vzít jen část za ním). Ale osobně preferuji způsob, který "říká, co se děje", a ne nějakou práci se Stringem. Navíc pokud vám někdo do adresy napíše místo zpětných lomítek lomítko obyčejné, už je to problém. Máme na to přece funkci GetFileName.
Dim nazev As String = IO.Path.GetFileName(cesta)
Velmi podobná je funkce GetFileNameWithoutExtension, která také vrátí jméno souboru, ale bez přípony. A ještě často používám funkci GetDirectoryName, která vrátí cestu k adresáři, ve kterém se nachází daný soubor. Je to vlastně opačná funkce k GetFileName - GetFileName odřízne cestu a nechá jen název souboru, GetDirectoryName odřízne název souboru a nechá jen cestu k jeho rodičovskému adresáři.
Potřebuji dočasný soubor
Občas vaše aplikace potřebuje dočasný soubor pro uložení něčeho. Jednoduché, vygenerujete náhodně nějaký název a uložíte. A jako na potvoru si přepíšete jiný dočasný soubor, takže musíte přidat ještě kontrolu, jestli takový soubor s náhodným názvem již neexistuje. Ale to už zase není na jeden řádek. Proto je rychlejší a lepší použít funkci GetTempFileName z naší třídy System.IO.Path.
Dim tempFilePath As String = IO.Path.GetTempFileName()
Pokud použijete tuto funkci, máte jistotu, že do daného souboru budete mít práva zápisu a tento soubor bude také uložen v adresáři, který je na to určen, a ne někde v Program Files, kam slušné aplikace nikdy nezapisují, pokud to nejsou instalátory.
Cesty ke speciálním složkám ve Windows
Poslední podkapitolou týkající se souborového systému jsou cesty ke speciálním složkám, jako např. Application Data, Dokumenty, Temp, Obrázky, Videa, Program Files atd. Téměř v každé verzi Windows jsou tyto cesty maličko jinačí. Rozhodně nikdy nevypisujte tyto cesty natvrdo, já osobně si třeba téměř všechny tyto cesty měním - na disku C: mám jen Windows a Program Files, ale Dokumenty a všechno ostatní mám na disku D. Když jednou za rok přeinstalovávám, nemusím nic zálohovat, nemusím hledat, kde jsem ještě co zapomněl. Občas se ale objeví (zejména starší) aplikace, která má někde cestu uvedenou pevně a neptá se systému, kde že má dokumenty. Aby se nám tohle nestávalo, máme na to funkci Environment.GetFolderPath, které jako parametr předáme, jakouže to cestu chceme. Vše se nám ukáže v kontextové nabídce IntelliSense, stačí si jen vybrat. Takto třeba získáme cestu ke složce Obrázky.
Dim obrazky As String = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)
Je libo něco změřit?
Občas se vyskytne potřeba změřit, jak dlouho něco trvá. Asi nejjednodušší je použít funkci Environment.TickCount, která vrací počet milisekund od startu systému. Na extra přesné měření rozhodně nění, má přesnost asi 15-20ms, ale na běžné použití bohatě dostačuje.
Dim start As Long = Environment.TickCount
'... nějaká časově náročnější operace
MsgBox(Environment.TickCount - start)
Spojování Stringů
Potřebujete poskládat nějaký delší text ze spousty textů menších? Tak na to pozor, pokud nepoužijete StringBuilder, můžete proces velice zpomalit. Pokud totiž napíšu a &= "abc", protože chci za to, co už v a je připojit text "abc", vytvoří se v paměti nová proměnná, zkopíruje se do ní původní obsah a a přidají se za ni nová 3 písmenka. Stará proměnná a se zahodí. To je náročné nejen paměťově (takto zaplácanou paměť je potřeba odstranit), tak i časově, zbytečně kopírujeme tuny textu kvůli přidání 3 písmenek. A právě proto zde máme třídu StringBuilder, která to udělá tak, jak bychom čekali. Jen tak pro zajímavost, obyčejné spojování Stringů v první ukázce trvalo něco přes 25 sekund, zatímco při použití StringBuilderu byl čas neměřitelný (ukázalo se 0ms, přesnost měření ale byla asi 20ms, používal jsem TickCount). Rozdíl je tedy opravdu dramatický.
'takhle určitě NE!
Dim t As String = ""
For i As Integer = 1 To 50000
t &= "abcdefg"
Next
'takhle už ano
Dim sb As New System.Text.StringBuilder()
For i As Integer = 1 To 50000
sb.Append("abcdefg")
Next
Dim t As String = sb.ToString()
Na druhou stranu, pokud skládáte deset krátkých slov, pak je použití StringBuilderu spíš pomalejší, vytvoření objektu a volání ToString, abychom získali finální řetězec, totiž něco stojí. Pokud ale slučujete delší texty nebo velký počet textů, pak není co řešit.
Převod Stringu na bajty a opačně
Často potřebujeme text převést na pole bajtů, protože jej potřebujeme např. zašifrovat nebo poslat po síti. Šifrovací a síťové funkce pracují s poli bajtů, proto musíme mít funkce pro převod hodnoty String na pole bajtů. Není to až tak triviální, protože do 1 bajtu můžeme uložit pouze 256 různých čísel, zatímco písmenek máme daleko více. Znaková sada Unicode jich čítá přes 100 000, z toho tedy logicky plyne, že některé znaky musí zabírat více než 1 bajt. Pro převod tedy máme funkce GetBytes a GetString, které najdeme u třídy s kódováním, které chceme použít. Pokud tedy kódujeme ve Windows 1250, použijeme System.Text.Encoding.Default.GetBytes, pokud chceme UTF-8, pak použijeme System.Text.Encoding.UTF8.GetBytes atd. Příklad je zde:
Dim text As String = "Příliš žluťoučký kůň úpěl ďábelské ódy."
Dim bajty() As Byte = System.Text.Encoding.UTF8.GetBytes(text) 'převést text na bajty
Dim text2 As String = System.Text.Encoding.UTF8.GetString(bajty) 'získat text zpátky
Formátování textu, data a čísel
V mnoha aplikacích potřebujeme nějak přehledně zobrazit určité údaje, jako např. datum, čas anebo nějakou částku. Protože různé národy mají různě šílené zvyklosti pro zapisování data, času, měny a čísel, .NET framework obsahuje robustní část věnovanou právě lokálním zvyklostem a nastavení. To je ale téma na samostatný článek, spíše bych chtěl ukázat formátovací funkce, které se velmi často hodí.
Jistě mi dáte za pravdu, že první zápis v MsgBoxu není tak přehledný jako ten druhý, a přitom oba dva udělají skoro to samé:
Dim castka As Decimal = 134.5
Dim zacatek As Date = #1/1/2007#
Dim konec As Date = #3/31/2007#
MsgBox("Za období '" & zacatek & "' - '" & konec & "' máte přeplatek " & castka & " Kč.")
MsgBox(String.Format("Za období '{0}' - '{1}' máte přeplatek {2:c}.", zacatek, konec, castka))
V prvním MsgBoxu poskládáme větu s daty a částkou. Přes všechno skládání jablek a hrušek do sebe zaniká pravá podstata toho, co chceme udělat. Kdybychom třeba chtěli aplikaci lokalizovat do jiného jazyka s jiným slovosledem, asi by se nám to dělalo hodně těžko. Daleko jednodušší je použít funkci String.Format, které řekneme, kam se má do věty co dosadit. Texty pro lokalizaci můžeme mít uloženy úplně někde mimo a když je bude překladatel opravovat, tak může i změnit slovosled a vše půjde hladce.
Na místo, kde je {0} se dosadí proměnná zacatek, na místo {1} se dosadí proměnná konec a na místo {2} se dosadí proměnná castka. Navíc můžeme specifikovat formát, v jakém se má částka zobrazit - pokud napíšeme {2:c}, zformátuje se hodnota jako měna (134.5 se zobrazí jako 134,50 Kč) podle toho, jaké jazykové prostředí máte nastaveno v systému Windows (samozřejmě lze ale i říci, že chcete použít nějaké konkrétní, to už je jedno). Dosazené datum se nám zobrazí i s časem, což se nám moc nehodí, takže můžeme napsat {0:d}, aby se zobrazilo jenom datum, tedy 1.1.2007, anebo můžeme napsat {0:D}, aby se vypsalo dlouhé datum, tedy 1. ledna 2007. Podrobnější informace o formátování najdete v nápovědě, jsou tam přesně popsány možnosti formátování a některé předdefinované formáty. Já zde uvedu jen ty nejzajímavější:
Desetiná čísla |
c |
měna |
134,50 Kč |
|
f05 |
číslo na 5 desetinných míst (místo 05 dosaďte požadovaný počet míst) |
3,14159 |
|
p02 |
hodnota v procentech na 2 desetinná místa (číslo se vynásobí 100, takže pro zobrazení 50% předejte hodnotu 0,5) |
57,64% |
|
n03 |
dlouhá čísla s mezerami a oddělovači tisíců, milionů atd (pro celá čísla zvolte n00) |
123 456 789,003 |
|
e06 |
vědecký formát (mantisa . 10exponent) |
1,346789e+019 |
Datum a čas |
d |
krátké datum |
1.1.2007 |
|
D |
dlouhé datum |
1. ledna 2007 |
|
t |
krátký čas |
14:27 |
|
T |
dlouhý čas |
14:27:40 |
|
g |
krátké datum, krátký čas |
31.12.2007 14:28 |
|
G |
krátké datum, dlouhý čas |
31.12.2007 14:28:27 |
|
f |
dlouhé datum, krátký čas |
31. prosince 2007 14:28 |
|
F |
dlouhé datum, dlouhý čas |
31. prosince 2007 14:28:27 |
|
s |
ISO 8601 (používají některé databáze) |
2007-12-31T14:30:28 |
|
r |
RFC 1123 (standardizovaný formát) |
Mon, 31 Dec 2007 14:31:29 GMT |
Funkce String.Format tedy jako první parametr musí dostat formátovací řetězec, přičemž další parametry (může jich bát libovolný počet) do tohoto řetězce dosadí. Pokud chcete dosazovat vlastní datové typy či objekty, můžete, akorát jim musíte naimplementovat metudu ToString, ale to je nad rámec tohoto článku.
Seznamy pro jakákoliv data
Občas se nám stane, že potřebujeme pracovat s hromadou položek, akorát nikdy nevíme, kolik jich ještě bude. Pokud to víme předem, třeba i těsně před načtením, můžeme si udělat pole potřebné velikosti. Často to ale nevíme, dokud jsme položky nenačetli všechny a nezjistili, že už je konec.
.NET framework má velice hezkým způsobem naimplementovány nafukovací seznamy, které pojmou tolik položek, kolik budeme potřebovat, a navíc mohou být jakéhokoliv typu. Nemusíme do nich ale ukládat jako typ Object a pak vše přetypovávat zase zpět. My již při deklaraci řekneme, že chceme, aby seznam byl na hodnoty typu String. Tento seznam najdeme ve jmenném prostoru System.Collections.Generic.
Dim seznam As New List(Of String) 'vytvoříme seznam
seznam.Add("první položka") 'přidáme do něj položky
seznam.Add("druhá položka")
seznam.Add("třetí položka")
MsgBox(seznam(1).Substring(2)) 's položkou pracujeme jako se Stringem
Dim pole() As String = seznam.ToArray() 'ze seznamu můžeme dostat pole Stringů
seznam.RemoveAt(1) 'odebrání prvku ze seznamu
MsgBox(seznam.Count) 'počet prvků seznamu
seznam.Sort() 'setřídění seznamu
seznam.Clear() 'vyprázdnění seznamu
Zde jsou vidět často používané metody, které seznam nabízí. Metoda Add slouží k přidávání položek do seznamu, metoda RemoveAt odebere položku na určitém indexu, vlastnost Count vrací počet položek, metoda Sort umí seznam setřídit a metoda Clear jej vyprázdní.
Ještě je dobré vědět, jak takový seznam funguje uvnitř. Při vytvoření instance třídy má v sobě seznam pole o nějaké počáteční velikosti. Jak do něj postupně přidáváme prvky, pole se jednou zaplní. Jakmile se do něj již prvky nevejdou, vytvoří se nové pole, které je dvakrát větší, stávající hodnoty se do něj překopírují a přidá se nová položka, která se již nevešla. Přidávání je tedy rychlé až na chvíle, kdy se zrovna pole zvětšuje. Naštěstí můžeme v konstruktoru říci, jak velké má být pole na začátku, abychom proces optimalizovali. Výhoda tohoto seznamu je v tom, že jej můžeme použít pro opravdu jakýkoliv datový typ, díky generickému přístupu pouze v závorce za klíčovým slovem Of při deklaraci řekneme, že má být seznam na objekty NetworkStream, a máme nafukovací seznam na objekty, které posílají po síti nějaká data. Navíc je již při kompilaci známo, jakého jsou položky typu, proto když napíšeme seznam(1)., tak se nám hned zobrazí IntelliSense nápověda se všemi dostupnými metodami.
Slovník
Slovník je druhá chytrá věcička, která se nám může hodit. Je to vlastně tzv. name-value kolekce - jedná se o soubor nějakých hodnot, kde každá z nich má svoje jednoznačné ID, podle kterého s ní pracujeme. Používá opět generický přístup, takže jak klíč, tak samotná hodnota, může mít jakýkoliv datový typ na světě. Jak se tedy pracuje se System.Collections.Generic.Dictionary? Jednoduše:
Dim slovnik As New Dictionary(Of Integer, String) 'vytvoříme slovník
slovnik.Add(1, "jedna") 'přidáme do něj položky
slovnik.Add(2, "dvě")
slovnik.Add(3, "tři")
slovnik.Add(4, "čtyři")
If slovnik.ContainsKey(2) Then MsgBox("Dvojka je ve slovníku!") 'je dvojka ve slovníku?
slovnik.Remove(3) 'odebereme trojku
MsgBox(slovnik.Count()) 'zobrazit počet položek
MsgBox(slovnik(4)) 'vypsat čtyřku
slovnik.Clear() 'vyprázdnit slovník
Opět máme metody Add a Remove, metoda Add přidá nějakou hodnotu pod nějakým klíčem, metoda Remove odstraní hodnotu podle klíče. Metoda ContainsKey vrátí True, pokud je nějaký klíč již ve slovníku, slovnik(4) vrátí hodnotu, která je pod klíčem 4, a Count a Clear je stejné, jako u seznamu.
Slovník používá interně nějakou hashovací funkci, takže nalezení položky podle klíče je velmi rychlé.
To by bylo pro tento článek asi tak všechno, samozřejmě je to jen zanedbatelná část toho, co nám .NET framework umožňuje, nebavili jsme se o vláknech, neukázali jsme si namespace My, což je jakási VB.NET specialita atd. Ale je jasné, že se tohle vše do jednoho článku nemůže vejít, proto své jakékoliv připomínky pište do diskuse.