Value types in Visual C# include the primitive types such as int, float, and decimal. Value types also include enum and struct types (discussed in Chapter 2). Other types in the .NET Framework are also explicitly declared to be value types, such as the Rectangle, Point, and Size structures found in the System.Drawing namespace. These types often have short lifetimes and are rarely shared between multiple clients, so declaring them as value types improves performance and reduces the memory pressure on the garbage collection mechanism. The garbage collection mechanism is described in detail later in this chapter, in the section “Reference Type Lifetime and Garbage Collection.”
When a value type variable is declared in Visual C#, the variable contains the actual type instance. For example, an integer named Age is declared as follows:
int Age = 42;
The Visual C# .NET compiler will allocate 4 bytes of stack area for the Age variable, making it available for direct access without any indirection to the managed heap. For consistency, value type variables can also be declared using the new syntax, as shown here:
int Age = new int(42);
The effect of this statement might be surprising to C++ programmers: the compiler will detect that the variable is an instance of a value type and allocate it on the stack. In Visual C#, the allocation policy for an object depends on the type definition.
Value types have a lifetime that’s limited by their scope. Unlike an object allocated on the heap, a value type variable declared in a method call is allocated on the stack, as shown in Figure 3-1.
Value type variables live on the stack and are destroyed with the stack when a method returns. In Figure 3-1, the Main method calls the DisplayMsg method. As DisplayMsg begins executing, two values are added to the stack: the return address inside the Main method and a temporary stack variable. When the DisplayMsg method returns, the stack values are destroyed.
By default, parameters in Visual C# are passed by value. Local copies are made for each value type parameter—only the copy is used by the called method. Passing parameters by value can cause performance penalties when large value types are passed as parameters. Although the primitive types don’t carry a high cost when passed by value, the cost of copying every value stored in a structure can quickly add up, especially if the method is called frequently.
To avoid performance penalties when passing large value types as parameters, consider passing the parameters by reference. The ref and out parameter modifiers discussed in Chapter 2 can be used with value types to reduce the cost of passing a value type parameter. Instead of creating and copying a large structure, you’ll pass only a reference to the value, as shown here:
BigStruct s1, s2; CompareBigStructs(ref s1, ref s2);
Keep in mind that when a value type is passed by reference, the called method can change the value of the variable. The called method is accessing the original variable, not a copy. It’s up to you to determine whether the possible side effects justify the performance gains.
All types are derived from the object class, but the value types aren’t allocated on the managed heap. Value type variables actually contain the bits required to represent their values, so how then can these types be stored in arrays and used in methods that expect reference variables? The answer lies in a Visual C# feature known as boxing.
Visual C# enables you to box your value type variables so that they can be used exactly like reference types. Boxing effectively places the value type variable in a reference “box,” which is then used as a reference type variable, as shown in Figure 3-2.
Boxing usually occurs implicitly—for example, when a value type is assigned to an object or passed as a parameter that expects the object type. When a conversion from a value type to a reference type is required, the compiler will perform the boxing operation for you implicitly, as follows:
int Age = 42; object obj = Age; // Implicitly boxed
In contrast, when a conversion from reference type to value type is required, you must explicitly state your intentions because the conversion, known as unboxing, can easily result in lost data or an improper conversion. For example, if an int is boxed and stored in a reference to object, the following code will recover the value of the int:
object obj; int Age = (int)obj;
An unboxing attempt that might lead to data loss will cause an exception to be thrown. For example, the following code will cause an InvalidCastException to be thrown:
long feetPerLightYear; object obj = feetPerLightYear; int Age = (int)obj; // Not OK; overflows.
New value types are declared as struct types. The Visual C# .NET compiler will automatically derive new struct types from the System.ValueType class rather than directly from the object class. The System.ValueType class provides extra functionality for comparing value types for equality. By default, types derived from the object class compare for equality by testing for the location of the compared instances. Because two value type variables never have the same location, value type equality is determined by comparing the values stored by the variable.
A new value type requires a structure that contains the data used to store the data for your new type as well as a definition of the operations that can be performed on the type. Defining those operations requires a technique known as operator overloading, which is discussed in Chapter 4.