Constructors and Value Types [Ron Petrusha]

A number of recent customer comments have made it clear that some confusion surrounds the use of constructors with value types. Most common are the complaints that we've failed to document a constructor for a particular value type. In one case, though, a customer asked that we remove the documentation on constructors for value types, since he or she was certain that they are not supported by the .NET Framework.

Just as you can define parameterized constructors for reference types, you can define parameterized constructors for value types. For example, the following code defines a VehicleInfo structure that has two parameters: the number of wheels on a vehicle, and its number of doors.

[Visual Basic]

Public Structure VehicleInfo

   Private nWheels As Integer

   Private nDoors As Integer

   Public Sub New(ByVal wheels As Integer, ByVal doors As Integer)

      nWheels = wheels

      nDoors = doors

   End Sub

   Public ReadOnly Property NumberOfWheels() As Integer

      Get

      Return nWheels

      End Get

   End Property

   Public ReadOnly Property Doors() As Integer

      Get

         Return nDoors

      End Get

   End Property

End Structure

 

[C#]

using System;

public struct VehicleInfo

{

   private int nWheels;

   private int nDoors;

   public VehicleInfo(int wheels, int doors)

   {

      nWheels = wheels;

      nDoors = doors;

   }

   public int NumberOfWheels

   {

      get { return nWheels; }

   }

   public int Doors

   {

      get { return nDoors; }

   }

}

The example compiles normally, and when we use IL DASM to examine our type, we can see that it includes the parameterized constructor.

Constructors and Value Types in ILDASM

So far, we haven't observed any differences between defining constructors for value types and defining them for reference types. However, this is where the similarity ends. While we can define a default or parameterless constructor for reference types, the attempt to define a default constructor for value types produces a compiler error. For example, if we modify the previous code by replacing the parameterized constructor with a default constructor and adding setters for the structure's two properties, the Visual Basic compiler displays the following error message:

error BC30629: Structures cannot declare a non-shared 'Sub New' with no parameters.

The equivalent C# code also fails to compile, with a slightly different error message:

error CS0568: Structs cannot contain explicit parameterless constructors

Not only can we not define a parameterless constructor for value types, but compilers don't supply one automatically if there is no defined constructor, as they do for reference types. For example, if we again modify our source code by deleting the parameterless constructor and then successfully compile, we can again use IL DASM to inspect the members of our type. As the following figure shows, a parameterless constructor is not a member of the structure.

 

Constructors and Value Types in ILDASM

 

So to summarize, there are two major differences in the definition of constructors between value types and reference types:

  • You can define a parameterless constructor for a reference type. You cannot define one for a value type.
  • If you do not define any constructors, the compiler supplies a parameterless constructor for a reference type. It does not supply one for a value type.

Given this, it may seem surprising that if we attempt to instantiate a VehicleInfo object by calling its parameterless constructor, as the following example does, our code compiles successfully.

[Visual Basic]

Module Example

   Public Sub Main()

      Dim bicycle As New VehicleInfo()

      bicycle.Doors = 0

      bicycle.NumberOfWheels = 2

   End Sub

End Module

 

[C#]

using System;

public class Example

{

   public static void Main()

   {

      VehicleInfo bicycle = new VehicleInfo();

      bicycle.Doors = 0;

      bicycle.NumberOfWheels = 2;

   }

}

On the one hand, no parameterless constructor exists in our VehicleInfo type. On the other hand, we apparently are able to call this non-existent constructor. How can we explain this incongruous behavior?

Let's begin by using IL DASM to examine the MSIL for a "normal" call to a parameterless constructor from a class. For the purpose of the example, we can define a class with no members named TypicalClass and then instantiate it from another class that contains Main, the application entry point. The following is the MSIL emitted by a Visual Basic compiler for the Main method. Note the explicit call to the TypicalClass default constructor. (A C# compiler also emits an identical call to the TypicalClass default constructor.)

.method public static void Main() cil managed

{

  .entrypoint

  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )

  // Code size 7 (0x7)

  .maxstack 1

  .locals init (class TypicalClass V_0)

  IL_0000: newobj instance void TypicalClass::.ctor()

  IL_0005: stloc.0

  IL_0006: ret

} // end of method Example::Main

We can then compare this with the IL generated by our call to the non-existent VehicleInfo parameterless constructor. The following is the IL emitted by the C# compiler, although for our purposes the IL emitted by the Visual Basic compiler is identical.

.method public hidebysig static void Main() cil managed

{

  .entrypoint

  // Code size 28 (0x1c)

  .maxstack 2

  .locals init (valuetype [NoConstructor1]VehicleInfo V_0)

  IL_0000: nop

  IL_0001: ldloca.s V_0

  IL_0003: initobj [NoConstructor1]VehicleInfo

  IL_0009: ldloca.s V_0

  IL_000b: ldc.i4.0

  IL_000c: call instance void [NoConstructor1]VehicleInfo::set_Doors(int32)

  IL_0011: nop

  IL_0012: ldloca.s V_0

  IL_0014: ldc.i4.2

  IL_0015: call instance void [NoConstructor1]VehicleInfo::set_NumberOfWheels(int32)

  IL_001a: nop

  IL_001b: ret

} // end of method Example::Main

The call to the non-existent parameterless constructor in our source code has been replaced by a call to Initobj, an MSIL opcode that is documented at https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.initobj(VS.100).aspx. Initobj initializes each field in a value type to its default value.

This feature - the ability to instantiate a value type by using a language's constructor syntax to invoke a default value type even though one does not exist -- is known as the implicit default constructor. In C# in particular, it is necessary to use the implicit default constructor to instantiate value types that do not define parameterized constructors, since otherwise the C# compiler generates error CS0165: "Use of unassigned local variable 'bicycle'", because C# does not allow variables to be used before they're explicitly initialized.

On the other hand, Visual Basic does not  forbid the use of unassigned value types, since it provides a call to the implicit default constructor if none is present in source code. For example, the following Visual Basic code is similar to the previous example, except that it omits the call to the VehicleInfo default constructor.

[Visual Basic]

Module Example

   Public Sub Main()

      Dim bicycle As VehicleInfo

      bicycle.Doors = 0

      bicycle.NumberOfWheels = 2

   End Sub

End Module

The following is the MSIL emitted by a Visual Basic compiler for this version of the Main method.

.method public static void Main() cil managed

{

  .entrypoint

  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )

  // Code size 17 (0x11)

  .maxstack 2

  .locals init (valuetype [NoConstructor1]VehicleInfo V_0)

  IL_0000: ldloca.s V_0

  IL_0002: ldc.i4.0

  IL_0003: call instance void [NoConstructor1]VehicleInfo::set_Doors(int32)

  IL_0008: ldloca.s V_0

  IL_000a: ldc.i4.2

  IL_000b: call instance void [NoConstructor1]VehicleInfo::set_NumberOfWheels(int32)

  IL_0010: ret

} // end of method Example::Main

In this case, the Initobj opcode has been replaced by init, but the result is the same: each field of a value type is initialized to its default value. In Visual Basic, an implicit default constructor is invoked whether or not that constructor is actually used in source code.