Dnes bych se s našimi čtenáři rád podělil o jednu nepříjemnou zkušenost z Delphi XE, související s použitím generických struktur a tříd.
Dobrá zpráva je, že v Delphi XE2 Update 3 se tato chyba již neprojevuje. Problém se týká případů, kdy nadefinujete generickou strukturu s více jak dvěma typy (TMyStruct<A,B> = record … end), velikost implementace není větší než 4 byty a voláte funkci, která má tuto strukturu vrátit jako svůj výsledek.
Zní to docela složitě, ale příklad to ozřejmí:
type
TMyStruct<Type1,Type2> = record
Value1: Type1;
Value2: Type2;
end;
TTestClass<Type1,Type2> = class
public
function Get(AValue1: Type1; AValue2: Type2): TMyStruct<Type1,Type2>;
end;
function TTestClass<Type1,Type2>.Get(AValue1: Type1; AValue2: Type2):
TMyStruct<Type1,Type2>;
begin
// zde nastane AV chyba
Result.Value1 := AValue1;
Result.Value2 := AValue2;
end;
function Test(A, B: Byte): Integer;
var
ATestClass: TTestClass<Byte,Byte>;
Value: TMyStruct<Byte,Byte>;
begin
ATestClass := TTestClass<Byte,Byte>.Create;
try
// tak tohle se nepovede…
Result := ATestClass.Get(A, B).Value1 + ATestClass.Get(A, B).Value2;
finally
ATestClass.Free;
end;
end;
Zavoláním funkce TEST s libovolnými parametry zajistíte spolehlivé spuštění výjimky z důvodu porušení ochrany paměti.
Co se vlastně stane? Pokud se podíváte do zdrojového kódu vytvořeného kompilátorem, zjistíte, že metoda Get ukládá výsledek do paměti na předem alokované místo, které musí připravit volající metoda. Naproti tomu ale funkce Test, jelikož se výsledek volání vejde do 32 bitového registru EAX, očekává vrácení výsledku v registru a žádné místo v paměti nepřipraví. Důsledkem toho dojde k přepisu prakticky náhodného místa v operační paměti počítače. Naštěstí je to většinou 0x00000000 a pokud používáte procesor s kontrolou ochrany paměti, tak aplikace skončí AV chybou. V horším případě, který lze rovněž simulovat, ale dojde k přepisu libovolného jiného místa a důsledky mohou být nepředvídatelné.
Jistě si říkáte, že výše popsaný případ je velmi specifický. Nicméně má nepříjemný důsledek a tím je problematické použití generické třídy TDictionary<TKey,TValue>, která je jinak velmi užitečná. Pokud použijete pro TKey a TValue datové typy, jejichž velikost se vejde do 4 bytů, pak není možné použít konstrukci for APair in AnInstanceOfTheDictionary do … , jelikož vrácený datový typ TPair<TKey,TValue> odpovídá výše popsanému typu TMyStruct<Type1,Type2> a první průchod cyklem okamžitě vyvolá AV chybu nebo přepis paměti.
Při použití této konstrukce je tedy nutné mít na zřeteli, aby součet velikostí použitých datových typů byl alespoň 5 bytů anebo změnit cyklus tak, aby procházel klíče slovníku, tj. for AKey in AnInstanceOfTheDictionary.Keys do …
Problematické jsou například implementace TDictionary<Byte,Byte>, TDictionary<Byte,Word> apod.
Naopak bez problémů jsou TDictionary<Byte,Integer>, TDictionary<string,Byte> apod.
Věřím, že příznivce Delphi tato ukázka neodradí od používání jinak velice výkonných generických tříd a případně pomůže těm z Vás, kteří se do podobné situace dostali a nedokázali přijít na to, proč jim aplikace padá nebo se nechová dle očekávání.
Igor Gottwald
Pozn. editora: rádi uveřejníme i Vaše příspěvky - nebojte se mi napsat.