C# Question Mark Alias and Operators
The C# language has several different uses for question marks. To programmers unfamiliar with these uses, it can be a bit confusing. However, once you know and understand them, they become second nature just like &&
and ||
.
Operator | Name | VS | C# | Framework |
---|---|---|---|---|
a? b: c |
Conditional Operator | 2003 | 1.2 | * |
a?? b |
Null-Coalescing Operator | 2005 | 2.0 | * |
T? |
Nullable Type Alias | 2005 | 2.0 | .NET 2.0 |
a?. b |
Null-Conditional Member Access Operator | 2015 | 6.0 | * |
a?[ b] |
Null-Conditional Index Operator | 2015 | 6.0 | * |
*The feature is a language extension only, and does not require a specific .NET Framework version.
Conditional Operator ( ?
:
)
Also known as the ternary operator.
The C# conditional operator is pretty straight-forward to use. It is conceptually like an if...then...else
for values. Or, perhaps as a better comparison, it is very similar to an MS Excel IF(a,b,c)
cell formula function.
The operator is actually a two-part operator that is always comprised of both ?
and :
parts. But, those two characters will never actually be next to each because the two parts of the operator are used to divide the three components of the expression. The usage looks something like this:
// If a is true then assign b to x, else assign c to x
x = a ? b : c;
In the simple example above, a
, b
and c
are all variables, where a
must be a Boolean and b
and c
must be of the same type as x
. But, there is no reason they have to be variables. Think of a
as a placeholder for any expression that evaluates to a Boolean, whether that be a literal, constant, variable, conditional expression, or the value returned by a property or method call.
Similarly, b
and c
are also any kind of expression. There is a restriction, however, that b
and c
must evaluate to the same type as each other, and to x
. This makes makes perfect sense since one of those two values will always get assigned to x
.
If x
is a reference type, the b
or c
is allowed to be a literal null
. This does not violate the restriction that b
and c
must be of the same type, because a literal null
does not technically have a type and can be assigned to any reference type.
x = a ? b : null;
x = a ? null : b;
But, if you are using nullable types (discussed later in this page), you will likely need to do some casting. Consider the example below. All it is trying to do is use the value of a
if it is not null
, or use 0
if a
is null
.
int? a = null;
// This does not build...
int x = (null == a) ? 0 : a;
//...but this does
int y = (null == a) ? 0 : (int)a;
In that example, the line that sets y
works, but the line that sets x
will generate a compiler error:
Cannot implicitly convert type 'int?' to 'int'. An explicit conversion exists (are you missing a cast?)
Even though it seems like the compiler should be smart enough to know that you want to do the cast and implicitly do it for you, it does not make that assumption. And, an int
is not an int?
so the "then value" and "else value" are different types, which is not allowed.
One of the most common uses of the conditional operator is to assign a default value if something is null, although that can also be accomplished in less code using the null-coalescing operator discussed later on this page.
// Traditional if...then...else
if (null == objA)
{
objB = new MyDefaultObject();
}
else
{
objB = objA;
}
// Conditional operator
objB = (null == objA)
? new MyDefaultObject()
: objA;
// Null-coalescing operator
objB = objA ?? MyDefaultObject();
When it comes to source code, maintainability is a must. In this regard, there are two key points that will go a long way towards making the conditional operator understandable by all:
- Format it for legibility
- Never, ever nest conditional operators
In the most recent example above, notice how the Conditional operator is formatted. Although it is not required to do so, the condition has been enclosed in ( )
parenthesis to make it resemble the conditions of more traditional code elements such as if ( )
and while ( )
. Additionally, the "then value" and "else value" have been place on separate lines, each preceded by the ?
or :
, to make it easier to visualize it as "then" and "else".
x = (condition)
? thenValue
: elseValue;
All of the examples thus far have assigned the result to a variable. But, there is no requirement to do so. You can assign an expression using a conditional operator to a field or property, or pass it in as a method argument, etc.. When passed as a method argument, I sometimes violate my rule about formatting the expression such that the "then value" and "else value" are on different lines. But, if the condition or either of it's return values is verbose, you should probably be pulling it out into a separate assignment anyway instead of passing it directly into a method. You should reserve passing an expression using conditional operator as a method parameter for the trivial cases such as a simple range check.
var x = DoSomeCalculationWithABC(
a,
(b < MIN_B) ? MIN_B : b,
c
);
As stated before, never, ever nest conditional operators!!! Do not use a second conditional operator for any part (condition, then value, else value) of a first conditional operator. Sure, C# allows it. But just 'cause you can do it don't mean it's to be done.
// Yuck! If you write this...
var x = a ? b : c ? d : e;
// ...did you mean this...
var y = (a)
? b
: (c ? d : e);
// ... or did you mean this?
var z = (a ? b : c)
? d
: e;
By the way, the answer is that x
and y
are equivalent in that example.
Null-Coalescing Operator ( ??
)
The null-coalescing operator was mentioned in the last example in the section on the conditional operators. Unlike the conditional operator which uses two separate symbols to divide three components, null-coalescing operator uses one 2-character symbol, that is two sequential question marks, with nothing between them:
// If a is not null then assign a to x. Otherwise, assign b to x
x = a ?? b;
The null-coalescing operator was released along side the nullable types, and for good reason: It provides a really convenient mechanism to provide a default value if a nullable type is currently null
.
int? a = null;
// If a is not null then assign a to x, otherwise assign 0 to x
int x = a ?? 0;
Unlike the conditional operator of the previous section, notice that a
did not have to be cast with (int)
in this example!
Also, unlike the conditional operator that becomes cumbersome to use when nested inside of another conditional operator, the null-coalescing operator works great nested inside of another null-coalescing operator!
x = a ?? b ?? c ?? d;
y = a ?? (b ?? (c ?? d) );
In the example above, x
and y
will always refer to the same object (a
, b
, c
or d
), or to null
if they are all null
, so there is really no need to use the parenthesis. The logic in that example above is conceptually something like this:
if (null != a)
{
x = a;
}
else if (null != b)
{
x = b;
}
else if (null != c)
{
x = c;
}
else
{
// At this point, it does not matter if d is null or non-null.
x = d;
}
But, it is a bit naive to thing that what the compiler generates is that simplistic, as that logic in the example above has three major drawbacks:
- Because the values (
a
,b
,c
) are compared againstnull
and then returned from a separate statement, it is very possible that the values might not have beennull
at the time they were checked, but were alreadynull
by the time they were set tox
(think threading, and event handler that was triggered while calling a method, etc.). - Recall that the values
a
,b
,c
andd
in that example are really place holders for anything that can generate a value. For example, they could be methods that return values. And, because the values are evaluated twice, if they were methods it might be "dangerous" to call the method twice. - The assignment to
x
appears nested under all of the conditions.
Therefore, it's perhaps better to conceptualize it more like this:
var action = new Funct<T>( () =>
{
// The value a is cached into aa, so aa can be compared and returned. That
// protects against the case where a is a variable that changes. It also means
// that a is only evaluated once, which is a big help if a is really a method call
// instead of a variable.
var aa = a as T;
if (null != aa)
{
return aa;
}
var bb = b as T;
if (null != bb)
{
return bb;
}
var cc = c as T;
if (null != cc)
{
return cc;
}
return d as T;
};
// The assignment is only in one place in the code.
T x = action.Invoke();
// It sure is alot less typing to do this!
T x = a ?? b ?? c ?? d;
As is the case with all of these ?
operators in C#, the IL generated by the compiler is more compact and efficient than any equivalent logic you can actually write in the C# language itself.
Although it is allowed in C#, I would not even consider using an expression with a conditional operator as one of the objects in the null-coalescing "chain"of objects. These operators are essentially "syntax candy" -- they are intended to give you a sweet way of doing something common with fewer lines to type, but if you stuff too much sweetness in at one time you're going to get a headache!
// Yuck! Does this...
x = a ?? b ? c : d ?? e;
//...mean this...
y = a ?? ( (b) ? c : d) ?? e;
//...or this?
z = a ?? ( (b) ? c : (d ?? e) )
In that example, what happens if b
is false and c
is null? In that case, y
and z
would have different results. In the case of y
, it would then check e
. But, in the case of z
, it would return null
(because c
is null) without consideration for e
.
Basically, if you find yourself trying mix too many different question mark operators, or trying to do something other then one of the following things with the ??
, then you're probably doing something wrong:
- Assign a default value to use if the value [of a nullable type] is
null
- Given a list of two or more objects separated by
??
operators, return the first object that is notnull
, returningnull
if all of the objects arenull
.
Nullable Type Alias (T?
)
Generics have been around a long time (C# 2.0, .NET 2.0). The Nullable<T>
generic struct was introduced as part of that that initial release. Since the C# language was expanded to support generics at the same time that the .NET Framework was being expanded to included generic types, the compiler was able to incorporate new C# language syntax that is short-hand for a Nullable<T>
.
// The following lines all compile into the exact same IL.
Nullable<System.Int32> a = null;
Nullable<int> a = null;
System.Int32? a = null;
int? a = null
In that example above, all four declarations create the exact same intermediate language and executable code. That is true because int
is just an alias for System.Int32
, and T?
is just an alias for Nullable<T>
. By definition, when you use an alias the compiler creates the exact same IL as if you did not use the alias.
I will probably cover Nullable<T>
itself in another article at some point, which would include topics like boxing and unboxing, and programmatically determining the type of a nullable variable. For this article on question marks in C#, however, it is enough to simply show how the question mark is used, which was done in that example above.
Null-Conditional Member Access Operator (?.
)
This much-needed operator was new for C# 6.0 (Visual Studio 2015). I cannot even count how many times I have had to do something like this:
if ( (null != a)
&& (null != a.b)
&& (null != a.b.c)
&& (null != a.b.c.d) )
{
x = a.b.c.d.e;
}
else
{
x = null;
}
Or perhaps you prefer something like this:
if ( (null == a)
|| (null == b)
|| (null == c)
|| (null == d) )
{
x = null;
}
else
{
x = a.b.c.d.e;
}
But, with new new null-conditional member access operator, you can now do the equivalent logic with much less code:
// Assign x to a.b.c.d.e if-and-only-if a, b, c & d are all non-null.
// But, if a, b, c and/or d is null, then assign x to null.
x = a?.b?.c?.d?.e;
Similar to the null-coalescing operator (??
), the null-conditional member access operator (?.
) also does caching to prevent the values from changing between when they were null-checked and when they are accessed to get the return value. That is, each of the components is only evaluated once. Therefore, you can think of the logic as something more like this:
var action = new Func<T>( () =>
{
var aa = a;
if (null == aa)
{
return null;
}
var bb = aa.b;
if (null == bb)
{
return null;
}
var cc = bb.c;
if (null == cc)
{
return null;
}
var dd = cc.d;
if (null == dd)
{
return null;
}
var ee = dd.e as T;
if (null == ee)
{
return null;
}
return ee;
};
T x = action.Invoke();
// Isn't this much nicer?
T x = a ?. b ?. c ?. d ?. e;
Because of the "caching" that is inherently part of the null-conditional member access operator, there is a particularly useful scenario when it comes to calling events and delegates:
// If nothing is "attached" to an event or delegate, it is null. You need to check it
// for null before you call it so that you don't get an exception. But, this code is
// not safe because MyEvent might have been non-null when it was null-checked, but
// null by the time it is actually called.
if (null != MyEvent)
{
MyEvent(this, eventArgs);
}
// Since a delegate is a value type, an easy way around that is to copy the delegate
// and do the null-check and call against the copy. That way, even if MyEvent becomes
// null, the code will still work on the previous value stored in myEvent.
var myEvent = MyEvent;
if (null != myEvent )
{
myEvent (this, eventArgs);
}
// But, because of the inherent caching built into the null-conditional member access
// operator, you can just do this!
MyEvent?.Invoke(this, eventArgs );
In section on the null-coalescing operator, I stated that you should probably not mix too many different question mark operators within the same expression because it gets too confusing. My exception to that rule is that it is ok to use a single null-coalescing operator (??
) at the end of the chain of null-conditional member access operators (?.
), where the null-coalescing operator is essentially used to specify a default value to use if the null-conditional member access operator chain evaluates to null
. But, even when doing that, it is useful to throw in some extra white spaces around the null-coalescing operators:
// If a, b, c, d and/or e is null, then assign x to f. Otherwise,
// assign x to a.b.c.d.e
x = a ?. b ?. c ?. d ?. e ?? f;
That works because the ?.
has higher precedence than ??
, which means it is the same as this:
x = ( a ?. b ?. c ?. d ?. e ) ?? f;
Null-Condition Index Operator (?[
]
)
The null-condition index operator is very similar to the null-conditional member access operator, accept that it is used to check if an object with an index operator is null
before attempting to access the index operator.
object[] array1 = null;
var a1 = array1?[0]; // a1 == null
var b1 = array1?[1]; // b1 == null
var c1 = array1?[2]; // c1 == null
object[] array2 = new object[]{
"Hello World", // 0
null}; // 1
var a2 = array2?[0]; // a2 == "Hello World"
var b2 = array2?[1]; // b2 == null
var c2 = array2?[2]; // Throws IndexOutOfRangeException !
There are three things of note when it comes to the null-condition index operator:
- It behaves pretty much just like the null-condition member access operator (
?.
) - If the value accessed at the specified index is itself
null
, you will not really know if the object containing the indexer was null or the object at the index isnull
(compare b1 vs b2 in the example above) - It does not protect against IndexOutOfRangeExceptions!
Programmer, Engineer