Tuesday, October 10, 2006 6:00 PM bart

Answers to C# Quiz - Nullables, enums, casts and conversions

This post covers the answers to the C# Quiz - Nullables, enums, casts and conversions. If you didn't solve it yet, move away from this page and try to solve it yourself first. The real answer is still a few scrolls away on the average screen resolution so you can safely navigate to the original post without cheating :-).

The code

Anyway, let's get started by providing you with the code in a non-image format as promised:

using System;

class
NullableBrainTeaser
{
   private enum
TestEnum
   {
      Apple = 1,
      Banana = 2
   }

   static void Main(string
[] args)
   {
      try
{ One(TestEnum.Apple); }
      catch (Exception
ex) { Write(ex); }

      try { One(null
); }
      catch (Exception
ex) { Write(ex); }

      try
{ One(1); }
      catch (Exception
ex) { Write(ex); }

      try
{ Two(TestEnum.Apple); }
      catch (Exception
ex) { Write(ex); }

      try { Two(null
); }
      catch (Exception
ex) { Write(ex); }

      try
{ Two(1); }
      catch (Exception
ex) { Write(ex); }

      try
{ Three(TestEnum.Apple); }
      catch (Exception
ex) { Write(ex); }

      try
{ Three(null); }
      catch (Exception
ex) { Write(ex); }

      try
{ Three(1); }
      catch (Exception
ex) { Write(ex); }
   }

   static void One(object
bar)
   {
      TestEnum foo = (TestEnum)bar;
     
Console
.WriteLine(foo);
   }

   static void Two(object
bar)
   {
      TestEnum? foo = (TestEnum?)bar;
      Write(foo);
   }

   static void Three(object
bar)
   {
      TestEnum? foo = bar
as
TestEnum?;
      Write(foo);
   }

   static
void Write(TestEnum? foo)
   {
      Console.WriteLine(foo == null ? "null"
: foo.ToString());
   }

   static void Write(Exception
ex)
   {
      Console.ForegroundColor = ConsoleColor
.Red;
      Console
.WriteLine(ex.GetType());
      Console
.ResetColor();
   }
}

Method One

The first method is pretty straightforward:

   static void One(object bar)
   {
      TestEnum foo = (TestEnum)bar;
     
Console
.WriteLine(foo);
   }

Passing a TestEnum object as parameter bar will work fine, no question on that.

Passing a null value however will cause a NullReferenceException because the cast target is a value type and cannot hold a null reference. A little illustration of this case:

class Bla
{
   public static void
Main()
   {
      string s = Cast<string>(null);
//OK
      int i = Cast<int>(null);
//NOK
   }

   static T Cast<T>(object
o)
   {
      return
(T)o;
   }
}

Passing an integer value (i.e. 1 in the sample) will work fine too because enums support explicit casting from their base value type (which is an int in this case).

The first three lines of the output will be:

Apple
System.NullReferenceException
Apple

There's a bit more to know about this. Consider the IL for a moment (omitted the call to Write):

.method private hidebysig static void One(object bar) cil managed
{
.locals init (valuetype NullableBrainTeaser/TestEnum V_0)
IL_0001: ldarg.0
IL_0002: unbox.any NullableBrainTeaser/TestEnum
IL_0007: stloc.0
} // end of method NullableBrainTeaser::One

The key is the unbox.any instruction that was added in the .NET 2.0 CIL instruction set (see ECMA-335 June 06). The standard indicates that this instruction is equivalent to unbox+ldobj if the object on top of the stack is a boxed value type (it is if we pass 1), so in that case we'll end up with a valid TestEnum object. This sequence of unbox+ldobj calls should be somewhat familiar if you had prior exposure to IL (effectively it results in a cast). If a reference type is passes however, the standard specifies the operation to be equivalent to castclass, which does throw a NullReferenceException indeed if the specified object is a null reference.

Method Two

The second method uses a nullable TestEnum (Nullable<TestEnum> or TestEnum?):

   static void Two(object bar)
   {
      TestEnum? foo = (TestEnum?)bar;
      Write(foo);
   }

