Die 2 Fragezeichen ?? ... und die null

Nicht unbekannt - aber dennoch nicht gerade oft verwendet - fristet der C#-Operator ?? (der “null-coalescing operator“) sein dasein. Zu Unrecht? Lässt sich doch mit diesem recht einfach testen, ob in einer Objektvariable ein Objekt oder null steht und entsprechend etwas anderes zurückgeben. In dem Beispiel

1
2
3
string test1 = null, test2 = "irgendwas";
Trace.WriteLine(test1 ?? "(null)");
Trace.WriteLine(test2 ?? "(null)");

wird - wie man sich bestimmt schon denken kann - zuerst (null) und dann irgendwas ausgegeben. Oder zu Deutsch: Es wird der linke Operand zurückgegeben, falls dieser nicht null ist, ansonsten wird der rechte Operand zurückgegeben.

Damit wird einem C#-Entwickler eine sehr einfache und leserliche Möglichkeit gegeben, auf null-Werte zu reagieren. Selbstverständlich kann man mit dieser Schreibweise auch “Schindluder” betreiben und den Code so unleserlich wie nur irgend möglich gestalten. Wenn man das aber nicht vorhat, warum dann nicht verwenden?

Ich hab mir mal die verschiedenen Möglichkeiten zusammengeschrieben, die alle das Gleiche bewirken:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Version 1
return _test ?? "";
//Version 2
if( _test == null ) {
return "";
} else {
return _test;
}
//Version 3
if( _test != null ) {
return _test;
} else {
return "";
}
//Version 4
return _test == null ? "" : _test;
//Version 5
return _test != null ? _test : "";

Danach hab ich mir den generierten MSIL-Code angeschaut. Die Version 1 mit den ?? ist wirklich der kürzeste. Es wird nur eine lokale Variable verwendet. Die Variable auf dem Stack wird mittels dup dupliziert. In allen anderen Versionen wird die Variable 2x auf den Stack geschrieben (1x zum Testen, 1x zum zurückgeben, falls nicht null). Die Versionen mit der if-Abfrage verwenden sogar eine weitere lokale Variablen (bool) für das Testresultat.

Der IL-Code sieht also tatsächlich besser aus. Danach hab ich einen kleinen Test zusammengeschrieben. Sollten wirklich massive Unterschiede zu finden sein?

Naja, ich kann eindeutig sage: es kommt drauf an ;)
Gleich vorweg. Die Unterschiede sind eigentlich zu gering um überhaupt beachtet zu werden (weswegen ich auch auf die Darstellung und Beschreibung der y-Achse verzichte - die Unterschiede sind wirklich nicht relevant). Unterschiede gibt es aber dennoch. Insgesamt am Besten schnitt bei den Tests die Versionen 4 ab. Man sieht - auch weniger IL-Code ist nicht immer schneller ausgeführt. Es kommt immer auf die Optimierung an.

Nullables

Damit ?? aber nicht vollkommen unnütz dasteht, will ich noch eine relativ unbekannte Möglichkeit beschreiben. Auch arbeiten mit diesem Operator so zusammen, wie man sich das wünscht. D.h. ohne die Eigenschaft HasValue direkt angeben zu müssen. Mit den Anweisungen

1
2
3
4
5
int? test_1 = null;
int? test_2 = 3;
Trace.WriteLine( test_1 ?? 0 );
Trace.WriteLine( test_2 ?? 0 );

schreibt man somit erst 0 und dann 3 in das TraceLog. Im Hintergrund wird immer noch HasValue befragt (nur schreiben muss man es nicht). Die Performanceunterschiede sind zwar auch hier fast unnötig zu erwähnen, aber dennoch:

So haben dann auch die 2 Fragezeichen durchaus Ihre Berechtigung. Zumindest im Durchschnitt sind diese bei Nullable-Types zu bevorzugen.