8. Value Types

You have used value types throughout this book; for example, int is a value type. This chapter discusses not only using value types, but also defining custom value types. There are two categories of custom value types: structs and enums. This chapter discusses how structs enable programmers to define new value types that behave very similarly to most of the predefined types discussed in Chapter 2. The key is that any newly defined value types have their own custom data and methods. The chapter also discusses how to use enums to define sets of constant values.

Image

Structs

All of the C# “built-in” types, such as bool and decimal, are value types, with the exception of string and object, which are reference types. Numerous additional value types are provided within the framework. It is also possible for developers to define their own value types.

To define a custom value type, you use a similar syntax as you would use to define class and interface types. The key difference in the syntax is that value types use the keyword struct, as shown in Listing 8.1. Here we have a value type that describes a high-precision angle in terms of its degrees, minutes, and seconds. (A “minute” is one-sixtieth of a degree, and a second is one-sixtieth of a minute. This system is used in navigation because it has the nice property that an arc of one minute over the surface of the ocean at the equator is exactly one nautical mile.)

Begin 6.0

LISTING 8.1: Declaring a struct


// Use keyword struct to declare a value type.
struct Angle                                                                 
{
  public Angle(int degrees, int minutes, int seconds)
  {
      Degrees = degrees;
      Minutes = minutes;
      Seconds = seconds;
  }

  // Using C# 6.0 read-only, automatically implememted properties.
  public int Degrees { get; }
  public int Minutes { get; }
  public int Seconds { get; }

  public Angle Move(int degrees, int minutes, int seconds)
  {
      return new Angle(
          Degrees + degrees,
          Minutes + minutes,
          Seconds + seconds);
  }
}


// Declaring a class--a reference type
// (declaring it as a struct would create a value type
// larger than 16 bytes.)
class Coordinate
{
  public Angle Longitude { get; set; }

  public Angle Latitude { get; set; }
}


This listing defines Angle as a value type that stores the degrees, minutes, and seconds of an angle, either longitude or latitude. The resultant C# type is a struct.

Note that the Angle struct in Listing 8.1 is immutable because all properties are declared using C# 6.0’s read-only, automatically implemented property capability. To create a read-only property without C# 6.0, programmers will need to declare a property with only a getter that accesses its data from a readonly modified field (see Listing 8.3). C# 6.0 provides a noticeable code reduction when it comes to defining immutable types.


Note

Although nothing in the language requires it, a good guideline is for value types to be immutable: Once you have instantiated a value type, you should not be able to modify the same instance. In scenarios where modification is desirable, you should create a new instance. Listing 8.1 supplies a Move() method that doesn’t modify the instance of Angle, but instead returns an entirely new instance.

There are two good reasons for this guideline. First, value types should represent values. One does not think of adding two integers together as mutating either of them; rather, the two addends are immutable and a third value is produced as the result.

Second, because value types are copied by value, not by reference, it is very easy to get confused and incorrectly believe that a mutation to one value type variable can be observed to cause a mutation in another, as it would with a reference type.



Guidelines

DO create value types that are immutable.


Initializing Structs

In addition to properties and fields, structs may contain methods and constructors. However, user-defined default (parameterless) constructors were not allowed until C# 6.0. When no default constructor is provided, the C# compiler automatically generates a default constructor that initializes all fields to their default values. The default value is null for a field of reference type data, a zero value for a field of numeric type, false for a field of Boolean type, and so on.

