if(x == null || x.y == null || x.y.z == null) return null; return x.y.z.foobar;
Wenn ihr solchen Code, kennt und dauernd schreiben müsst, kommt hier die Lösung für euch:
Monad Extensions
Monads kommen an sich aus der Kategorientheorie und werden in Funktionalen Programmiersprachen eingesetzt. Da in funktionalen Programmiersprachen alle Funktionen Seiteneffektfrei sein müssen, dürfen sie auch nicht, wie in imperativen Programmiersprachen, Exceptions werfen. Daher gibt es dort Monads, welche Funktionen Enkapsulieren, welche Exceptions schmeißen könnten. Dafür gibt es dann den speziellen Rückgabewert „Nothing“, welcher von allen Werttypen ableitet. Da ich hier aber keine Erklärung von funktionaler Programmierung abliefern möchte, kommen wir zum Punkt:
Wie helfen und Monads in C#?
Ein Maybe Monad ist etwas relativ simples: Eine Funktion, die nur dann ausgeführt wird, wenn der übergebene Wert nicht „Nothing“ ist. Ist der übergebene Wert „Nothing“, dann wird die Ausführung an dieser Stelle gestoppt und es wird „Nothing“ zurückgegeben. In C# ist dieses „Nothing“ quasi der null-Wert.
Stellt euch 2 Funktionen vor:
Func<int, int ,int> substract = (x,y) => x - y; Func<int, int, int> divide = (x,y) => x / y;
An sich zwei relativ simple Funktionen (Ich weiß, divide mit nur Integern ist oft nicht gewollt, reicht hier aber für unsere Zwecke aus). Diese können nun wunderbar verschachtelt werden:
int x = divide(10, substract(2, 1)); // 10 int y = divide(30, substract(10, 4)); // 5 int z = divide(100, substract(3, 3)); // Exception
Aber halt! Haben wir nicht eben noch gesagt, dass Funktionen seiteneffektfrei sein müssen und keine Exceptions schmeißen dürfen? Anstatt eine Exception zu schmeißen, sollte unsere Funktion also lieber einen Wert zurückgeben, der angibt, dass die Berechnung nicht ausgeführt werden kann.
Da es in C# keine „Nothing“ gibt, haben wir hier verschiedene Möglichkeiten zur Auswahl:
- null gibt an, dass kein Wert existiert, kann aber nur für Referenztypen verwendet werden.
- 0 ist bei Zahlen das, was null am nächsten kommt.
- Nullable
gibt die Abwesenheit eines Werttypen an, kann aber nicht überall verwendet werden und kann eine Exception werfen, wenn man versucht auf den Wert zuzugreifen, obwohl keiner existiert (und Exceptions wollten wir ja gerade vermeiden)
Da null für Integer absolut keinen Sinn macht, gibt es noch 2 Möglichkeiten, divide zu implementieren:
Func<int, int, int> divide1 = (x,y) => y == 0 ? 0 : x / y; Func<int, int, int?> divide2 = (x,y) => y == 0 ? new int?() : x / y;
Nun können wir die Methoden verschachteln, ohne dass eine Exception geworfen werden kann. Abhängig vom Kontext kann für uns entweder die 1., oder die 2. Lösung von Vorteil sein.
int z1 = divide1(100, substract(3, 3)); // new int?() int z2 = divide2(100, substract(3, 3)); // 0
Da es aber aufwendig ist, alle Methoden so zu schreiben und wir uns massenhafte Null-Checks ja gerade sparen wollten, kommen hier die Monad Extensions ins Spiel. Diese sorgen dafür, dass eine Methode nur dann ausgeführt wird, wenn der Eingabewert nicht null ist. Sie machen also aus einer Methode eine Monad-Methode. Da dies in funktionalen Programmiersprachen als Maybe bekannt ist, nennen wir das ganze Monad Extensions 🙂
Um auf diese einfach zugreifen zu können, kann man diese als Extensionmethod zu object implementieren. Ob man dies tut, ist eine Glaubensfrage, da einige der Meinung sind, man sollte keine Extensionmethods zu object schreiben, da dadurch das Intellisense Menü zu überfrachtet wird. Ich habe hier jedoch diesen Weg gewählt, da es so deutlich angenehmer ist, diese zu benutzen. Wer dies nicht möchte, lässt einfach das this Keyword weg und erhällt normale statische Methoden.
Kommen wir also zu unser ersten Methode für Referenztypen, „With“:
public static TResult With<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator) where TResult : class where TInput : class { if (o == null) return null; return evaluator(o); }
Diese können wir uns nun zu Nutze machen, um Nullchecks zu sparen.
Anstatt
if(x == null || x.y == null || x.y.z == null) return null; return x.y.z.foobar;
Zu schreiben, können wir nun folgendes schreiben:
return x.With(_ => _.y).With(_ => _.z).With(_ => _.foobar);
Bei solch einfachen Werten bringt das noch nicht viel, bei zusammengesetzen Rückgabewerten, wird der Nutzen allerdings deutlicher:
var t1; var t2; var t3; if(x1 == null || x1.y == null || x1.y.z == null) t1 = null; else t1 = x1.y.z.foobar; if(x2 == null || x2.y == null || x2.y.z == null) t2 = null; else t2 = x2.y.z.foobar; if(x3 == null || x3.y == null || x3.y.z == null) t3 = null; else t3 = x3.y.z.foobar; return Tuple.Create(t1, t2, t3);
wird zu
return Tuple.Create(x1.With(_ => _.y).With(_ => _.z).With(_ => _.foobar), x2.With(_ => _.y).With(_ => _.z).With(_ => _.foobar), x2.With(_ => _.y).With(_ => _.z).With(_ => _.foobar));
Deutlich kürzer, oder?
Hier noch 2 Möglichkeiten, um mit Werttypen umzugehen (analog zu divide):
public static Nullable<TResult> WithNullable<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator) where TResult : struct where TInput : class { if (o == null) return new Nullable<TResult>(); return evaluator(o); } public static TResult WithValue<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator) where TResult : struct where TInput : class { if (o == null) return default(TResult); return evaluator(o); }
Besonders praktisch ist auch diese Methode für Collections, welche eine leere Collection zurückgibt, wenn der Parameter null ist.
public static IEnumerable<TSource> NullToEmpty<TSource>(this IEnumerable<TSource> source) { return source ?? Enumerable.Empty<TSource>(); }
Hierdurch lässt sich die Prüfung auf eine Collection, die entweder Null oder Leer sein könnte deutlisch vereinfachen:
if(!myCollection.NullToEmpty().Any()) DoSomething();
Ihr seht also: Mit den Monad Extensions kann man viele Dinge vereinfachen. Weitere nützliche Extension Methods findet ihr in der angehängten Datei.
Man sollte die Monad Extensions allerdings mit Bedacht einsetzen und ggf. doch den ein oder anderen „normalen“ Null-Check einbauen, da der Code hierdurch auch unleserlicher werden kann. In vielen fällen lässt sich der Code damit allerdings deutlich einfacher verständlich gestalten, als mit massenweise Null-Checks.