Miről beszélünk?
C#-ban egy referencia típusú változó mindig a saját típusára, vagy az abból leszármazott típusra mutathat. Ez teljesen transzparens, amikor egy object referencián meghívsz egy metódust, mindegy, hogy a referencia tényleg egy object-re, string-re, Kiskutya-ra mutat-e. Ezzel valószínűleg mindenki találkozott már az első "Objektum-orientált programozás" órán, valamint azóta kb. egycsilliárdszor.
Nevezzük ezt mondjuk "egyes számú szabály"-nak.
Ha két típust (mondjuk T-t és U-t) egymáshoz hasonlítunk, az alábbi négy lehetőségből pontosan egy lesz igaz:
- T nagyobb, mint U (OOP terminológiával: T őse U-nak - a System.Object őse a System.String-nek, tehát az object nagyobb, mint a string)
- T kisebb, mint U (OOP terminológiával: T leszármazottja U-nak - a System.String a System.Object leszármazottja, tehát a string kisebb, mint az object)
- T egyenlő U-val (OOP terminológiával: T és U ugyanaz a típus)
- T-nek nincs kapcsolata U-val (OOP terminológiával: navajon?)
Vegyünk egy műveletet, ami a típusokkal mókol valamit, egy adott típusból egy másikat csinál valamilyen szabály alapján: T-ból T'-t, U-ból U'-t.
Hogyha a T' ugyanolyan relációban lesz U'-vel (a fenti négyből), mint T volt U-val, akkor a művelet kovariáns. Ha a művelet megfordítja a relációt (pontosabban: a kisebbséget-nagyobbságot megfordítja, az egyenlőséget és a "közömbösséget" változatlanul hagyja), akkor a művelet kontravariáns.
Érthetetlen, ugye? Nézzünk egy példát!
Adott két típus, mondjuk a System.Object és a System.String (közülük az object a "nagyobb"). Most vegyünk egy "műveletet", ami csinál valamit a típusokkal. Ilyen művelet például a "képezzünk T tömböt", vagy a "képezzünk T listát". Lesz tehát object[] (aka. T'), string[] (aka. U'), List<object> (legyen mondjuk T''), List<string> (logikusan U'') típusunk.
Milyen viszonban vannak ezek a típusok egymással? Az object[] nagyobb, mint a string[]? Hát nézzük meg, az egyes számú szabály értelmében, ha az, akkor értékül lehet neki adni:
// C# 1.0 kód string[] stringArray = new string[] { "http://", "otperc", ".net" }; object[] objectArray = stringArray;
A C#-ban a tömbképzés már a nyelv legelső verziója óta kovariáns.
És a List<string> castolható List<object>-té?
// C# 2.0 kód var stringList = new List<string> { "http://", "otperc", ".net" }; var objectList = (List<object>)stringList; // ez nem fordul!
Erre bizony még a nyelv 3.0-ás változatában is egy "Cannot convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'" a fordító válasza.
Veszélyes utakon
De vajon ha a tömböknél meg tudták oldani a dolgot már az 1.0-ban, akkor miért nem ment a genericnél a másodiknál? Eric Lippert azt mondja erről: "az a mód, ahogy a C# támogatja a tömböknél a kovarianciát, broken.
Azért került be a CLR-be, mert a CLR tervezői képessé akarták tenni azt a Java(-szerű) nyelvek támogatására, és ehhez szükség volt rá. Aztán mi beraktuk a C#-ba, ha már a CLR-ben bent volt. Sokat vitatkoztunk ezen a döntésen, és ma már nem örülök neki túlságosan, hogy így alakult, de most már nem sok mindent tudunk csinálni."
Miről beszél Lippert? Lássunk egy példakódot:
private static void GetDataMethod(object[] objectArrayParam) { foreach (var item in objectArrayParam) { Console.WriteLine(item); } } private static void SetDataMethod(object[] objectArrayParam) { objectArrayParam[0] = new object(); } static void Main(string[] args) { string[] stringArray = new string[] { "http://", "otperc", ".net" }; object[] objectArray = stringArray; GetDataMethod(objectArray); SetDataMethod(objectArray); }
Az első metódus lefut, de a második futtatásakor egy "ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array." üzenetet kapunk. A hangsúly a futtatásakoron van: sikerült egy erősen típusos nyelvbe egy olyan konstrukciót beemelni, ami remek lehetőséget ad a compile-time típusbiztosság-ellenőrzés kikerülésére - amivel pedig megnyílnak a pokol kapui.
A helyes út
A C# 2.0-tól van olyan variancia-támogatás is a nyelvben, ami nem “broken”, méghozzá a generikus delegate-ek:
private static string GetString() { return "http://otperc.net"; } static void Main(string[] args) { Func<object> functionThatReturnsObject; functionThatReturnsObject = GetString; object o = functionThatReturnsObject(); }
A Func<T> egy T típust visszaadó metódust reprezentál - tehát a Func<object> egy olyat, ami object-et ad vissza. Ez a "metódusreferencia" bátran mutathat olyan metódusra, ami string-et ad vissza. Mi baj lehet? Semmi. A hívó ugy is object-et vár, nem érheti meglepetés. Bármi olyanra mutathatunk, ami specializáltabb, mint mi (az object-es példa esetében praktikusan bármire). Visszafele viszont nem megy a dolog, ha a hívó string-et vár, nem adhatunk object-et.
A generikus delegate-ek a castolása visszaadott típusokra nézve kovariáns.
Nézzük a másik esetet, mikor a típusparaméter nem a visszaadott típust, hanem a paraméter típusát mondja meg. Az Action<T> delegate egy visszatérési érték nélküli, egy T típusú paramétert váró metódust reprezentál. Ha egy metódus mondjuk object-et vár, bátran elérhetjük egy olyan Action-ön keresztül, ami string-et (vagy bármi már specializáltabbat) vár, mert az "alatta lévő", object-et váró metódus simán meg fogja enni:
private static void ConsumeObject(object obj) { Console.WriteLine(obj); } static void Main(string[] args) { Action<string> methodThatConsumesString = ConsumeObject; methodThatConsumesString("http://otperc.net"); }
A generikus delegate-ek castolása a paraméterek típusára nézve kontravariáns - a típusbiztosság pedig mindkét esetben compile-time ellenőrzött.
Variancia a C# 4.0-ban
Eddig láttunk példát a kontra- és kovarianciára is, annak jó és kevésbé jó implementációjára tömböknél, delegate-eknél. Egy valamit nem láttunk általános generikus típusoknál, pl. egy List<T>-nél a variancia használatára. Azért nem láttunk, mert nincs. C# 3.0-ig nem volt lehetőségünk mondjuk egy List<string>-et List<object>-té castolni - C# 4.0-tól majd lesz. Bizonyos esetekben.
Nem meglepő, hogy ezt a fajta támogatást a nyelvbe "nem-broken" módon igyekeznek behozni - a klasszikus kecskés-káposztás felállásban, miszerint használhassunk kontra- vagy kovariáns hozzárendeléseket ott, ahol azok működőképesek és hasznosak, de maradjon meg a fordításidejű típusbiztosság-ellenőrzés is. Hogy tudjuk szétszeparálni az működő eseteket a nem-működőktől? Nézzük meg újra a legelső példakódunkat:
GetDataMethod(objectArray); // ez a metódus lefut
SetDataMethod(objectArray); // ez ArrayTypeMismatchException-t dob
Miért tud futni az első, és miért nem a második? Mi a különbség?
Mondjuk első közelítésben az "adatáramlás iránya". Az elsőben csak iterálunk, "kiveszünk" adatokat, míg a másodikban "berakni" próbálunk.
A delegate-es példánál is, a visszatérési érték típusa ("kimenő irány") kovariáns - ami X típust ad vissza, az visszaadhat X-et, vagy bármi specializáltabbat. A másik irányba meg fordítva.
A C# 4.0-ba két új kulcsszó került be a variancia támogatására: az in és az out varianciamódosítót (jó, egyikük kulcsszó-pályafutásának sem ez a kezdete, de ez most egy új szerepkör). Ezzel a két kulcsszóval generikus interface-ek típusparamétereit jelölhetjük meg, mint "bemeneti" (kontravariáns) vagy "kimeneti" (kovariáns) paraméter.
Egy példa talán érthetőbbé teszi a dolgot: vegyük például az IEnumerable<T> generikus interfészt. Egy ilyen osztályból T típusú elemek "jönnek ki" - tehát T-nél "nagyobb", általánosabb elemeket is kivehetünk. Egy IEnumerable<string> vígan tud IEnumerable<object>-ként is viselkedni. Bajt nem csinálhatunk, mert "befele" nem megy adat, nem fordulhat elő, hogy egy, magát IEnumerable<object>-nek mutató IEnumerable<string>-be egy int típusú adatot rakunk, merthogy az object megbírja azt is.
A .NET Framework 4.0-ban ennek az interfésznek így néz ki a definíciója:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Azzal, hogy ki lett rakva az out, a T paraméter kovariánssá vált:
// C# 4.0 kód: IEnumerable<string> enumerableOfString = new string[] { "http://", "otperc", ".net" }; IEnumerable<object> enumerableOfObject = enumerableOfString;
Annak pedig, hogy az out-ot ki lehetett rakni, egy előfeltétele volt: a T csak a "kimeneti oldalon" szerepelt az interfész definíciójában.
Most nézzünk meg egy másik interfészt:
public interface IComparer<in T> { int Compare(T x, T y); }
Itt most az "in" módosítót használták, a T paramétert kontravariánssá téve - talán most már nem túl meglepő a felfedezés, hogy a T csak bemenő paraméterként használt. T helyére bepasszol T, vagy bámi nála specializáltabb: egy comparer, ami össze tud hasonlítani két object-et, két stringet is össze tud.
Természetesen az in és az out típusparaméterek használata nem csak a framework fejlesztők kiváltsága, mi is használhatjuk őket a saját interfészeinkben.
Osztályokon nem, azt egy "Invalid variance modifier. Only interface and delegate type parameters can be specified as variant." üzenettel jutalmazza a fordító. A variancia ugyanis műveleteken értelmezett, a műveleteket pedig leginkább a delegate-ek és az interfészek írják le. Az List<T> típusparaméterére nem rakhattak ilyen módosítót, mert a T ki- és bemenő oldalon is használt. Viszont az List<T> egyben IEnumerable<T> is (illetve IEnumerable<out T>), úgyhogy ha "azt az arcát mutatja", akkor használhatjuk úgy.
Szintén ellenőrzi a fordító, hogy ha in vagy out varianciamódosítót használsz, akkor a típusparamétered tényleg csak a megfelelő helyen bukkanjon fel, out paramétert bemenőként használva "Invalid variance: The type parameter 'T' must be contravariantly valid on '<metódusnév>'. 'T' is covariant." üzenetet kapunk, in-t kimenőként használva pedig "Invalid variance: The type parameter 'T' must be covariantly valid on '<metódusnév>'. 'T' is contravariant."-et.
A teljes és kendőzetlen igazság
Be kell vallanom: a fenti postból sok minden nem igaz. Illetve igaz, csak nem úgy. Néhol egyszerűsítésekkel éltem, pl. a “kisebbség-nagyobbság”, “hozzárendelhetőség” és a “leszármazás” fogalmait illene jobban tisztába rakni. Belátható, hogy a kettő nem ugyanaz, mert bár fentebb láttuk, hogy az IEnumerable<object>-nek símán értékül adhatjuk az IEnumerable<string>-et, ez az “egyes számú szabály” értelmében azt is jelenti, hogy az IEnumerable<string> öröklési láncában (bocs, megint pongyola vagyok, interfészről beszélünk: öröklési fájában) valahol szerepelnie kéne az IEnumerable<object>-nek. Egyet biztosíthatok: nem szerepel.
Ha ezek a részletek is a helyükön lennének, akkor korrekt lenne az írás, viszont érthetetlen. A neten rengeteg anyag fellelhető, but I won’t bring it to you – you have to bing it for yourself. :)

0 megjegyzés:
Megjegyzés küldése