IIF in VB.net vs. ? : in C#
Just a quick thing that annoys me a teeny bit.
In C#, if you want to create a short one-line IF...Then...Else statement, you can use the ? : syntax - in practice that's:
string description = reader.IsDBNull(_indexDesc) ? null :
reader.GetString(_indexDesc).Trim();
This is a nice shortcut, as in C# - it doesn't evaluate the entire expression when it runs that statement.
The equivalent in VB.net is:
Dim description As String = _
IIf(reader.IsDBNull(_indexDesc), _
Nothing, _
reader.GetString(_indexDesc).Trim())
So both of the statements above should operate the same way, right?
Well, according to the top portion of the MSDN Documentation, it states:
"The IIf function provides a counterpart for the ternary Conditional Operator: ? : in Visual C++."
However, if you scroll Community Content section, there is an entry stating that this is in-fact not the case.
At runtime, VB.net will evaluate the entire expression when it runs the above statement. In this situation, if the DataReader returns True at that first conditional statement, you will get a run-time NullReferenceException. Not very nice at all.
If you look at the IL code for the C# code above - it compiles down to:
L_0094: ldarg.1
L_0095: ldarg.0
L_0096: ldfld int32 xxx.SiteMap.SqlSiteMapProvider::_indexDesc
L_009b: callvirt instance bool
[System.Data]System.Data.Common.DbDataReader::IsDBNull(int32)
L_00a0: brtrue.s L_00b5
L_00a2: ldarg.1
L_00a3: ldarg.0
L_00a4: ldfld int32 xxx.SiteMap.SqlSiteMapProvider::_indexDesc
L_00a9: callvirt instance string
[System.Data]System.Data.Common.DbDataReader::GetString(int32)
L_00ae: callvirt instance string [mscorlib]System.String::Trim()
L_00b3: br.s L_00b6
L_00b5: ldnull
You can see at the bolded line (L_00a0) that it very clearly does a jump to the end (L_00b5) if the first expression returns true. In fact, because of the shortcut here - it doesn't even evaluate the "else" clause if the condition returns true.
Now, if you look at the IL code for the VB.Net code above - it compiles down to:
L_009e: ldarg.1
L_009f: ldarg.0
L_00a0: ldfld int32 xxx.Web.UI.Providers.SqlSiteMapProvider::_indexDesc
L_00a5: callvirt instance bool
[System.Data]System.Data.Common.DbDataReader::IsDBNull(int32)
L_00aa: ldnull
L_00ab: ldarg.1
L_00ac: ldarg.0
L_00ad: ldfld int32 xxx.Web.UI.Providers.SqlSiteMapProvider::_indexDesc
L_00b2: callvirt instance string
[System.Data]System.Data.Common.DbDataReader::GetString(int32)
L_00b7: callvirt instance string [mscorlib]System.String::Trim()
L_00bc: call object [Microsoft.VisualBasic]
Microsoft.VisualBasic.Interaction::IIf(bool, object, object)
L_00c1: call string [Microsoft.VisualBasic]
Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
L_00c6: stloc.1
So what happens here? Well, since the IIF statement is a function in the Microsoft.VisualBasic namespace and not a language "feature" (as in C#) - there is really no special compilation. It completes both DataReader method calls (lines L_00a5 and L_00b2) within the IIF statement before it passes the values to the function. In this way, it behaves the same as when you call any other method (in either language for that matter).
So, what's the solution? Well, as has been documented here, here and here (among many other places), you can simply go back to the rather verbose:
Dim description As String
If reader.IsDBNull(_indexDesc) Then
description = Nothing
Else
description = _
reader.GetString(_indexDesc).Trim()
End If
This compiles itself down to:
L_009e: ldarg.1
L_009f: ldarg.0
L_00a0: ldfld int32 xxxx.Web.UI.Providers.SqlSiteMapProvider::_indexDesc
L_00a5: callvirt instance bool [System.Data]
System.Data.Common.DbDataReader::IsDBNull(int32)
L_00aa: stloc.s VB$CG$t_bool$S0
L_00ac: ldloc.s VB$CG$t_bool$S0
L_00ae: brfalse.s L_00b4
L_00b0: ldnull
L_00b1: stloc.1
L_00b2: br.s L_00c7
L_00b4: nop
L_00b5: ldarg.1
L_00b6: ldarg.0
L_00b7: ldfld int32 xxxx.Web.UI.Providers.SqlSiteMapProvider::_indexDesc
L_00bc: callvirt instance string [System.Data]
System.Data.Common.DbDataReader::GetString(int32)
L_00c1: callvirt instance string [mscorlib]System.String::Trim()
L_00c6: stloc.1
L_00c7: nop
If you look at the lines L_00b2, it clearly jumps down to L_00c7 if the branch returns true.
The other alternative is one that was actually suggested on the aforementioned MSDN page - in the Community Contributions section - which utilizes Generics:
Public Function IIf(Of T)(ByVal Expression As Boolean, _
ByVal TruePart As T, ByVal FalsePart As T) As T
If Expression Then
Return TruePart
Else
Return FalsePart
End If
End Function
Unfortunately, this doesn't really solve my initial problem - it just provides a type-safe way of returning a value from the IIF function (it normally just returns a value of type Object), which is then converted to a String (see line L_00c1 in the second IL snipped above).
In the end, the only true way to solve the problem is with the extremely verbose If...then syntax shown above. This means that for VB.net developers - it will require 6 lines to do the same thing that can be done in only one line in C#.
Sometimes it just doesn't pay to be a VB.net developer.
Technorati Tags:
vb.net,
c#,
.net,
IIF