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: , , ,
Published 22 June 07 09:52 by Greg
Filed under:

Comments

# Samurai Programmer.com said on July 22, 2007 12:59 AM:

I had a rather lengthy blog entry about a month ago where I talked about the lack of a ternary operator

Anonymous comments are disabled