9. 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 all 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.

The value types have three properties called: structs, boxing, and enums. The structs featuring initializing structs and inheritance and interfaces with value types. Enums features converting between enumerations and strings and enumerations as flags.

Structs

All of the C# built-in types, such as bool and int, are value types, except for string and object, which are reference types. Numerous additional value types are provided within the framework. Developers can also define their own value types.

To define a custom value type, you use a syntax similar to the syntax 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 9.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 9.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 implemented 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 as 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 9.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 9.3). Use of C# 6.0 provides for a noticeable code reduction when it comes to defining immutable types.

Begin 7.2

Starting with C# 7.2, you can verify that, in fact, you have successfully defined a struct that is read-only by declaring it as such:

readonly struct Angle {}

Now the compiler will verify that the entire struct is immutable, reporting an error if there is a field that is not read-only or a property that has a setter.

End 7.2
Begin 8.0

If you need finer-grained control than declaring the entire class as read-only, C# 8.0 allows you to define any struct member as read-only (including methods and even getters—which potentially may modify an object’s state even though they shouldn’t). For example, in Listing 9.1, the Move() method can include a readonly modifier:

readonly public Angle Move(int degrees, int minutes, int seconds) { ... }

(Doing so is allowable but redundant when the class is read-only.)

Any read-only members that modify a struct’s data (properties or fields), or that invoke a non-read-only member, will report a compile-time error. By supporting the concept of read-only access to members, developers declare the behavioral intent of whether a member can modify the object instance. Note that properties that are not automatically implemented can use the readonly modifier on either the getter or the setter (although the latter would be strange). To decorate both, the readonly modifier would be placed on the property itself, rather than on the getter and setter individually.

End 8.0

NOTE

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 9.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 in one value type variable can be observed to cause a mutation in another, as it would with a reference type.

Remarkably, the tuple (System.ValueTuple) is one example that breaks the immutable guideline. To understand why it is the exception, see https://IntelliTect.com/WhyTupleBreaksTheImmutableRules.

Initializing Structs

In addition to properties and fields, structs may contain methods and constructors. However, no default constructor may be defined for a struct. Instead, 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 9.2, for example, would not compile if the line _Degrees = 42 was uncommented.

Listing 9.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 9.3, the constructor that initializes the property (if uncommented out) rather than the field produces a compile error.

Listing 9.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; // Shorthand for this.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—that is, accessing the Degrees property is implicitly equivalent to this.Degrees. To resolve this issue, you need to initialize the fields directly, as demonstrated in the constructor of Listing 9.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.

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 these methods in a struct are almost the same as those for classes (see Chapter 10). 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 10 for more details.

Boxing

We know that variables of value types directly contain their data, whereas variables of reference types 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 must 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 a value type that directly refers to 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 the 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 called unboxing. The unboxing conversion first 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 Common Intermediate Language (CIL). Each operation has specific instructions, as shown in Table 9.1.

Table 9.1: Boxing Code in CIL

C# Code

CIL Code

static void Main()
{



    int number;
    object thing;

    number = 42;

    // Boxing
    thing = number;

    // Unboxing
    number = (int)thing;



return;
}
.method private hidebysig
    static void  Main() cil managed
{
  .entrypoint
  // Code size       21 (0x15)
  .maxstack  1
  .locals init ([0] int32 number,
           [1] object thing)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   42
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  box        [mscorlib]System.Int32
  IL_000a:  stloc.1
  IL_000b:  ldloc.1
  IL_000c:  unbox.any  [mscorlib]System.Int32
  IL_0011:  stloc.0
  IL_0012:  br.s       IL_0014
  IL_0014:  ret
} // end of method Program::Main

 

 

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 9.4 and Output 9.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 9.4: Subtle Box and Unbox Instructions

class DisplayFibonacci
{
  static void Main()
  {

      int totalCount;
      // Intentionally using ArrayList to demonstrate boxing
      System.Collections.ArrayList list =
          new System.Collections.ArrayList();

      Console.Write("Enter a number between 2 and 1000:");
      totalCount = int.Parse(Console.ReadLine());
      if (totalCount == 7)  // Magic number used for testing
      {
          // Execution-time error:
          // Triggers exception when retrieving value as double.
          // list.Add(0);  // Cast to double or 'D' suffix required.
                        // Whether cast or using 'D' suffix,
                        // CIL is identical.
      }
      else
      {
          list.Add((double)0);
        }

      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]! );
      }

      // Using a foreach to clarify the box/unbox operations rather than
      // Console.WriteLine(string.Join(", ", list.ToArray()));
      foreach (double count in list)
      {
          Console.Write("{0}, ", count);
      }
  }
}

Output 9.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 9.4, when compiled, produces five box instructions 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 ArrayList1 method is int Add(object value). As such, any value type passed to this method is boxed.

    1. It is important that we use a collection of type object, not a strongly typed collection like a generic collection, as discussed in Chapter 12

  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 12). 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: When calling Add() without first casting to a double (or using a double literal), you could 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 when retrieving the value from within the foreach loop would fail. The problem is that the unbox operation is immediately followed by 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 9.5 shows a similar error commented out and followed by the correct cast.

Listing 9.5: 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 9.8.

Listing 9.8: 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 shown in Listing 9.9.

Listing 9.9: 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’s 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 ConnectionState.ConnectionStateConnected and similar references. 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. Alternatively, you can assign explicit values to enums, as shown in Listing 9.10.

Listing 9.10: 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 9.10 uses a short. For consistency, the syntax for enums emulates the syntax of inheritance, but it doesn’t actually create 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 more members.

2. See the discussion in the section “Enums as Flags” later in this chapter.

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 an enumerated type variable 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 enum values (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.

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 9.11.

Listing 9.11: 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 from int[] to uint[].) However, you should use this approach cautiously, because no C# specification requires 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 9.3 to the trace buffer.

Output 9.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 9.12 provides an example of how to do it without generics (see Chapter 12), and Output 9.4 shows the results.

Listing 9.12: Converting a String to an Enum Using Enum.Parse()

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

Output 9.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 18).

Until Microsoft .NET Framework 4, no TryParse() method was available, so code written to target prior versions needs to include appropriate exception handling if the string potentially might not correspond to an enum value identifier. Microsoft .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 9.13.

Listing 9.13: 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 be converted 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).

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 9.14, indicates various attributes of 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 be, 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 9.14: 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 flag, use the Enum.HasFlags() method (which was added with Microsoft .NET Framework 4.0) or use the bitwise AND operator. Both cases are illustrated in Listing 9.15.

Listing 9.15: Using Bitwise OR and AND with Flag Enums3

3. Note that the FileAttributes.Hidden value does not work on Linux.

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($"{file.Attributes} = {(int)file.Attributes}");

      // Added in C# 4.0/Microsoft .NET Framework 4.0
      if (!(
          System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
              OSPlatform.Linux) ||
          System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
              OSPlatform.OSX)))
      {
           if (!file.Attributes.HasFlag(FileAttributes.Hidden))
           {
                throw new Exception("File is not hidden.");
           }
      }
      // Use bit operators prior to C# 4.0/.NET 4.0
      if (( file.Attributes & FileAttributes.ReadOnly) !=
          FileAttributes.ReadOnly)
      {
          throw new Exception("File is not read-only.");
      }

      // ...
}

The results of Listing 9.15 appear in Output 9.5.

Output 9.5

Hidden | ReadOnly = 3

Using the bitwise OR operator allows you to set the file attributes to both read-only and hidden.

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 9.16 shows an example.

Listing 9.16: 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.

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 intended to interoperate with unmanaged code.

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.

Chapter 10 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
18.217.144.32