Programujeme ve Visual Basic .NET - 16. díl - předávání parametrů odkazem a hodnotou

V tomto dílu seriálu o programovacím jazyce VB.NET se budeme zabývat dvěma základními způsoby předávání parametrů mezi dvěma bloky kódu, jejich použitím, přednostmi a nevýhodami.
V předchozí části jsme se seznámili s dvěma základními způsoby výměny dat mezi bloky kódu: použitím sdílených proměnných a předáváním parametrů. Použití sdílených proměnných je rychlejší a vhodné pro předávání objemu dat. Současně je však každé takové sdílení porušením zapouzdření (tzv. enkapsulace) - čili principu, na kterém staví strukturované i objektově orientované programování. Z tohoto důvodu bychom se měli snažit program vždy tvořit tak, aby programové moduly na stejné urovni sdílely co nejmenší počet proměnných a pro jejich předávání využívat parametry.

Předávání parametrů hodnotou

S ohledem na výše uvedené zásady se ve výchozím režimu VB.NET jednoduché parametry mezi procedurami předávají tak, že programový blok nedostane původní proměnnou, ale jen její kopii. To umožňuje volané proceduře, aby si je mohla měnit dle libosti bez rizika toho, že tím ovlivní běh kódu ve volající třídě. Tomuto způsobu volání se říká volání hodnotou (anglicky "By Value"). Nehrozí tedy, že když ve funkci nadeklarujeme proměnnou téhož jména jako ve volající proceduře, dojde k její změně zevnitř volané funkce. V uvedené ukázce na sobě volání funkce nijak nezávisejí, ačkoliv uvnitř samotné funkce Obvod() se hodnota proměnné r mění.

Imports System.Console
Module modMain
  Sub Main
    Dim r = 4
    WriteLine(Obvod(r))
    WriteLine(Obvod(r))
  End Sub
  Function Obvod(r)
    r = r * 2
    Return System.Math.PI * r
  End Function
End Module

Prostředí VisualStudio.NET a některá další IDE automaticky přidávají před název parametr klíčové slovo (atribut) ByVal (="By Value") a v případě, že jej nepoužijeme jej překladač předpokládá při předávání jednoduchých proměnných automaticky. Zápis  - deklarace funkce:

Function Obvod(r) ...

je z tohoto důvodu v takovýchto případech ekvivalentní zápisu:

Function Obvod(ByVal r) ...

Jak je ukázáno níže, v praxi bychom však neměli na implicitní doplňování atributu ByVal spoléhat, protože funguje opravdu jen u zcela jednoduchých datových typů, jako je číslo nebo řetězec (viz níže). Proto bychom jej měli v deklaracích uvádět vždy explicitně - právě tak, jak je při deklaraci parametrů procedur a funkcí automaticky doplňuje Visual Studio.

Předávání parametrů odkazem

To, co je z hlediska zapouzdření obvykle výhodné se v některých případech může snadno stát přítěží - například v případech, že pomocí parametru předáváme proceduře nějaký rozsáhlý objekt, třeba dlouhý textový řetězec vzniklý načtením celého souboru. Předávání proměnné hodnotou totiž ve skutečnosti znamená, že VB.NET runtime musí paměť pro tuto rozsáhlou promennou vyčlenit (tzv. "alokovat") dvakrát a přenášet volanému kódu obsah proměnné do nové kopie. To může být náročné jak na paměť, tak na výkon procesoru - zvláště tehdy, provádíme-li taková funkční přiřazení opakovaně, což v případě volání procedur bývá obvyklé. Krom toho výchozí způsob předávání parametrů nevyužívá možnost VB.NET procedur tento parametru změnit a vrátit jej jako výstupní. Ačkoliv tento způsob není nijak čistý z hlediska zapouzdřenosti, s ohledem na výkon a malé systémové nároky jej přednostně používají systémové funkce aplikačního rozhraní (API) a některá COM rozhraní (např. rozhraní ADSI a WMI). Jako výchozí jej také používají předchozí verze Visual Basic-u, na což si musíme při migraci kódu dávat pozor.

