LINQ Coding Guidelines–”Error not found”

9. Oktober 2014

Es gibt sie einfach nicht: Coding Guidelines für LINQ.

Eigentlich ist das verwunderlich, denn für C# im allgemeinen haben wir mehr als genug Auswahl: Automatische Code-Formatierung in Visual Studio, diverse Coding Guidelines als Dokumentation (msdn, lh), StyleCop zur Prüfung.

Und alle ignorieren LINQ.

 

Eine unmittelbare Konsequenz daraus sehe ich immer wieder: Komplexe LINQ-Ausdrücke die an Unlesbarkeit kaum zu überbieten sind; deren Logik sich nur durch “mind compiling and execution” erfassen lässt – sprich, man muss sie gedanklich durchkompilieren und ausführen um sie zu verstehen; einfache LINQ-Ausdrücke, die sich alle Mühe geben, komplex zu erscheinen.

Und das, obwohl LINQ doch eigentlich als deklaratives Sprachmittel der Hort der Lesbarkeit sein sollte.

Ein schönes Beispiel hat Luke mit seinem RayTracer, auch wenn er das zugegebenermaßen nur als Experiment ansieht.

Doch auch vergleichsweise moderate LINQ-Ausdrücke lassen sich oft in ihrer Lesbarkeit deutlich verbessern. Das folgende Beispiel stammt aus einer Beispielanwendung eines Kollegen (mit dessen Einverständnis ich das hier verwende ;-)):

   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

  12:                                                     .Select(c =>

  13:                                                         new KeyValuePair<string, string>(

  14:                                                             c.Id.ToString(CultureInfo.InvariantCulture),

  15:                                                             c.Statement))

  16:                         })

  17:         .FirstOrDefault();

  18: }

Der folgende Ausdruck ist aus einem Kundenprojekt (anonymisiert, um die Schuldigen zu schützen):

   1: return (from item in repository.ServersThisApplicationDependsOn

   2:         where (item.CONSUMING_APPLICATION == applicationId) && (includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))

   3:         orderby item.SERVER_NAME

   4:         select item).AsEnumerable().Select(item => new

   5:         {

   6:             item.CONSUMING_APPLICATION,

   7:             item.SERVER_NAME,

   8:             item.DEPENDENCY_STATUS,

   9:             item.SERVER_STATUS,

  10:             item.SERVER_OS,

  11:             item.SERVER_LOCATION,

  12:             item.SERVER_FQDN,

  13:             LATEST_TARGET_HARDWARE_REMOVAL_DATE = GetFormatedNullableDateTime(item.LATEST_TARGET_HARDWARE_REMOVAL_DATE)

  14:         });

Dito der Code aus der Suchseite einer Anwendung:

   1: var condition = LinqHelper.True<EntityModel.Transaction>();

   2: var conditionBalance = condition.And(t => (((searchCriteria.MinimumAmount == null) || (t.Amount >= searchCriteria.MinimumAmount)) &&

   3:                                           ((searchCriteria.MaximumAmount == null) || (t.Amount <= searchCriteria.MaximumAmount))));

   4: var conditionDate = conditionBalance.And(t => (((searchCriteria.StartDate == null) || (t.Date > searchCriteria.StartDate.Value.AddDays(-1))) &&

   5:                                               ((searchCriteria.EndDate == null) || (t.Date < searchCriteria.EndDate.Value.AddDays(1)))));

   6: string transactionType = GetTransactionType(searchCriteria.CashTransactionType);

   7: var conditionTransactionType = conditionDate.And(t => ((searchCriteria.CashTransactionType == TransactionType.All) || (transactionType == t.Type)));

   8:  

   9: result = entities.Transaction

  10:     .Join(

  11:         entities.Account.Where<Account>(ac => ac.AccountID == accountId),

  12:         t => t.Account.AccountID,

  13:         a => a.AccountID,

  14:         (t, a) => t)

  15:        .Where(conditionTransactionType.Compile());

Zur schlechten Lesbarkeit kommen hier noch Bugs.

 

Beispiele für schlecht lesbaren LINQ-Code gibt es also zuhauf. Bei Guidelines, wie man das verbessern kann, wird es hingegen dünn:

  • Es gibt einige wenige StyleCop-Regeln (SA1102-SA1105)
  • C# Coding Conventions (C# Programming Guide) hat im Abschnitt “LINQ Queries” ein paar sehr grundlegende Ratschläge, kratzt damit aber nur an der Spitze des Eisbergs.
  • Die 101 LINQ Samples folgen einer einheitlichen Formatierung, ohne diese allerdings zu beschreiben. Außerdem sind sie als Beispielcode nicht immer der beste Ratgeber für Implementierungen in einem Projekt, da sie sich üblicherweise auf genau einen Aspekt konzentrieren.

Dummerweise gibt es auch schlechte Ratschläge, etwa:

 

Jetzt wäre es an der Zeit, dass ich mit Coding Guidelines für LINQ dagegenhalte. Allerdings sprechen zwei Dinge gegen dieses Vorgehen:

  1. Der Beitrag wird zu lang 😉
  2. Ich habe keine schlüsselfertigen Coding Guidelines. (Sorry!)

Was ich aber habe sind eine ganze Reihe von Hinweisen, wie man es besser machen kann. Auslöser sind dabei immer reale Probleme, die mir in Reviews untergekommen sind.

DISCLAIMER: Die Beispiele, die ich hier und in den folgenden Beiträgen zeige, entstammen – soweit nicht anders angegeben – realen Projekten. Sie wurden von mir lediglich anonymisiert (um die Schuldigen zu schützen ;-)) und ggf. gekürzt, wenn es der Sache dient.

Wer das in seine Coding Guidelines einfließen lassen will ist natürlich willkommen.