C#: Effiziente Wertabfrage von Expressions

17. November 2010

Der monatliche Office Day bei der SDX bietet immer wieder Anlass zu interessanten Diskussionen, die teilweise nette technische Schmankerl bereithalten. So unterhielt ich mich letzte Woche mit einem Kollegen über die Möglichkeit Guard-Klassen per C# Expressions zu schreiben. Mit einer solchen Klasse können leicht Argument-Prüfungen zu Beginn von Methoden durchgeführt werden, die Prüflogik wird dabei in der Guard-Klasse gekapselt.

Eine Methode einer solchen Guard-Klasse könnte z.B. AssertNotNull(...) sein, die prüft ob ein übergebenes Methodenargument null ist und in diesem Fall eine ArgumentNullException wirft. Mit Expressions kann der zu prüfende Parameter elegant an AssertNotNull() übergeben werden:

   1: public void MyMethod(SomeType elem)

   2: {

   3:     Guard.AssertNotNull(() => elem);

   4:     

   5:     // eigentliche Logik

   6: }

Über die Auswertung der Expression kann die AssertNotNull()-Methode sowohl den Argumentnamen als auch den Argumentwert extrahieren. Es müssen also nicht beide Werte als Parameter an AssertNotNull() übergeben werden (was zudem die Übergabe des Argumentnamens als string bedeuten würde).

Mit Kompilierung

Der Wert des angegebenen Arguments wird dabei einfacherweise oftmals durch Kompilieren und Aufrufen der Expression ermittelt. Diese Implementierung zeigt der folgende Codeausschnitt:

   1: public static class Guard

   2: {

   3:     public static void AssertNotNull<T>(Expression<Func<T>> selector)

   4:     {

   5:         T value = selector.Compile().Invoke();

   6:         if (value == null)

   7:         {

   8:             string name = ((MemberExpression)selector.Body).Member.Name;

   9:             throw new ArgumentNullException(name);

  10:         }

  11:     }

  12: }

Ohne Kompilierung

In unserer Unterhaltung berichtete mir mein Kollege allerdings von einer Möglichkeit den Wert auch ohne die aufwändige Kompilierung aus der Expression auszulesen. Möglich macht es eine Extraktion des Parameters als ConstantExpression, aus der man über eine FieldInfo-Instanz mit GetValue() den Wert erhält:

   1: public static class Guard

   2: {

   3:     public static void AssertNotNull<T>(Expression<Func<T>> selector)

   4:     {

   5:         var memberSelector = (MemberExpression)selector.Body;

   6:         var constantSelector = (ConstantExpression)memberSelector.Expression;

   7:         object value = ((FieldInfo)memberSelector.Member)

   8:             .GetValue(constantSelector.Value);

   9:         

  10:         if (value == null)

  11:         {

  12:             string name = ((MemberExpression)selector.Body).Member.Name;

  13:             throw new ArgumentNullException(name);

  14:         }

  15:     }

  16: }

Sieht erstmal nicht spektakulär aus, außer dass es komplizierter ist und als Voraussetzung wirklich direkt eine Variable in der Expression übergeben werden muss (Properties und Hierarchien funktionieren im Vergleich zur ersten Lösung nicht, was jedoch bei der direkten Prüfung von Methodenargumenten irrelevant ist).

Die Laufzeit macht’s

Warum also die Auswertung der Expression dem Kompilieren vorziehen? Ganz einfach: das Kompilieren kostet deutlich mehr Zeit! In einem kleinen Laufzeittest habe ich beide Lösungen gegenüber gestellt. Ein Aufruf der AssertNotNull()-Methode mit einem validen Argument (!= null) ergab über 10.000 Iterationen das folgende Ergebnis:

ExpressionAuswertung

Das muss man sich mal auf der Zunge zergehen lassen: die kompilierte Lösung benötigt über 53 Sekunden, die Lösung über die Auswertung der Expression ist in nicht einmal 0,35 Sekunden fertig! Das macht einen Zeitfaktor von über 156, den die nicht-kompilierte Lösung schneller ist als die kompilierte Umsetzung.

Fazit: Bei der Auswertung von Expressions sollte man immer auch auf die Laufzeit bedacht sein. Sobald eine Methode in einer Schleife aufgerufen wird und man nach defensiver Programmierung stets pingelig Methodenparameter prüft, kann eine Argumentprüfung zu einem nicht zu unterschätzenden Zeitfaktor werden. Eine Kompilierung der Expression sollte dabei möglichst vermieden werden…