V takových případech  lze předávání parametrů provést odkazem, nebo-li referenci (anglicky "By Reference"). Prakticky jde o to, že se blok paměti vyhražený pro parametr při jeho předávání proceduře neduplikuje, volaná procedura dostane místo toho dostane pouze informaci, kde se v paměti nachází původní proměnná - tzv. odkaz. Volající procedura pak příslušnou část paměti uzamkne a může ji spravovat samostatně, např. změnit hodnoty proměnné. Tím může dojít k tomu, že volaná funkce může původní hodnotu kdykoliv přepsat a volající procedura ji pak může použít jako novou hodnotu. V tom případě se parametr, který při volání hodnotou sloužil pouze ke čtení (tzv. vstupní parametr) nyní může sloužit i pro předávání návratové hodnoty procedury (tzv. výstupní parametr). Každý výstupní parametr je z tohoto důvodu současně i parametr předávaný odkazem. Předávání proměnné odkazem se dosáhne použitím klíčového slova ByRef (="By Reference") před názvem parametru, např.:

System.Console
Module modMain
  Sub Main
    Dim r = 4
    WriteLine(Obvod(r))
    WriteLine(Obvod(r))
  End Sub
  Function Obvod(ByRef r)
    r = r * 2
    Return System.Math.PI * r
  End Function
End Module

Pokud si ukázku vyzkoušíme a spustíme například v prostředí prostředí Snippet Compiler, uvidíme, že se chová podobně jako příklad v 15.dílu seriálu - obě procedury již na sobě nejsou nezávislé a jedna modifikuje data v druhé. Princip zapouzdřenosti bloků kódu je tedy při volání odkazem porušen.

Historická poznámka pro pokročilé - VarPtr funkce v prostředí VB.NET:

Fyzické adrese paměti, kde je proměnná předáváná odkazem umístěna se říká ukazatel, nebo-li pointer, ale protože prostředí .NET spravuje paměť automaticky, není jeho získání v prostředí VB.NET tak jednoduché, jako v případě starších verzí VB, kde k jejímu získání sloužila funkce VarPtr(). Jelikož však některé speciální API funkce namísto prostého předání odkazu hodnotu ukazatele vyžadují, např. při volání odkazu na odkaz, uvedeme si na tomto místě její náhražku pro případné budoucí použití:

Function VarPtr(ByVal obj)
  Dim GC As System.Runtime.InteropServices.GCHandle = System.Runtime.InteropServices.GCHandle.Alloc(obj, System.Runtime.InteropServices.GCHandleType.Pinned)
  VarPtr = GC.AddrOfPinnedObject.ToInt32: GC.Free()
End Function

Jak vidíme, pro adresu fyzického umístění .NET objektu v paměti se musíme dotázat funkce prostředí GarbageCollector (tzv. "uklízeč", nebo "shromažďovač smetí"), což je proces, který v prostředí .NET zodpovídá za pravidelné uvolňování paměti nepoužitými objekty - a má tedy jejich adresy k dispozici "z první ruky". Odkaz na spravované objekty prostředí .NET zprostředkuje .NET třída System.Reflection.Pointer, která však s fyzickou adresou spravovaných objektu nesouvisí, protože prostředí .NET paměť a tím i ukazatele svých objektů spravuje nezávisle na operačním systému, pod kterým běží a v prostředí VB.NET její metody nelze používat.

Výchozí způsob předávání parametrů, jednoduché a složené proměnné

Jelikož pro předávání výstupní hodnot je primárně určena návratová hodnota funkce nebo metody, měli bychom se k předávání jednoduchých parametrů odkazem uchylovat jen případech, kdy tento způsob předávání hodnot výslovně požaduje volaná procedura (např. funkce Win32 API ), nebo když potřebujeme opakovaně předávat velký objem dat, popř. když potřebujeme vrátit procedurou více hodnot současně. V tomto posledním případě však bývá obvykle vhodnější a přehlednější vzájemně související parametry "zabalit" do struktury, nebo třídy (viz 11. díl seriálu) a předávat jako jediný parametr.

Tento přístup si můžeme vyzkoušet například při volání funkce pro výpočet obvodu obdélníka na základě délky hran a, b podle vzorce  Obvod = 2 * a + 2 *b. Počet předávaných parametrů omezíme tím, že je zapouzdříme do společné třídy ClsObdelnik podle ukázky níže:

Imports System.Console
Class ClsObdelnik
  Public a, b
End Class
Module modMain
  Sub Main
    Dim Obd As New ClsObdelnik
    Obd.a = 4: Obd.b = 2
    WriteLine(Obvod(Obd))
    WriteLine(Obvod(Obd))
  End Sub
  Function Obvod(CObd)
    CObd.a = 2 * CObd.a * 2: CObd.b = 2 * CObd.b
    Return CObd.a + CObd.b
  End Function
End Module

