LINQ Coding Guidelines #13–Verwendung von Generics als Datentypen

12. Juni 2015

Wenn es fertige Datentypen für die Abfrage gibt – zum Beispiel durch das Entity Framework generierte Klassen – liegt deren Verwendung auf der Hand. (Hat der letzte Beitrag nicht auch so angefangen? Egal…)
Bei trivialen Ausschnitten oder Sonderfällen sind aber oft grundlegende Datentypen aus dem .NET Framework oder eigene Generics eine Option.

Empfehlung:
* Beim Einsatz von Generics ist type inference zu unterstützen.
* Für Ergebnisdatentypen sollten dedizierte Datentypen verwendet werden. 

Gelegentlich hat man im Ergebnis mit Generics zu tun. Das kann am fachlichen Kontext liegen, wenn man Fachbibliotheken einsetzt (spontan fallen mir Matrizen als Beispiel ein). Oder man hat triviale Informationshäppchen aus zwei, drei Einzelwerten. Wie hier:

   1: public ApiExpert Get(int id)

   2: {

   3:     return this.db.eXperts

   4:         .Where(x => x.Id == id)

   5:         .ToArray()  

   6:         .Select(x => new ApiExpert

   7:         {

   8:             Id = x.Id,

   9:             IsAdmin = x.IsAdmin,

  10:             Name = x.Name,

  11:             CustomerResponse = x.CustomerStatements.Select(c =>

  12:                 new KeyValuePair<string, string>(c.Id.ToString(CultureInfo.InvariantCulture), c.Statement)

  13:                 )

  14:         })

  15:         .FirstOrDefault();

  16: }

In diesem Beispiel fällt sofort ein Grundproblem beim Einsatz von Generics auf: Bei Konstruktoren wird keine type inference unterstützt (auch wenn Eric Lippert das verteidigt soll sich das demnächst ändern). Ergebnis ist ein unleserliches Konstrukt.

Alternativer Ansatz sind Factory-Methoden, also statische Hilfsmethoden, die ein Objekt erzeugen – wobei type inference dann genutzt werden kann. Man kann sich diese selbst definieren, oder auf Datentypen ausweichen, die das schon mitbringen.

Dadurch wird der Ausdruck schon lesbarer:

   1: public ApiExpert Get(int id)

   2: {

   3:     return this.db.eXperts

   4:         .Where(x => x.Id == id)

   5:         .ToArray()  

   6:         .Select(x => new ApiExpert

   7:         {

   8:             Id = x.Id,

   9:             IsAdmin = x.IsAdmin,

  10:             Name = x.Name,

  11:             CustomerResponse = x.CustomerStatements.Select(c =>

  12:                 CreatePair(c.Id.ToString(CultureInfo.InvariantCulture), c.Statement)

  13:                 )

  14:         })

  15:         .FirstOrDefault();

  16: }

Noch lesbarer wird es, wenn man eine spezielle Extension Method bereitstellt, die auch die Intention deutlich macht:

   1: public ApiExpert Get(int id)

   2: {

   3:     return this.db.eXperts

   4:         .Where(x => x.Id == id)

   5:         .ToArray()  

   6:         .Select(x => new ApiExpert

   7:         {

   8:             Id = x.Id,

   9:             IsAdmin = x.IsAdmin,

  10:             Name = x.Name,

  11:             CustomerResponse = x.CustomerStatements.Select(c =>

  12:                 c.ToIdAndStatement()

  13:                 )

  14:         })

  15:         .FirstOrDefault();

  16: }

Damit haben wir die Abfrage ausreichend lesbar gestaltet.

Die Verwendung allgemeiner Datentypen sollte man aber ebenfalls im Auge behalten. KeyValuePair<> bietet die Properties Key und Value. Ob diese Benennung inhaltlich zur Bedeutung der  Daten passt ist eine andere Frage. Und Tuple<> macht sich mit Item1, Item2, etc. gleich ganz von jeder Bedeutung frei. Hier gilt eine ähnliche Empfehlung wie bei anonymen Datentypen: Die Definition eines dedizierten Datentyps ist – von einfachsten Fällen abgesehen – eine klare Empfehlung.