A C# programozási nyelv

Névtelen típusok

Bevezető

Elképzelhető olyan szituáció, amikor csak azért van szükségünk egy osztályra, hogy összetartozó adatokat reprezentáljunk vele. Ilyen esetekben nincs szükségünk osztály metódusokra, eseményekre vagy egyéb egyedi funkcionalitásra. Továbbá ilyen esetekben ezt az osztályt csak az alkalmazáson belül használjuk, és nem szeretnénk újrafelhasználni. Ennek ellenére az ilyen ideiglenes osztályokat a C# korábbi verzióiban ugyanúgy „kézzel” kellett definiálni, mint minden más osztályt.

Ilyen osztályok készítése természetesen eddig sem volt bonyolult, viszont néha fölöslegesen sok munkát jelentett, ha sok tulajdonságot akartunk nyilvántartani. A C# 3.0 verziójától kezdődően ezt a munkát megkönnyítette az Auto-Implemented Propertyk bevezetése. Erre támaszkodik a névtelen típusok létrehozása is.

Az ilyen ideiglenes osztályokat korábban általában a következő módon definiáltuk:

internal class SomeClass
{
    // Privát adattagok...
    // Property definíciók a privát adattagokhoz...
    // ToString() felüldefiniálása, hogy az összes adattagot megjelenítse...
    // GetHashCode() és Equals() felüldefiniálása...
}

Névtelen típus definíciója

Amikor névtelen típust definiálunk, a var kulcsszót kell használnunk, majd az objektumot inicializálni. Ezt szemlélteti az alábbi kódrészlet:

// Egy autó tulajdonságait reprezentáló névtelen típus.
var myCar = new { Color = "Szürke", Make = "Saab", CurrentSpeed = 55 };

// Az adatok megjelenítése.
Console.WriteLine("Szín = {0}, Gyártmány = {1}", myCar.Color, myCar.Make);

A myCar változó típusát implicit adtuk meg, aminek van értelme ebben az esetben, mivel most nem akarunk foglalkozni az autó mögötti esetleges üzleti logikával, egyszerűen csak összetartozó adatokként szeretnénk kezelni az autó tulajdonságait.

Fordítási időben a C# automatikusan generál egy osztályt egyedi névvel. Mivel ez a típus nem látható nyelvi szinten ezért kell a var kulcsszót használnunk.

Az objektum inicializálásánál property neveket adunk meg, majd ezeknek értéket. A propertyk típusa a megadott értékek típusát veszi fel. Később ezeket a propertyket a szokásos módon tudjuk elérni.

Névtelen típusok belső reprezentációja

Minden névtelen típus automatikusan a System.Object típusból származik, ezért örökli annak metódusait is. Ezek a metódusok a következőek: ToString(), GetHashCode(), Equals(), GetType().

Érdekes kérdés, hogy ezek a metódusok mit adnak vissza és hogyan valósítja meg őket a fordító.

Ha a fenti példa kódunkra meghívjuk ezeket az alábbihoz hasonló eredményt kapunk:

Metódus Eredmény
ToString() { Color = Bright Pink, Make = Saab, CurrentSpeed = 55 }
GetHashCode() -439083487
GetType().Name <>f__AnonymousType0`3
GetType().BaseType System.Object

A fenti példában látszik, hogy a fordító a névtelen típusunknak automatikusan a <>f__AnonymousType0`3 nevet adta. Ez a név természetesen változhat, ezért erre közvetlenül nem tudunk hivatkozni.

A legfontosabb észrevétel, hogy minden név/érték párból, amit az objektum inicializálásánál megadtunk a fordító egy megegyező nevű csak olvasható propertyt hoz létre és a hozzátartozó csak olvasható privát adattagot.

Ha Reflectorral visszafejtjük a generált kódot, látható, hogy mit kezd a fordító a névtelen típusunkkal:

[CompilerGenerated, DebuggerDisplay(@"\{ Color = {Color}, Make = {Make}, CurrentSpeed = {CurrentSpeed} }", Type="<Anonymous Type>")]
internal sealed class <>f__AnonymousType0<<Color>j__TPar, <Make>j__TPar, <CurrentSpeed>j__TPar>
{
  // Fields
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly <Color>j__TPar <Color>i__Field;
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly <CurrentSpeed>j__TPar <CurrentSpeed>i__Field;
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private readonly <Make>j__TPar <Make>i__Field;

  // Methods
  [DebuggerHidden]
  public <>f__AnonymousType0(<Color>j__TPar Color, <Make>j__TPar Make, <CurrentSpeed>j__TPar CurrentSpeed);
  [DebuggerHidden]
  public override bool Equals(object value);
  [DebuggerHidden]
  public override int GetHashCode();
  [DebuggerHidden]
  public override string ToString();

  // Properties
  public <Color>j__TPar Color { get; }
  public <CurrentSpeed>j__TPar CurrentSpeed { get; }
  public <Make>j__TPar Make { get; }


}

