Předchůdcem anonymních metod z Delphi 2009 jsou vložené (nested) procedury. To ale neznamená, že by éra vložených procedur skončila. Osobně je rád používám na zpřehlednění kódu v případě trošku delší procedury nebo na lokální provedení opakované akce. Hlavní výhodou je možnost přístupu k lokálním proměnným dané procedury.
Je samozřejmé, že vložené procedury můžete použít i v případě metod třídy. Pokud se ptáte, proč raději třeba v případě metod nenapsal místo vložené procedury další metodu tak odpověď zní: další izolace kódu. Proč kód, použitý jen jednou v konkrétním případě, popř. proměnné použité jen na jednom místě, nějak publikovat i jen v rámci třídy?
Ukáži možnosti a na co si dát pozor z hlediska optimalizace v jednotlivých případech. Jen na začátek: (skoro) každé volání vložené procedury něco stojí, a to něco může být v případě častého volání (např. v cyklu) nepříjemné. Ale to platí obecně o volání metod a procedur.
procedure TestA;
var
s: string;
procedure TestA_A;
begin
s := s +'text';
writeln(s);
end;
begin
s := 'TestA';
TestA_A;
end;
Základní a asi nejpoužívanější varianta. TestA_A má přístup k lokálním proměnným (v tomto případě jen k proměnné s) a mohou je měnit. Lehce nebezpečné, ale celkem efektivní: malá ztráta při volání. Nevýhodou je, že nemůžete TestA_A označit jako inline (DCC Error: Project1.dpr(14): E2449 Inlined nested routine 'TestA_A' cannot access outer scope variable 's'), což si myslím, že je škoda.
procedure TestB;
procedure TestB_A(var s2: string);
begin
s2 := s2 + 'text';
writeln(s2);
end;
var
s: string;
begin
s := 'Test B';
TestB_A(s);
end;
V tomto případě z TestB_A nelze přistupovat k lokálním proměnným (deklarace je před deklarací proměnné), tj. případné proměnné je nutné předat jako parametry. Mimochodem to znamená, že můžete použít inline a tím (pokud ji kompilátor použije a to většinou ano) odstraníte režii spojenou s volání, přesto je kód strukturován.
Pokud potřebujete ve vložené proceduře lokální proměnnou, musí Delphi kompilátor vygenerovat trošku šachování se zásobníkem, což v případě intenzivního volání např. v cyklu může být problém. Můžete zkusit přidat inline, ale druhé řešení i když ne moc kosher, je raději deklarovat požadovanou proměnnou jako lokální v nadřízené proceduře. Ale obecně se tím moc nezabývejte, to by muselo být volání opravdu intenzivní.
Mnohem vážnější je nepoužívat při předávání const, což platí obecně pokud v rámci metody nebo procedury nebudeme parametr měnit.
Z hlediska optimalizace to platí hlavně pro složitější datové typy jako string, record, Variant nebo pole.
Mějme tuto opravdu užitečnou proceduru:
procedure TestC;
var
s: string;
procedure TestCA(const s2:string);
begin
writeln(s2);
end;
procedure TestCB(var s2:string);
begin
writeln(s2);
end;
procedure TestCC(s2:string);
begin
writeln(s2);
end;
procedure TestCD(const s2:string); inline;
begin
writeln(s2);
end;
begin
s := 'Test C';
TestCA(s);
TestCB(s);
TestCC(s);
TestCD(s);
end;
Pokud jednotlivá volání seřadíme podle náročnosti volání, tak je to TestCD (žádná ztráta, volání je skoro jistě eliminováno a kód procedury je vložen přímo), TestCA a TestCB (v tomto případě je to jedno, ale varianta s const je preferovaná a navíc zaručuje, že se nespletete a nedojte náhodou k chybnému přiřazení), pak dlouho nic a pak TestCC.
Podíváme na výpis v assembleru - nejdříve varianta s const:
Project1.dpr.41: begin
00408BB0 53 push ebx
00408BB1 8BD8 mov ebx,eax
Project1.dpr.42: writeln(s2);
00408BB3 A1F4A94000 mov eax,[$0040a9f4]
00408BB8 8BD3 mov edx,ebx
00408BBA E8A9BCFFFF call @Write0LString
00408BBF E860A9FFFF call @WriteLn
00408BC4 E813A2FFFF call @_IOTest
Project1.dpr.43: end;
00408BC9 5B pop ebx
00408BCA C3 ret
Prostě přímočaré - 26 bajtů + 8 bajtů volání. A nyní varianta bez const:
Project1.dpr.49: begin
00408BE8 55 push ebp
00408BE9 8BEC mov ebp,esp
00408BEB 51 push ecx
00408BEC 8945FC mov [ebp-$04],eax
00408BEF 8B45FC mov eax,[ebp-$04]
00408BF2 E8A9BBFFFF call @LStrAddRef
00408BF7 33C0 xor eax,eax
00408BF9 55 push ebp
00408BFA 68328C4000 push $00408c32
00408BFF 64FF30 push dword ptr fs:[eax]
00408C02 648920 mov fs:[eax],esp
Project1.dpr.50: writeln(s2);
00408C05 A1F4A94000 mov eax,[$0040a9f4]
00408C0A 8B55FC mov edx,[ebp-$04]
00408C0D E856BCFFFF call @Write0LString
00408C12 E80DA9FFFF call @WriteLn
00408C17 E8C0A1FFFF call @_IOTest
Project1.dpr.51: end;
00408C1C 33C0 xor eax,eax
00408C1E 5A pop edx
00408C1F 59 pop ecx
00408C20 59 pop ecx
00408C21 648910 mov fs:[eax],edx
00408C24 68398C4000 push $00408c39
00408C29 8D45FC lea eax,[ebp-$04]
00408C2C E80FB8FFFF call @LStrClr
00408C31 C3 ret
73 bajtů + 8 bajtů volání. Na začátku se zvětší počet výskytu předávaného řetězce (System._LStrAddRef), pak se vypíše text a nakonec se sníží počet výskytů řetězce (System._LStrClr). Pokud by byl počet výskytu nulový, řetězec by byl uvolněn. Delphi provádí na pozadí celkem magii pro pohodlnou práci s řetězci (např. uvedené počítání referencí místo prosté kopie) a někdy je mu vhodné naznačit pro dosažení lepšího výsledku.
No a jen pro ukázku varianta s inline, tj. TestCD. Není volání, kód je v rámci hlavního kódu.
Project1.dpr.61: TestCD(s);
00408C74 A1F4A94000 mov eax,[$0040a9f4]
00408C79 8B55FC mov edx,[ebp-$04]
00408C7C E8E7BBFFFF call @Write0LString
00408C81 E89EA8FFFF call @WriteLn
00408C86 E851A1FFFF call @_IOTest
Celkem 18 bajtů.
Možná se Vám zdá divné, že tu počítám bajty v dnešní době, ale chtěl jsem ukázat, že stačí málo a Váš program bude přehlednější (rozdělení delší procedury na menší bloky), ale zároveň stejně efektivní jako kdyby byl psán nudloidně.
Takže se nebojte napsat vloženou proceduru (popř. anonymní metodu) a nebojte se, ony se ty bajty a cykly procesoru nastřádají :-).