To ensure that a local value type variable can be fully initialized by a constructor, every constructor in a struct must initialize all fields (and read-only, automatically implemented properties) within the struct. (In C# 6.0, initialization via a read-only, automatically implemented property is sufficient because the backing field is unknown and its initialization would not be possible.) Failure to initialize all data within the struct causes a compile-time error. To complicate matters slightly, C# disallows field initializers in a struct. Listing 8.2, for example, will not compile if the line _Degrees = 42 was uncommented.

LISTING 8.2: Initializing a struct Field within a Declaration, Resulting in an Error


struct Angle
{
  // ...
  // ERROR:  Fields cannot be initialized at declaration time
  // int _Degrees = 42;
  // ...
}


If not explicitly instantiated via the new operator’s call to the constructor, all data contained within the struct is implicitly initialized to that data’s default value. However, all data within a value type must be explicitly initialized to avoid a compiler error. This raises a question: When might a value type be implicitly initialized but not explicitly instantiated? This situation occurs when instantiating a reference type that contains an unassigned field of value type as well as when instantiating an array of value types without an array initializer.

To fulfill the initialization requirement on a struct, all explicitly declared fields must be initialized. Such initialization must be done directly. For example, in Listing 8.3, the constructor that initializes the property (if uncommented out) rather than the field produces a compile error.

LISTING 8.3: Accessing Properties before Initializing All Fields


  struct Angle
                 {
  // ERROR:  The 'this' object cannot be used before
  //         all of its fields are assigned to
  // public Angle(int degrees, int minutes, int seconds)
  // {
  //     Degrees = degrees;                                            
  //     Minutes = minutes;                                            
  //     Seconds = seconds;                                            
  // }

  public Angle(int degrees, int minutes, int seconds)
  {
      _Degrees = degrees;
      _Minutes = minutes;
      _Seconds = seconds;
  }

  public int Degrees { get { return _Degrees; } }
  readonly private int _Degrees;

  public int Minutes { get { return _Minutes; } }
  readonly private int _Minutes;

  public int Seconds { get { return _Seconds; } }
  readonly private int _Seconds;

  // ...
   }


It is not legal to access this until the compiler knows that all fields have been initialized; the use of Degrees is implicitly this.Degrees. To resolve this issue, you need to initialize the fields directly, as demonstrated in the constructor of Listing 8.3 that is not commented out.

Because of the struct’s field initialization requirement, the succinctness of C# 6.0’s read-only, automatically implemented property support, and the guideline to avoid accessing fields from outside of their wrapping property, you should favor read-only, automatically implemented properties over fields within structs starting with C# 6.0.


Guidelines

DO ensure that the default value of a struct is valid; it is always possible to obtain the default “all zero” value of a struct.


Using the default Operator

As described earlier, if no default constructor is provided (which is possible only starting with C# 6.0), all value types have an automatically defined default constructor that initializes the storage of a value type to its default state. Therefore, it is always legal to use the new operator to create a value type instance. As an alternative syntax, you can use the default operator to produce the default value for a struct. In Listing 8.4, we add a second constructor to the Angle struct that uses the default operator on int as an argument to the previously declared three-argument constructor.

LISTING 8.4: Using the default Operator to Obtain the Default Value of a Type


// Use keyword struct to declare a value type.
struct Angle                                               
{
  public Angle(int degrees, int minutes)
      : this( degrees, minutes, default(int) )
  {
  }

  // ...
}


The expressions default(int) and new int() both produce the same value. In contrast, that is not necessarily the case for custom-defined value types if the constructor is a C# 6.0 custom default constructor. In C# 6.0, a default constructor initializes its data to nondefault values. The result is that an invocation of the default constructor—which requires the new operator—would not produce the same value that default(T) produces. Like reference types, custom default constructors are invoked only explicitly via the new operator. However, unlike reference types, whose default value is null, implicit initialization of value types results in a zeroed-out memory block equivalent to the result of the default operator. Hence, default(T) is not necessarily equivalent to new T() when a value type has a default constructor. Furthermore, accessing the implicitly initialized value type is a valid operation; accessing the default value of a reference type, in contrast, would produce a NullReferenceException. For this reason, you should take care to explicitly initialize value types with custom default constructors if the default(T) value is not a valid state for the type.


Note

Default constructors on value types are invoked only by explicit uses of the new operator.


End 6.0

Inheritance and Interfaces with Value Types

All value types are implicitly sealed. In addition, all non-enum value types derive from System.ValueType. As a consequence, the inheritance chain for structs is always from object to System.ValueType to the struct.

Value types can implement interfaces, too. Many of those built into the framework implement interfaces such as IComparable and IFormattable.

System.ValueType brings with it the behavior of value types, but it does not include any additional members. The System.ValueType customizations focus on overriding all of object’s virtual members. The rules for overriding base class methods in a struct are almost the same as those for classes (see Chapter 9). However, one difference is that with value types, the default implementation for GetHashCode() is to forward the call to the first non-null field within the struct. Also, Equals() makes significant use of reflection. Therefore, if a value type is used frequently inside collections, especially dictionary-type collections that use hash codes, the value type should include overrides for both Equals() and GetHashCode() to ensure good performance. See Chapter 9 for more details.


Guidelines

DO overload the equality operators (Equals(), ==, and !=) on value types, if equality is meaningful. (Also consider implementing the IEquatable<T> interface.)


Boxing

We know that variables of value type directly contain their data, whereas variables of reference type contain a reference to another storage location. But what happens when a value type is converted to one of its implemented interfaces or to its root base class, object? The result of the conversion has to be a reference to a storage location that contains something that looks like an instance of a reference type, but the variable contains a value of value type. Such a conversion, which is known as boxing, has special behavior. Converting a variable of value type that directly refers its data to a reference type that refers to a location on the garbage-collected heap involves several steps.

1. Memory is allocated on the heap that will contain the value type’s data and the other overhead necessary to make the object look like every other instance of a managed object of reference type (namely, a SyncBlockIndex and method table pointer).

2. The value of the value type is copied from its current storage location into the newly allocated location on the heap.

3. The result of the conversion is a reference to the new storage location on the heap.

The reverse operation is unboxing. The unboxing conversion checks whether the type of the boxed value is compatible with the type to which the value is being unboxed, and then results in a copy of the value stored in the heap location.

Boxing and unboxing are important to consider because boxing has some performance and behavioral implications. Besides learning how to recognize these conversions within C# code, a developer can count the box/unbox instructions in a particular snippet of code by looking through the CIL. Each operation has specific instructions, as shown in Table 8.1.

Image

TABLE 8.1: Boxing Code in CIL

When boxing and unboxing occur infrequently, their implications for performance are irrelevant. However, boxing can occur in some unexpected situations, and frequent occurrences can have a significant impact on performance. Consider Listing 8.5 and Output 8.1. The ArrayList type maintains a list of references to objects, so adding an integer or floating-point number to the list will box the value so that a reference can be obtained.

LISTING 8.5: Subtle Box and Unbox Instructions


class DisplayFibonacci
{
  static void Main()
  {

      int totalCount;
      System.Collections.ArrayList list =
          new System.Collections.ArrayList();

      Console.Write("Enter a number between 2 and 1000:");
      totalCount = int.Parse(Console.ReadLine());

      // Execution-time error:
      // list.Add(0);  // Cast to double or 'D' suffix required.
                       // Whether cast or using 'D' suffix,
                       // CIL is identical.
      list.Add((double)0);
      list.Add((double)1);
      for (int count = 2; count < totalCount; count++)
      {
          list.Add(
              ((double)list[count - 1] +
              (double)list[count - 2]) );
      }

      foreach (double count in list)
      {
          Console.Write("{0}, ", count);
      }
  }
}


OUTPUT 8.1

Enter a number between 2 and 1000:42
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141,

The code shown in Listing 8.5, when compiled, produces five box and three unbox instructions in the resultant CIL.

1. The first two box instructions occur in the initial calls to list.Add(). The signature for the ArrayList method is int Add(object value). As such, any value type passed to this method is boxed.

2. Next are two unbox instructions in the call to Add() within the for loop. The return from an ArrayList’s index operator is always object because that is what ArrayList contains. To add the two values, you need to cast them back to doubles. This cast from a reference to an object to a value type is implemented as an unbox call.

3. Now you take the result of the addition and place it into the ArrayList instance, which again results in a box operation. Note that the first two unbox instructions and this box instruction occur within a loop.

4. In the foreach loop, you iterate through each item in ArrayList and assign the items to count. As you saw earlier, the items within ArrayList are references to objects, so assigning them to a double is, in effect, unboxing each of them.

5. The signature for Console.WriteLine(), which is called within the foreach loop, is void Console.Write(string format, object arg). As a result, each call to it boxes the double to object.

Every boxing operation involves both an allocation and a copy; every unboxing operation involves a type check and a copy. Doing the equivalent work using the unboxed type would eliminate the allocation and type check. Obviously, you can easily improve this code’s performance by eliminating many of the boxing operations. Using an object rather than double in the last foreach loop is one such improvement. Another would be to change the ArrayList data type to a generic collection (see Chapter 11). The point being made here is that boxing can be rather subtle, so developers need to pay special attention and notice situations where it could potentially occur repeatedly and affect performance.

Another unfortunate boxing-related problem also occurs at runtime: If you wanted to change the initial two Add() calls so that they did not use a cast (or a double literal), you would have to insert integers into the array list. Since ints will implicitly be converted to doubles, this would appear to be an innocuous modification. However, the casts to double from within the for loop, and again in the assignment to count in the foreach loop, would fail. The problem is that immediately following the unbox operation is an attempt to perform a memory copy of the value of the boxed int into a double. You cannot do this without first casting to an int, because the code will throw an InvalidCastException at execution time. Listing 8.6 shows a similar error commented out and followed by the correct cast.

LISTING 8.6: Unboxing Must Be to the Underlying Type


      // ...
      int number;
      object thing;
      double bigNumber;

      number = 42;
      thing = number;
      // ERROR: InvalidCastException
      // bigNumber = (double)thing;
      bigNumber = (double)(int)thing;
      // ...


Enums

Compare the two code snippets shown in Listing 8.9.

LISTING 8.9: Comparing an Integer Switch to an Enum Switch


  int connectionState;
  // ...
  switch (connectionState)
  {
      case 0:
          // ...
          break;
      case 1:
          // ...
          break;
      case 2:
          // ...
          break;
      case 3:
          // ...
          break;
  }


  ConnectionState connectionState;
  // ...
  switch (connectionState)
  {
      case ConnectionState.Connected:
          // ...
          break;
      case ConnectionState.Connecting:
          // ...
          break;
      case ConnectionState.Disconnected:
          // ...
          break;
      case ConnectionState.Disconnecting:
          // ...
          break;
  }


Obviously, the difference in terms of readability is tremendous—in the second snippet, the cases are self-documenting. However, the performance at runtime is identical. To achieve this outcome, the second snippet uses enum values in each case.

An enum is a value type that the developer can declare. The key characteristic of an enum is that it declares at compile time a set of possible constant values that can be referred to by name, thereby making the code easier to read. The syntax for a typical enum declaration is show in Listing 8.10.

LISTING 8.10: Defining an Enum


enum ConnectionState
{
  Disconnected,
  Connecting,
  Connected,
  Disconnecting
}



Note

An enum can be used as a more readable replacement for Boolean values as well. For example, a method call such as SetState(true) is less readable than SetState(DeviceState.On).


You use an enum value by prefixing it with the enum name. To use the Connected value, for example, you would use the syntax ConnectionState.Connected. Do not make the enum type name a part of the value’s name so as to avoid the redundancy of something such as ConnectionState.ConnectionStateConnected. By convention, the enum name itself should be singular (unless the enums are bit flags, as discussed shortly). That is, the nomenclature should be ConnectionState, not ConnectionStates.

Enum values are actually implemented as nothing more than integer constants. By default, the first enum value is given the value 0, and each subsequent entry increases by 1. However, you can assign explicit values to enums, as shown in Listing 8.11.

LISTING 8.11: Defining an Enum Type


enum ConnectionState : short
{
  Disconnected,
  Connecting = 10,
  Connected,
  Joined = Connected,
  Disconnecting
}


In this code, Disconnected has a default value of 0 and Connecting has been explicitly assigned 10; consequently, Connected will be assigned 11. Joined is assigned 11, the value assigned to Connected. (In this case, you do not need to prefix Connected with the enum name, since it appears within its scope.) Disconnecting is 12.

An enum always has an underlying type, which may be any integral type other than char. In fact, the enum type’s performance is identical to that of the underlying type. By default, the underlying value type is int, but you can specify a different type using inheritance type syntax. Instead of int, for example, Listing 8.11 uses a short. For consistency, the syntax for enums emulates the syntax of inheritance, but this doesn’t actually make an inheritance relationship. The base class for all enums is System.Enum, which in turn is derived from System.ValueType. Furthermore, these classes are sealed; you can’t derive from an existing enum type to add additional members.


Guidelines

CONSIDER using the default 32-bit integer type as the underlying type of an enum. Use a smaller type only if you must do so for interoperability or performance reasons; use a larger type only if you are creating a flags enum (see the discussion later in this chapter) with more than 32 flags.


An enum is really nothing more than a set of names thinly layered on top of the underlying type; there is no mechanism that restricts the value of a variable of enumerated type to just the values named in the declaration. For example, because it is possible to cast the integer 42 to short, it is also possible to cast the integer 42 to the ConnectionState type, even though there is no corresponding ConnectionState enum value. If the value can be converted to the underlying type, the conversion to the enum type will also be successful.

The advantage of this odd feature is that enums can have new values added in later API releases, without breaking earlier versions. Additionally, the enum values provide names for the known values while still allowing unknown values to be assigned at runtime. The burden is that developers must code defensively for the possibility of unnamed values. It would be unwise, for example, to replace case ConnectionState.Disconnecting with default and expect that the only possible value for the default case was ConnectionState.Disconnecting. Instead, you should handle the Disconnecting case explicitly and the default case should report an error or behave innocuously. As indicated earlier, however, conversion between the enum and the underlying type, and vice versa, requires an explicit cast; it is not an implicit conversion. For example, code cannot call ReportState(10) if the method’s signature is void ReportState(ConnectionState state). The only exception occurs when passing 0, because there is an implicit conversion from 0 to any enum.

Although you can add more values to an enum in a later version of your code, you should do so with care. Inserting an enum value in the middle of an enum will bump the values of all later enums (adding Flooded or Locked before Connected will change the Connected value, for example). This will affect the versions of all code that is recompiled against the new version. However, any code compiled against the old version will continue to use the old values, making the intended values entirely different. Besides inserting an enum value at the end of the list, one way to avoid changing enum values is to assign values explicitly.


Guidelines

CONSIDER adding new members to existing enums, but keep in mind the compatibility risk.

AVOID creating enums that represent an “incomplete” set of values, such as product version numbers.

AVOID creating “reserved for future use” values in an enum.

AVOID enums that contain a single value.

DO provide a value of 0 (none) for simple enums, knowing that 0 will be the default value when no explicit initialization is provided.


Enums are slightly different from other value types because they derive from System.Enum before deriving from System.ValueType.

Type Compatibility between Enums

C# also does not support a direct cast between arrays of two different enums. However, the CLR does, provided that both enums share the same underlying type. To work around this restriction of C#, the trick is to cast first to System.Array, as shown at the end of Listing 8.12.

LISTING 8.12: Casting between Arrays of Enums


enum ConnectionState1
{
  Disconnected,
  Connecting,
  Connected,
  Disconnecting
}


enum ConnectionState2
{
  Disconnected,
  Connecting,
  Connected,
  Disconnecting
}


class Program
{
  static void Main()
  {
      ConnectionState1[] states =
          (ConnectionState1[])(Array)new ConnectionState2[42];
  }
}


This example exploits the fact that the CLR’s notion of assignment compatibility is more lenient than C#’s concept. (The same trick is possible for other illegal conversions, such as int[] to uint[].) However, use this approach cautiously because there is no C# specification requiring that this behavior work across different CLR implementations.

Converting between Enums and Strings

One of the conveniences associated with enums is that the ToString() method, which is called by methods such as System.Console.WriteLine(), writes out the enum value identifier:

  System.Diagnostics.Trace.WriteLine(
      $"The connection is currently { ConnectionState.Disconnecting }");

The preceding code will write the text in Output 8.3 to the trace buffer.

OUTPUT 8.3

The connection is currently Disconnecting.

Conversion from a string to an enum is a little more difficult to achieve, because it involves a static method on the System.Enum base class. Listing 8.13 provides an example of how to do it without generics (see Chapter 11), and Output 8.4 shows the results.

LISTING 8.13: Converting a String to an Enum Using Enum.Parse()


ThreadPriorityLevel priority = (ThreadPriorityLevel)Enum.Parse(
  typeof(ThreadPriorityLevel), "Idle");
Console.WriteLine(priority);


OUTPUT 8.4

Idle

In this code, the first parameter to Enum.Parse() is the type, which you specify using the keyword typeof(). This example depicts a compile-time way of identifying the type, like a literal for the type value (see Chapter 17).

Until .NET Framework 4, there was no TryParse() method, so code written to target prior versions needs to include appropriate exception handling if there is a chance the string will not correspond to an enum value identifier. .NET Framework 4’s TryParse<T>() method uses generics, but the type parameters can be inferred, resulting in the to-enum conversion behavior shown in Listing 8.14.

LISTING 8.14: Converting a String to an Enum Using Enum.TryParse<T>()


System.Diagnostics.ThreadPriorityLevel priority;
if(Enum.TryParse("Idle", out priority))
{
  Console.WriteLine(priority);
}


This technique eliminates the need to use exception handling if the string might not convert successfully. Instead, code can check the Boolean result returned from the call to TryParse<T>().

Regardless of whether the code uses the “Parse” or “TryParse” approach, the key caution about converting from a string to an enum is that such a cast is not localizable. Therefore, developers should use this type of cast only for messages that are not exposed to users (assuming localization is a requirement).


Guidelines

AVOID direct enum/string conversions where the string must be localized into the user’s language.


Enums As Flags

Many times, developers not only want enum values to be unique, but also want to be able to represent a combination of values. For example, consider System.IO.FileAttributes. This enum, shown in Listing 8.15, indicates various attributes on a file: read-only, hidden, archive, and so on. Unlike with the ConnectionState attribute, where each enum value was mutually exclusive, the FileAttributes enum values can and are intended for combination: A file can be both read-only and hidden. To support this behavior, each enum value is a unique bit.

LISTING 8.15: Using Enums As Flags


[Flags] public enum FileAttributes
{
  ReadOnly =          1<<0,      // 000000000000000001
  Hidden =            1<<1,      // 000000000000000010
  System =            1<<2,      // 000000000000000100
  Directory =         1<<4,      // 000000000000010000
  Archive =           1<<5,      // 000000000000100000
  Device =            1<<6,      // 000000000001000000
  Normal =            1<<7,      // 000000000010000000
  Temporary =         1<<8,      // 000000000100000000
  SparseFile =        1<<9,      // 000000001000000000
  ReparsePoint =      1<<10,     // 000000010000000000
  Compressed =        1<<11,     // 000000100000000000
  Offline =           1<<12,     // 000001000000000000
  NotContentIndexed = 1<<13,     // 000010000000000000
  Encrypted =         1<<14,     // 000100000000000000
  IntegrityStream =   1<<15,     // 001000000000000000
  NoScrubData  =         1<<17,     // 100000000000000000
}



Note

Note that the name of a bit flags enum is usually pluralized, indicating that a value of the type represents a set of flags.


To join enum values, you use a bitwise OR operator. To test for the existence of a particular bit you use the bitwise AND operator. Both cases are illustrated in Listing 8.16.

LISTING 8.16: Using Bitwise OR and AND with Flag Enums


using System;
using System.IO;

public class Program
{
public static void Main()
{
      // ...

      string fileName = @"enumtest.txt";

      System.IO.FileInfo file =
          new System.IO.FileInfo(fileName);

      file.Attributes = FileAttributes.Hidden |
          FileAttributes.ReadOnly;

      Console.WriteLine("{0} | {1} = {2}",
          FileAttributes.Hidden, FileAttributes.ReadOnly,
          (int)file.Attributes);

      if ( (file.Attributes & FileAttributes.Hidden) !=
          FileAttributes.Hidden)
      {
          throw new Exception("File is not hidden.");
      }

      if (( file.Attributes & FileAttributes.ReadOnly) !=
          FileAttributes.ReadOnly)
      {
          throw new Exception("File is not read-only.");
      }

      // ...
}


The results of Listing 8.16 appear in Output 8.5.

OUTPUT 8.5

Hidden | ReadOnly = 3

Using the bitwise OR operator allows you to set the file to both read-only and hidden. In addition, you can check for specific settings using the bitwise AND operator.

Each value within the enum does not need to correspond to only one flag. It is perfectly reasonable to define additional flags that correspond to frequent combinations of values. Listing 8.17 shows an example.

LISTING 8.17: Defining Enum Values for Frequent Combinations


[Flags] enum DistributedChannel
{
  None = 0,
  Transacted = 1,
  Queued = 2,
  Encrypted = 4,
  Persisted = 16,
  FaultTolerant =                                                 
      Transacted | Queued | Persisted                             
}


It is a good practice to have a zero None member in a flags enum because the initial default value of a field of enum type or an element of an array of enum type is 0. Avoid enum values corresponding to items such as Maximum as the last enum, because Maximum could be interpreted as a valid enum value. To check whether a value is included within an enum, use the System.Enum.IsDefined() method.


Guidelines

DO use the FlagsAttribute to mark enums that contain flags.

DO provide a None value equal to 0 for all flag enums.

AVOID creating flag enums where the zero value has a meaning other than “no flags are set.”

CONSIDER providing special values for commonly used combinations of flags.

DO NOT include “sentinel” values (such as a value called Maximum); such values can be confusing to the user.

DO use powers of 2 to ensure that all flag combinations are represented uniquely.


Summary

This chapter began with a discussion of how to define custom value types. Because it is easy to write confusing or buggy code when mutating value types, and because value types are typically used to model immutable values, it is a good idea to make value types immutable. We also described how value types are “boxed” when they must be treated polymorphically as reference types.

The idiosyncrasies introduced by boxing are subtle, and the vast majority of them lead to problematic issues at execution time rather than at compile time. Although it is important to know about these quirks so as to try to avoid them, in many ways paying too much attention to the potential pitfalls overshadows the usefulness and performance advantages of value types. Programmers should not be overly concerned about using value types. Value types permeate virtually every chapter of this book, yet the idiosyncrasies associated with them come into play infrequently. We have staged the code surrounding each issue to demonstrate the concern, but in reality these types of patterns rarely occur. The key to avoiding most of them is to follow the guideline of not creating mutable value types and following this constraint explains why you don’t encounter them within the built-in value types.

Perhaps the only issue to occur with some frequency is repetitive boxing operations within loops. However, generics greatly reduce boxing, and even without them, performance is rarely affected enough to warrant their avoidance until a particular algorithm with boxing is identified as a bottleneck.

Furthermore, custom-built structs are relatively rare. They obviously play an important role within C# development, but the number of custom-built structs declared by typical developers is usually tiny compared to the number of custom-built classes. Heavy use of custom-built structs is most common in code targeted at interoperating with unmanaged code.


Guidelines

DO NOT define a struct unless it logically represents a single value, consumes 16 bytes or less of storage, is immutable, and is infrequently boxed.


This chapter also introduced enums. Enumerated types are a standard construct available in many programming languages. They help improve both API usability and code readability.

The next chapter presents more guidelines for creating well-formed types—both value types and reference types. It begins by looking at overriding the virtual members of objects and defining operator-overloading methods. These two topics apply to both structs and classes, but they are somewhat more important when completing a struct definition and making it well formed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset