LINQ Coding Guidelines #12–Anonyme Klassen

11. Mai 2015

Wenn es fertige Datentypen für die Abfrage gibt – zum Beispiel durch das Entity Framework generierte Klassen – liegt deren Verwendung auf der Hand. Bei Zwischenergebnissen oder trivialen Ausschnitten fehlen solche Datenstrukturen aber; hier kommen oft anonyme Klassen zum Einsatz.

Empfehlung:
* Der Einsatz von anonymen Datentypen ist auf einfachste(!) Fälle und Zwischenergebnisse zu beschränken (alternativ: ganz zu vermeiden).

 

Die Samples auf MSDN machen es vor, sogar mit expliziten Beispielen:

   1: var upperLowerWords = 

   2:     from w in words 

   3:     select new { Upper = w.ToUpper(), Lower = w.ToLower() }; 

   4:  

   5: var digitOddEvens = 

   6:     from n in numbers 

   7:     select new { Digit = strings[n], Even = (n % 2 == 0) }; 

   8:  

   9: var productInfos = 

  10:     from p in products 

  11:     select new { p.ProductName, p.Category, Price = p.UnitPrice }; 

Das ist in Ordnung für Zwischenergebnisse (was voraussetzt, das eine weitere Verarbeitung unmittelbar danach folgt – was in den Beispielen aber nicht gegeben ist!).

Auch wenn man die Verarbeitung oft in den gleichen Ausdruck mit aufnehmen könnte, gibt es valide Gründe für die Trennung. Das Verhindern von redundanten Datenbankabfragen oder wiederholten Neuberechnungen gehört ebenso dazu, wie das Trennen anhand der Provider oder ganz einfach die Lesbarkeit.

Anonyme Typen machen dieses Vorgehen sehr einfach. Von einfachen Fällen abgesehen gibt es jedoch gute Gründe, die Datentypen explizit zu definieren.

Schauen wir uns als Beispiel den RayTracer an:

   1: var pixelsQuery =

   2:     from y in Enumerable.Range(0, screenHeight)

   3:     ...

   4:     select new { X = x, Y = y, Color = traceRay(new TraceRayArgs(ray, scene, 0)) };

   5:  

   6: foreach (var row in pixelsQuery)

   7:     foreach (var pixel in row)

   8:         setPixel(pixel.X, pixel.Y, pixel.Color.ToDrawingColor());

Für jedes Objekt, das als Parameter benötigt wird oder als Zwischenergebnis entsteht – Ray, TraceRayArgs, Scene, … – gibt es eine eigene Klasse. Und ausgerechnet das Ergebnis der Berechnung hat diese Aufmerksamkeit nicht verdient? (Rhetorische Frage!)

Oder hier:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)

   2: {

   3:     return repository.ServersThisApplicationDependsOn

   4:         .Where(item => item.CONSUMING_APPLICATION == applicationId)

   5:         .Where(item => includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))

   6:         .OrderBy(item => item.SERVER_NAME)

   7:         .AsEnumerable()

   8:         .Select(item => new

   9:         {

  10:             item.CONSUMING_APPLICATION,

  11:             item.SERVER_NAME,

  12:             item.DEPENDENCY_STATUS,

  13:             item.SERVER_STATUS,

  14:             item.SERVER_OS,

  15:             item.SERVER_LOCATION,

  16:         });

  17: }

Aufgrund der Arbeitsweise – Rückgabe als object – ist ein Datentyp zumindest technisch nicht notwendig. Wäre er nicht trotzdem sinnvoll, um konsistent an verschiedenen Stellen verwendet werden zu können? Oder – immerhin ist er fachlich relevant – um die Datenstruktur explizit zu dokumentieren und damit auch bei der Eingabe durch Intellisense zu unterstützen? (Wieder: Rhetorische Fragen!)

Ergo: Sieht man von trivialen Zwischenergebnissen innerhalb einer Abfrage ab, dann ist die Verwendung von explizit definierten Datentypen einem anonymen Datentyp fast immer vorzuziehen.