Névtelen típusok egyezősége

Equals metódus

Mint láttuk a fordító elég kézenfekvő módon valósította meg a névtelen típus ősosztályából származó metódusokat. Azonban ha Reflecorral megnézzük az Equals függvény megvalósítását a következőt láthatjuk:

[DebuggerHidden]
public override bool Equals(object value)
{
  var type = value as <>f__AnonymousType0<<Color>j__TPar, <Make>j__TPar, <CurrentSpeed>j__TPar>;
  return ((((type != null) && EqualityComparer<<Color>j__TPar>.Default.Equals(this.<Color>i__Field, type.<Color>i__Field)) && EqualityComparer<<Make>j__TPar>.Default.Equals(this.<Make>i__Field, type.<Make>i__Field)) && EqualityComparer<<CurrentSpeed>j__TPar>.Default.Equals(this.<CurrentSpeed>i__Field, type.<CurrentSpeed>i__Field));
}

A fenti példából látszik, hogy az Equals függvényt a fordító úgy valósította meg, hogy a névtelen típus adattagjait hasonlítja össze egymással és nem a névtelen típusokat. Ebből következik, hogyha definiálunk két egyforma tulajdonságokat tartalmazó névtelen típust, akkor azok egyezőségét tudjuk vizsgálni az Equals metódussal.
Például:

var myCar1 = new { Color = "Szürke", Make = "Saab", CurrentSpeed = 55 };
var myCar2 = new { Color = "Szürke", Make = "Saab", CurrentSpeed = 55 };
bool isEquals = myCar1.Equals(myCar2);

A fenti példában az isEquals értéke true lesz, mert a két névtelen típus adattagjai megegyeznek.

Egyenlőség operátor

A fordító azonban nem valósította meg az == operátort, ezért ennek működése az alapértelmezett maradt. Azaz csak annyit vizsgál, hogy a két referencia azonos objektum példányra mutat, ami természetesen nem lehet igaz.

var myCar1 = new { Color = "Szürke", Make = "Saab", CurrentSpeed = 55 };
var myCar2 = new { Color = "Szürke", Make = "Saab", CurrentSpeed = 55 };
bool isEquals = myCar1 == myCar2;

A fenti példában, mint várható volt az isEquals értéke false lesz.

Névtelen típust tartalmazó névtelen típus

Lehetőségünk van olyan névtelen típust definiálni, amely adattagjai szintén névtelen típusok. Például ha egy rendelés adatait akarjuk összefogni:

// Névtelen típust tartalmazó névtelen típus létrehozása.
var purchaseItem = new {
TimeBought = DateTime.Now,
ItemBought = new {Color = "Szürke", Make = "Saab", CurrentSpeed = 55}, Price = 34.000};

Névtelen típusok haszna, korlátai

A fenti névtelen típusok használata hasznos lehet LINQ programozásakor, ha egyszerűen szeretnénk reprezentálni az adatainkat, de a funkcionalitással nem szeretnénk foglalkozni. Ugyanakkor tisztában kell lennünk a névtelen típusok korlátaival is.

A következő felsorolás tartalmazza a névtelen típusok főbb korlátait, amelyek miatt bizonyos esetekben nem célszerű névtelen típusokat használni:

Névtelen típusok és a LINQ

LINQ lekérdezésekkor előfordul, hogy nincs szükségünk az eredmény halmazban az objektumok összes tulajdonságára. Ilyenkor két dolgot tehetünk, deklarálhatunk olyan osztályt, amelynek tulajdonságai megegyeznek az eredmény tulajdonságaival, vagy az fent ismertetett névtelen típus definícióval a lekérdezésen belül definiálunk az eredmények megfelelő névtelen típust.

Az utóbbi esetben megadhatunk összetett névtelen típust is vagy speciális esetben, ha csak egy tulajdonságot kérdezünk le, akkor a tulajdonság típusával fog megegyezni a névtelen típusként megadott eredmény halmaz elemei.

A névtelen típusok és a LINQ kapcsolatához nézzük az alábbi példát:

List<Car> lst = new List<Car>();
lst.Add(new Car() { Color = "Szürke", Make = "Saab", Year = 2003 });
lst.Add(new Car() { Color = "Fehér", Make = "Audi", Year = 2008 });
lst.Add(new Car() { Color = "Fekete", Make = "Opel", Year = 2003 });
lst.Add(new Car() { Color = "Zöld", Make = "VW", Year = 2002 });
var result = from c in lst
   where c.Year == 2003
  select new
  {
  Make = c.Make,
  Year = c.Year
   };

A fenti példában a result típusa olyan System.Linq.Enumerable.WhereSelectListIterator típusú lesz, ami névtelen típusokat tartalmaz.  Az eredményen foreach ciklussal végigmenve az elemek tulajdonságaira a megszokott módon lehet hivatkozni.