V tomto dílu si přiblížíme rozšířené možnosti, které nám prostředí VB.NET nabízí pro práci s polem prostřednictvím třídy System.Array.
V předchozích dílech seriálu jsme se podrobně seznámili s deklaracemi a inicializacemi pole. V případě výkladu některé ze starších verzí Visual Basic-u by tímto popis polí mohl skončit, protože prostředí VB6 s výjimkou metody Erase neobsahuje žádnou významnější podporu pro další práci s polem. Inicializace pole se zde musela provádět přiřazováním hodnot do jednotlivých prvků pole jednoho po druhém a třeba pro seřazení polí si programátor musel implementovat vlastní algoritmus. Naštěstí s nástupem platformy .NET se tato situace radikálně změnila a prostředí VB.NET nyní obsahuje řadu metod rozhraní pro usnadnění práce s poli, se kterými je užitečné se seznámit.
Třída System.Array
Jelikož v .NET pole představují normální objekty, lze v kódu přiřazovat jedno druhému jako normální proměnné stylem A = B. S ohledem na obsah pole je však přitom nutné respektovat následující snadno pochopitelná pravidla:
- Počet rozměrů zdrojového (A) a cílového (B) pole musí zůstat stejný. Naproti tomu, pokud je počet rozměrů (rank) pole při přiřazování zachován, počet prvků v jednotlivých rozměrech už shodný být nemusí. Jak již bylo řečeno, pole VB.NET jsou dynamická, a chybějící či přebývající počet prvků se při přiřazení automaticky doplní nebo vypustí:
Dim a(10, 20), b(5, 7), c(5, 11, 100)
a = b ` v pořádku
a = c ` chyba překladu, pole a(] a c() mají různý počet rozměrů (2 a 3)
- Oba typy polí musí mít prvky buďto referenčního typu, nebo hodnotového typu. A pokud jsou obě pole hodnotového typu, musejí mít shodný typ. Přiřazení pole hodnotového typu do referenčního či naopak povede k chybě konverze, stejně jako pokus o přiřazení pole jednoho hodnotového typu do druhého:
Dim a(10, 20) As Double, b(5, 6) As Integer, c(10, 20) As Object
a = b ` chyba překladu, pokus o přiřazení pole typu Integer do pole typu Double
a = c ` chyba překladu, pokus o přiřazení pole referenčního typu do pole hodnotového typu
- Pokud jsou obě pole referenčního typu, pokus o přiřazení při kompilaci selže, pokud by při přiřazení došlo k zúžení typu (type narrowing) a potenciální ztrátě informace. Toto chování je nezávislé na aktuálním nastavení prostředí VB.NET direktivami Option Strict a Option Explicit:
Jak již bylo uvedeno dříve, všechna pole VB.NET jsou odvozena od třídy System.Array. Tato třída zastřešuje metody, které jsou každé její instanci k dispozici. Jakmile pole inicializujeme, vytvoříme tím současně i instanci třídy System.Array. Se základními metodami této třídy Rank(), GetUpperBound(), Length() a LongLength() jsme se již seznámili ve 20. dílu seriálu.
Metoda Array.Clear
Dosti často se v praxi vyskytuje potřeba prvky pole rychle inicializovat na výchozí hodnotu. Může se zdát jednodušší namísto mazání pole staré pole prostě zrušit a deklarovat nové, stejné velikosti, ale není tomu tak. Vyhrazení (alokace) většího souvislého bloku potřebné paměti může představovat relativně časově náročný proces a díky způsobu, jakým pracuje úklidový proces garbage collector .NET nemusí dojít k uvolnění původní paměti okamžitě, takže se při opakovaném použití tohoto postupu mohlo dojít k vyčerpání dostupné paměti systému. Můžeme také požadovat inicializaci prvků pole jen v určitém rozsahu. Proto k inicializaci pole v .NET slouží samostatná metoda Array.Clear(), přebírající celkem tři parametry: pole, které se má inicializovat a počáteční a počet prvků v rozsahu, který se bude resetován na výchozí hodnotu. Ta bude odpovídat typu daného pole - pro číselné hodnoty je to nula, pro řetězce prázdný znak a pro ostatní objekty hodnota Nothing:
Imports System
. . .
Dim a() As Integer = {1, 2, 3, 4, 5}
Array.Clear(a, 1, 3)
For Each i As Int32 In a
Console.Write(i & " ")
Next
Ukázka vypíše prvky pole, inicializované metodou Array.Clear() ve třech tři prvcích, indexem 1 počínaje:
1 0 0 0 5
Jak z ukázky níže vyplývá, s ohledem na lineární uložení vícerozměrných polí v paměti se metoda Array.Clear() se nezastaví ani před několika-rozměrným polem:
Dim d(,) As Double = New Double(2, 1){{1, 2}, {3, 4}, {5, 6}}
Array.Clear(d, 2, 3)
For Each i As Int32 In d
Console.Write(i & " ")
Next
1 2 0 0 0 6
Poznámka: Ve starších verzích VB k podobnému účelu jako metoda Array.Clear() sloužil příkaz Erase, který však nepodporuje inicializaci pole v určitém rozsahu a pole vždy maže jako celek. Můžeme jej používat i nadále, pokud nám toto chování nevadí, protože výhodou příkazu Erase je, že jím lze inicializovat několik polí současně:
Dim b%(10), c&(20)
Erase b, c ` odpovídá volání
Array.Clear(b, 0, b.GetUpperBound(0))
Array.Clear(c, 0, c.GetUpperBound(0))
Metoda Array.Clone, rozhraní ICloneable
Poměrně častou činností při práci s proměnnými je vytvoření kopie. Jelikož však je pole referenční typ, není možné jednoduše vytvořit kopii pole přiřazením jedné proměnné do druhé. V zásadě je nutné vytvořit druhou proměnnou typu pole stejného typu a rozměrů a pak do něj jednotlivé prvky zkopírovat z původního pole s použitím cyklu For-Next. V prostředí .NET se tato otravná činnost obvykle zapouzdřuje do virtuální metody Clone rozhraní System.ICloneable.
Rozhraní v objektovém programování lze přirovnat k funkci dálkového ovladače k televizi, který lze použít i k dalším spotřebičům, pokud tyto disponují funkcemi, které rozhraní ovladače podporuje (tj. funkce jako "Vypnout", "Zapnout", "Nastavit hlasitost", apod.). To mj. umožňuje více různých spotřebičů ovládat jednotným a konzistentním způsobem. Samotné rozhraní ovladače však bez daného spotřebiče neumí nic, jeho funkce jsou jen "virtuální atrapa". Jelikož se konceptem rozhraní budeme podrobněji seznamovat později při výkladu OOP, na tomto místě pouze uvedeme, že virtuální metody rozhraní pro VB.NET představují typově konzistentní předpis (tzv. signaturu metody), jak by měly metody třídy navenek vypadat a fungovat (počet a typ parametrů a návratový typ). Vlastní provedení (tzv. implementace) rozhraní třídy (tj. vnitřní kód třídy) však zůstává zcela v režii programátora s definice rozhraní žádný kód neobsahuje. V prostředí .NET je naštěstí rozhraní System.ICloneable pro třídu System.Array již implementováno a metodu Clone() lze volat ihned po deklaraci, případně inicializaci pole:
Dim aInt0() As Int32 = New Int32(){1, 2, 3}
Dim aInt1() As Int32 = aInt0.Clone()
Metoda Clone() však trpí významným omezením v případě, že prvky pole tvoří referenční typ - pro lepší názornost třebas jiné pole. Je zřejmé, že nyní bychom měli zopakovat výše naznačený postup i pro všechny prvky tohoto vnořeného pole. Standardní implementace metody Clone() se však něčím podobným vůbec nezatěžuje - provádí jen tzv. mělkou (anglicky shallow) kopii objektu. Je tomu proto, že by zcela obecná implementace takové metody byla neúměrně složitá, a pokud by všechny její členy rovněž neimplementovaly rozhraní System.ICloneable, pak by byla vlastně nemožná.
Vnitřek tříd pro nás s ohledem na jejich zapouzdřenost zůstává skrytý a nemáme přímou možnost zjistit, jakým způsobem lze objekt duplikovat, pokud nám metodu pro vytvoření své vlastní kopie neimplementuje sám - na příkladu kopírování pole lze současně demonstrovat smysl a užitečnost implementace rozhraní. Proto metoda ICloneable.Clone() kopíruje jen členy hodnotových typů a pro ostatní členy (např. člen A1.B na schématu níže) zkopíruje pouze odkazy, které však i nadále směřují na původní umístění objektů v paměti. Například zásahem do členu A2.B v kopii objektu A2 současně změníme i člen původního objektu A1.B - což určitě není chování, které bychom od skutečné nezávislé kopie objektu očekávali.
Mělká kopie objektu ICloneable.Clone()
Metoda Array.Copy(), Array.CopyTo()
Z výše uvedeného důvodu je vnořených polí referenčního typu (jako jsou např. nepravidelná "jagged" pole a pole nespecifikovaného typu System.Object) metoda Clone() prakticky nepoužitelná, jelikož bychom si zásahem do kopie současně měnili i prvky původního pole. Proto prostředí CLR pro kopírování polí zavádí speciální metodu Array.Copy(). Ta provádí kopírování "důkladně" a pole referenčních typů rekurzivně kopíruje až do poslední úrovně odkazů - říkáme, že Array.Copy() provádí tzv. hlubokou kopii objektu (anglicky "deep copy") . Metoda Array.Copy() přitom navenek pracuje zcela podobně, jako bychom kopírovali prvky z jedné tabulky textového editoru do druhé: nejprve si připravíme prázdnou tabulku a pak do ní zkopírujeme blok položek určité délky od určité pozice na novou pozici cílové tabulky:
Kopírování prvků pole metodou Array.Copy()
Z naznačeného postupu vyplývá, že nejobecnější volání metody Array.Copy() vyžaduje celkem pět parametrů: odkaz na zdrojové a cílové pole, zdrojový a cílový index kopírovaného bloku a celkový počet prvků bloku, jež má být zkopírován. V prostředí CLR metoda Array.Copy() disponuje variantou, která zjednodušuje její použití v případech, kdy hodláme zkopírovat blok od počátku zdrojového pole na počáteční index cílového pole (tj. v případech, kdy počáteční i cílový index je nula). Takovým metodám se říká přetížené, v prostředí VB.NET se chovají podobně jako metody s nepovinnými (volitelnými) parametry (viz 16. díl seriálu):
Dim aSrc() As Int16 = New Int16(){0, 1, 2, 3, 4, 5}, aDst(5) As Int32
Array.Copy(aSrc, aDst, aDst.Length) ` nejjednodušší varianta: zkopíruje celé pole aSrc do aDst
Array.Copy(aSrc, 1, aDst, 2, 3) ` zkopíruje tři prvky aSrc od pozice 1 na pozici 3 pole aDst
V případě, že nám stačí zkopírovat celý obsah zdrojového pole na cílové pole a obě pole jsou pouze jednorozměrná (což bývá v praxi nejčastější případ), prostředí CLR nabízí odlehčenou variantu metody Array.CopyTo(), která se nezatěžuje logikou kopírování vícerozměrných polí, je optimalizována na rychlost a podporuje pouze tři parametry:
Dim aSrc() As Int16 = New Int16(){0, 1, 2, 3, 4, 5}, aDst(5) As Int32
Array.CopyTo(aSrc, aDst, aDst.Length) ` nejjednodušší varianta: zkopíruje celé pole aSrc do aDst
Array.CopyTo(aSrc, aDst, 3) ` zkopíruje tři prvky aSrc od pozice 1 na pozici 3 pole aDst
V případě, že se typ zdrojového a cílového pole liší, metody Array.Copy() , resp. Array.CopyTo() poměrně inteligentně zajistí i autoboxing a potřebné typové konverze v případě, že přetypování prvků nevyžaduje zúžení typu spojené s potenciální ztrátou dat (tj. v naší analogii s tabulkou v případech, kdy se prvky kopírovaného bloku do cílové tabulky "nevejdou").
Metoda Array.Sort() a Array.Reverse()
Metoda Array.Sort() je další příklad využití rozhraní v prostředí .NET, v daném případě jde o implementaci metody IComparable.Sort(). Aby bylo možné porovnat dvě instance třídy, musí být definován mechanismus, určující, která z nich při porovnání získá přednostní pořadí. Pro základní hodnotové typy a řetězce je tato funkčnost již ve třídě System.Array implementována. Pole čísel nebo řetězců lze tedy seřadit jednoduchým zavoláním metody Array.Sort() a ukázka níže nám vypíše prvky pole seřazené od nejmenšího do největšího:
Dim a() As Integer = {3, 2, 0, 1, 5}
Array.Sort(a)
For Each i As Integer In a
Console.Write(i & " ")
Next
0 1 2 3 5
V případech, kdy prvky pole obsahují proměnné obecného typu je setřídění takového pole poněkud složitější záležitost, protože příslušné prvky pole musí běhovému prostředí implementovat a vystavit metodu CompareTo() rozhraní System.Collections.IComparable nebo metodě Array.Sort() musíme předat sortovací objekt odvozený od rozhraní System.Collections.IComparer disponující metodou Compare(). To umožňuje běhovému prostředí .NET CLR při procházení prvky pole třídící metodou Heapsort mezi dvojicemi prvků metodu Compare() nebo CompareTo() zavolat, provést jejich porovnání a správné zařazení jednoho před druhý. Jelikož podrobnosti implementace obecného rozhraní budou probírány podrobně později, předvedeme si pouze generický příklad, jak lze takové rozhraní v praxi implementovat pro sestupné řazení hodnotových typů:
Imports System, System.Collections
Module modMain
Sub Main
Dim c() = {3, 2, 0, 1, 5}, i
Array.sort(c, New CComparer())
For Each i In c
Console.Write(i & " ")
Next
End Sub
End Module
Class CComparer: Implements IComparer
Function Compare%(x, y) Implements IComparer.Compare
Return x > y
End Function
End Class
Ukázka vypíše stejné prvky, jako výše, ale v obráceném pořadí:
5 3 2 1 0
Pokud potřebujeme pouze obrátit číslování prvků v poli, takže se první index (nula) stane posledním a naopak, není nutné implementovat řazení, jelikož třída System.Array pro pole tvořená jednoduchými hodnotovými typy a řetězce disponuje metodou Reverse(). Výše uvedenou ukázku sestupného řazení lze tudíž pro tyto nejčastější případy jednoduše realizovat následovně:
Dim a() As Integer = {3, 2, 0, 1, 5}
Array.Sort(a)
Array.Reverse(a)
For Each i As Integer In a
Console.Write(i & " ")
Next
Výsledkem bude opět výpis pole, seřazeného sestupně od největšího prvku k nejmenšímu:
5 3 2 1 0
Poznámka: Výše popsané ukázky představují pouze velmi základní nástin použití metod Array.Sort() a Array.Reverse(). Časem se k problematice třídění polí vrátíme při popisu třídění polí a dalších speciálních kolekcí podle klíče v rámci výkladu práce s kolekcemi typu ArrayList, Hashtable a SortedList.
Pole jako parametr
Výhodným rozšířením syntaxe prostředí VB.NET je možnost předávat procedurám jednorozměrné pole parametrů. Předávání pole parametrů nezaměňujme s předáváním parametru typu pole, kde pole vystupuje jako jednoduchá proměnná. Jde o možnost předávat procedurám nebo funkcím předem neurčený počet parametrů různého typu současně, což umožňuje dodat jejich volání téměř neomezenou flexibilitu (která však může mít nepříznivý dopad na srozumitelnost a jednoznačnost kódu). Předání pole parametrů se ve VB.NET deklaruje klíčovým slovem ParamArray. Na rozdíl od předchozích verzí VB.NET se takové pole parametrů předává vždy hodnotou - to znamená, že jej nemůžeme použít pro předávání výstupních parametrů, což trochu degraduje užitečnost tohoto způsobu předávání parametrů:.
Imports System
Module modMain
Sub Main
VypisZnamek("Marie", 3, 4, "dvojka", 3)
End Sub
Sub VypisZnamek(Jmeno, ParamArray Znamky())
For i As Integer = 0 To Znamky.GetUpperBound(0)
Console.WriteLine(Znamky(i))
Next
End Sub
End Module
Ukázka výše vypíše následující seznam. První předaný parametr se nezobrazí, protože není v proceduře
VýpisZnamek() použit. Ke zjištění počtu parametrů jsme použili metodu pole
GetUpperBound().
3
4
"dvojka"
3
Poznámka: Na rozdíl od předchozích verzí VB se všechny prvky pole parametrů předávají vždy hodnotou, to znamená, že tento způsob deklarace ve VB.NET nemůžeme použít pro předávání výstupních parametrů - což trochu degraduje užitečnost této metody.
Pole jako návratová funkce
Stejně jako pole mohou tvořit vstupní a výstupní parametry, mohou představovat i návratovou hodnotu funkce. Tu je nutné deklarovat jako pole se závorkami, např.
Module modMain
Sub Main
Dim b() As Byte = ByteArrayFunction(50)
End Sub
Function ByteArrayFunction(ByVal i As Integer) As Byte()
Dim b(i) As Byte
System.Array.Clear(b, 0, b.GetUpperBound(0))
Return b
End Function
End Module
Funkce ByteArrayFunction() zde slouží jen dosti akademický demonstrační příklad, protože zjevně neprovádí o nic více ani méně, než prosté volání Dim b(50) As Byte.
Stručné shrnutí
Třída System.Array disponuje bohatou funkčností, která nám v prostředí VB.NET může výrazně usnadnit práci s polem. Prvky polí VB.NET lze pomocí metod třídy System.Array reverzovat, řadit a prohledávat a také je lze kopírovat z jednoho pole do druhého včetně všech objektů, které prvky pole tvoří.
V dalším dílu se seznámíme s dalším základním referenčním typem VB.NET, kterým je datový typ řetězec a třídou System.String.