Spustíme-li si však tento příklad, uvidíme, že se v tomto případě prostředí VB.NET chová, jako kdybychom při deklaraci funkce Obvod použili atribut ByRef, tj. parametr CObd deklaruje odkazem Function Obvod(ByRef CObd) a hodnoty proměnných v procedurách se při následném běhu vzájemně ovlivňují. To proto, že u proměnnných tvořených strukturami nebo třídami (tzv. komplexních datových typů) prostředí VB.NET předpokládá, že by je s ohledem na jejich systémovou režii nebylo vhodné duplikovat a bylo by výhodnější používat volání odkazem. Z tohoto důvodu bychom neměli na implicitní doplňování atributů ByVal a ByRef při deklaraci procedur spoléhat a vždy je výslovně (explicitně) uvádět. Vývojové prostředí Visual Sudio a další nám toto usnadňuje tím, že při deklaraci nové funkce automaticky doplní volání parametrů hodnotou, tedy atributem ByVal.

Z logiky věci vyplývá, že atributy ByVal a ByRef jsou vzájemně výlučné, tzn. u každého parametru lze použít vždy jen jeden z nich. Je však pochopitelně možné při předávání více proměnných předávat část parametry hodnotou a zbytek odkazem. Odkazem lze předávat i volitelné parametry s výchozí hodnotou (viz předchozí díl seriálu).

Výkonová hlediska volání ByRef a ByVal

Rozdíl v rychlosti při volání parametrů odkazem a hodnotou si můžeme snadno vyzkoušet pomocí následující ukázky kódu, jež je také zopakováním práce s počítanými cykly a demonstruje práci s třídami sloužící pro odměřování času, kterou můžeme využít pro vlastních výkonové testy a benchmarky. Příklad několikatisíckrát  zopakuje volání dvou funkcí, které se navzájem liší pouze způsobem předávání svého parametru. Metoda Now() vrací aktuální hodnotu datumu a času a ukládá jej do struktury DateTime pro porovnání s aktuálním časem po proběhnutí testovací smyčky:

Imports System, System.Console
Module modMain
  Sub Main
    Dim r = 4, iMax = 100000, tStart, obvod, i
    tStart = Date.Now 
    For i = 1 To iMax
      obvod = ObvodByVal(r)
    Next
    WriteLine("Volání hodnotou: {0} msec", Date.Now.Subtract(tStart).Milliseconds)
    tStart = Date.Now
    For i = 1 To iMax
      obvod = ObvodByRef(r)
    Next
    WriteLine("Volání odkazem:  {0} msec", Date.Now.Subtract(tStart).Milliseconds)
  End Sub
  Function ObvodByVal(ByVal r)
    Return System.Math.PI * r * 2
  End Function
  Function ObvodByRef(ByRef r)
    Return System.Math.PI * r * 2
  End Function
End Module

Výsledný rozdíl se v tomto případě pohybuje v řádu několika jednotek až desítek procent, v praxi je tedy použití volání odkazem se zřetelem na své formální nedostatky ospravedlnitelné jen rozsáhlejší objekty - například při práci s řetězci, vzniklými načtením celých souborů, instancemi rozsáhlých tříd apod.

Stručné shrnutí

Seznámili jsme se dvěma základními způsoby předávání parametrů mezi procedurami - hodnotou a odkazem. Při volání odkazem, který je pro komplexní datové typy výchozí nedochází k vytvoření kopie proměnné v paměti a její předání tedy může být rychlejší, na druhé straně může být tato hodnota volanou procedurou změněna a použita jako výstupní parametr, což může činnost aplikace zkomplikovat.

 V přístím dílu se seznámíme s důležitým aspektem používání proměnných v prstředí VB.NET - a to datovými typy a jejich deklaracemi.

Témata článku: Software, Programování, Jednoduché sdílení, Díl, Odkaz, Reference, Pre, Speciální API, Původní funkce, Původní data, Původní objekt, Funkce, Pointer, Klíčový parametr, Jednoduchý princip, Jednoduchý typ, Parametr, IMAX, Jednoduchý vzorec

Určitě si přečtěte

Budoucností Windows 10 je Fluent Design. Takto bude jednou vypadat celý systém

Budoucností Windows 10 je Fluent Design. Takto bude jednou vypadat celý systém

** Fluent Design je vzhled, do kterého postupně Microsoft převleče celý systém ** Staví na průhlednosti a velkých plochách ** Do Windows 10 se z části dostane už zítra při vydání podzimní aktualizace

Včera | Stanislav Janů | 134


Aktuální číslo časopisu Computer

Nový seriál o programování elektroniky

Otestovali jsme 17 bezdrátových sluchátek

Jak na nákup vánočních dárků ze zahraničí

4 tankové tiskárny v přímém souboji