Let's start with the trivial case: passing a TestEnum object. This will work fine, since any type T can be casted (even implicity) to T?. You can see this in the SSCLI source black on white:

public struct Nullable<T> where T : struct
{
   ...

   public static implicit operator Nullable
<T>(T value) {
      return new Nullable
<T>(value);
   }

   ...
}

No wonder it works with explicit casting too. Just for the record, the implicit conversion works fine too:

TestEnum ee = TestEnum.Apple;
TestEnum? e = ee;

What about a null value? Any nullable type is a reference type (that's the whole point of the nullable type concept in the end) so assigning a null value to it isn't a problem. Casting to the nullable type isn't a problem either for the same reason of being reference type.

Somewhat less obvious might be a call to Two(1). This time the Two method is getting a System.Int32 value type as its înput and is asked to convert it to a TestEnum? nullable type. Converting it so a TestEnum wouldn't be a problem (as we saw in One) but TestEnum? is just one bridge too far. Let's explain what's going on by means of Two's IL code (omitted call to Write):

.method private hidebysig static void Two(object bar) cil managed
{
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype NullableBrainTeaser/TestEnum> V_0)
IL_0001: ldarg.0
IL_0002: unbox.any valuetype [mscorlib]System.Nullable`1<valuetype NullableBrainTeaser/TestEnum>
IL_0007: stloc.0
} // end of method NullableBrainTeaser::Two

The key is the unbox.any instruction that was added in the .NET 2.0 CIL instruction set (see ECMA-335 June 06). This instruction throws an InvalidCastException in a few situations, one of which being the following:

  • if the object on top of the stack (denoted as obj, in our case a boxed int value) is not as boxed value, etc... (this does not apply in our case)
  • if the type of the object on top of the stack (i.e. System.Int32) is not assignment compatible with the specified type token (this does apply!)

Indeed, an integer is not assignment compatible with a Nullable<TestEnum> type. Although you'd be able to assign an integer value to a TestEnum variable and you'd be able to assign that TestEnum variable to a TestEnum? variable on its turn, that's where it ends. Making the transition from an int to a Nullable<TestEnum> directly isn't possible.

Output for method Two therefore is:

Apple
null
System.InvalidCastException

Method Three

Finally, method Three. Only a slight difference with the second method:

   static void Three(object bar)
   {
      TestEnum? foo = bar
as
TestEnum?;
      Write(foo);
   }

Instead of doing a cast, we're using the as keyword. The difference? One extra line of IL:

.method private hidebysig static void Three(object bar) cil managed
{
.locals init (valuetype [mscorlib]System.Nullable`1<valuetype NullableBrainTeaser/TestEnum> V_0)
IL_0001: ldarg.0
IL_0002: isinst valuetype [mscorlib]System.Nullable`1<valuetype NullableBrainTeaser/TestEnum>
IL_0007: unbox.any valuetype [mscorlib]System.Nullable`1<valuetype NullableBrainTeaser/TestEnum>
IL_000c: stloc.0
} // end of method NullableBrainTeaser::Three

Simply stated, the isinst instruction casts the object on top of the stack to the specified type if it can do so and pushes it on ths stack. If it can't do the cast, it pushes null on the stack.

So, when passed a TestEnum object, things go fine because of the aforementioned (implicit) conversion operator on Nullable<T>. However, when passing in an integer value 1, the conversion can't take place because there is no direct conversion from an integer to an enum value (it requires an explicit conversion). When passing a null reference, isinst keeps the null reference. When unbox.any sees the null reference, the unbox.any operation is esual to castclass which returns null ("if obj is null, castclass succeeeds and returns null", III.4.3). So, no InvalidCastException this time but a simple null reference return (what the "as" keyword is supposed to do)

To wrap up, method Three produces:

Apple
null
null

I know, the differences are subtile, but that's what quizzes are for. Cheers!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:

Comments

No Comments