LINQ Coding Guidelines #1–Null-Werte für Enumerationen

23. Oktober 2014

Das Problem ist beschrieben, die theoretischen Grundlagen sind aus den Füßen, kommen wir also zur ersten Empfehlung bzgl. des Schreibens von LINQ-Ausdrücken…

Empfehlung Artikel 1 des Grundgesetzes zur LINQ-Programmierung:
”null” ist kein zulässiger Wert für Enumerationen! Zuwiderhandlungen werden mit Exceptions in Produktion bestraft.

 

Die null hat im Universum funktionaler Programmierung schlicht und einfach keinen Platz.

“Code written in the functional style is often safer and easier to reason about. For example, functional languages avoid using the null value.” (“F# and Functional Programming”)

“The null value is not normally used in F# for values or variables.” (Null Values (F#))

“I think the succinct summary of why null is undesirable is that meaningless states should not be representable.” (“Best explanation for languages without null”)

Unter anderem ist diese Garantie die Grundlage dafür, dass eine Verkettung von Aufrufen, wie sie zu LINQ gehören, überhaupt erst valide ist:

   1: IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

Ohne diese Garantie müsste der defensive Entwickler jeden Aufruf in dieser Kette einzeln machen und auf null prüfen:

   1: IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0);

   2: if (numQuery2 != null)

   3:     numQuery2 = numQuery2 .OrderBy(n => n);

Das würde dem Ganzen etwas die Eleganz nehmen…

 

Meistens bekommt man Enumerationen und verarbeitet diese weiter. In diesem Fall profitiert man einfach nur und muss das schon mutwillig kaputt machen. Manchmal muss man jedoch die Enumeration initial produzieren, z.B.:

   1: public IEnumerable<Orderbook> GetAll()

   2: {

   3:     IEnumerable<Orderbook> result = null;

   4:     if (User.IsAdmin(user))

   5:         result = _context.OrderbookSet;

   6:     // [...]

   7:     return result;

   8: }

Genau hier fällt das Kind in den Brunnen wenn die Berechtigungen nicht passen. Ein einfacher Aufruf wie…

   1: var candidates= Orderbook.GetAll().Where(ob => ob.Location == "Frankfurt");

… ist damit nicht mehr machbar.

 

Korrekt wäre hingegen:

   1: public IEnumerable<Orderbook> GetAll()

   2: {

   3:     IEnumerable<Orderbook> result = Enumerable.Empty<Orderbook>();

   4:     if (User.IsAdmin(user))

   5:         result = _context.OrderbookSet;

   6:     // [...]

   7:     return result;

   8: }

Wenn der Aufrufer keine Berechtigung hat die Daten zu sehen, bekommt er immer noch eine Enumeration, nur enthält diese eben keine Elemente.

Etwas umständlicher wird das, wenn man mit dem Entity Framework gegen IQueryable arbeitet, denn Queryable bietet keine Pendant zu Empty() an. Wohl aber eine Methode AsQueryable(), die ich verwenden kann, um ein IEnumerable zu “casten”:

   1: public IQueryable<Orderbook> GetAll(string userName)

   2: {

   3:     IQueryable<Orderbook> result = Enumerable.Empty<Orderbook>().AsQueryable();

   4:     if (User.IsAdmin(user))

   5:         result = _context.OrderbookSet;

   6:     // [...]

   7: }

Es ist also nicht notwendig, extra eine Collection anzulegen, wie ich das auch schon gesehen habe:

   1: IQueryable<Orderbook> result = new List<Orderbook>().AsQueryable();

 

Da der C#-Compiler nicht wissen kann, dass eine Methode funktionalen Erfordernissen Rechnung tragen muss, kann er den Entwickler hier leider nicht unterstützen.

Daher liegt diese Anforderung nur in Form einer Konvention vor, die der Entwickler selbst einzuhalten hat – unter Androhung von Strafe. 😉