Value Types


There are three main kinds of .NET value types. In this section, we'll discuss each of these in some depth:

  • Primitive types
    All programming languages define primitive types such as integers, floating-point numbers, and so on. In .NET, such types are value types. We'll discuss the primitive types in C#, and see how these types map to Microsoft Intermediate Language (MSIL) data types.

  • User-defined value types
    We can define our own value types to represent simple objects or small pieces of data in our application. In C#, structures are user-defined value types, and are defined using the struct keyword. The .NET Framework defines custom value types, such as System.DateTime and System.Drawing.Point, in a similar manner.

  • Enumerations
    An enumeration is a special kind of value type, which represents a type that has a small list of allowable values. An enumeration might specify the values Yes, No, and Maybe for example. Underneath, each of these values is normally represented by an integer, but defining an enumeration type allows us to assign meanings to a specific set of integral values.

    Unlike Java, both C++ and Visual Basic already support enumerations. The big difference with C# is that enumerations in the .NET world are strongly typed. For example, we might have a HairColor enumeration that allows Blonde, Red, Brown, and Black, and an EyeColor enumeration that can be Blue, Green, or Brown. This allows us to write readable code, while still ensuring that we can't accidentally give someone blue hair and red eyes.

Primitive Types

C# defines fifteen primitive types to represent integral numbers, floating-point numbers, Boolean values, and characters. Eleven of these primitive types are defined by the CLS to be interoperable with any other CLS-compliant programming language. The remaining four types are not CLS-compliant, so can only be used in private sections of a C# application or in any public interfaces where language interoperability is not required.

Each of these primitive types is actually just a synonym for a standard type in the .NET Framework's System namespace. There is no effective difference between a value type we define ourselves, and one of these special primitive types. However, these types do benefit from some special support in the C# language:

  • Literal syntax: primitive values can all be created using a literal syntax. For example, when we write float pi = 3.142f; we are using a literal to specify a floating-point value. We could use 3.142d to indicate a double, or any of a range of suffixes to identify other numeric types. Similar notations exist for other primitive types, like true and false for Boolean literals.

  • Operator support: primitive types can be combined using special operators. So we can use an addition operator (+) to add two numerical values, or the ‘&’ or ‘|’ operators to combine Booleans. In C#, it is also possible to define operators for our own types. We'll cover this in depth in Chapter 4.

The following table shows the mapping between C# primitive types and the equivalent structures in the System namespace. The table also shows how the C# .NET compiler translates these types into Microsoft Intermediate Language (MSIL) data types during compilation. Non-CLS-compliant types are marked with an asterisk:

Primitive Type

Equivalent .NET Structure

Equivalent MSIL Data Type

Description

bool

System.Boolean

bool

True/False value

byte

System.Byte

unsigned int8

8-bit unsigned integer

char

System.Char

char

Unicode 16-bit character

decimal

System.Decimal

System.Decimal

128-bit decimal value

double

System.Double

float64

IEEE 64-bit float

float

System.Single

float32

IEEE 32-bit float

int

System.Int32

int32

32-bit signed integer

long

System.Int64

int64

64-bit signed integer

object

System.Object

object

Base type of all types

sbyte*

System.SByte

int8

8-bit signed integer

short

System.Int16

int16

16-bit signed integer

string

System.String

string

Unicode string

uint*

System.Uint32

unsigned int32

32-bit unsigned integer

ulong*

System.Uint64

unsigned int64

64-bit unsigned integer

ushort*

System.Uint16

unsigned int16

16-bit unsigned integer

Notice that C# actually lists string and object as primitive types, although both of these are reference types, not value types. As object is the root of the whole .NET class hierarchy we'll discuss System.Object further in Chapter 2. However, while most programming languages include some form of string as a primitive type, the .NET Framework takes a slightly different approach. For now, we'll use this section to look at those primitives implemented as value types, and we'll discuss strings when we look at reference types a little later.

C and C++ developers should be aware that the descriptions given in the table for the C# primitive types will always be consistent within the .NET Framework. In particular, in C#, an int is always 32 bits. In C/C++ the size of an int is platform dependent– although this is commonly overlooked. Similarly, in C#, a long is 64 bits, where in C++ long represents "an integral type that is larger than or equal to the size of type int". These definitions obviously apply right across all of the .NET languages and this leaves a little less scope for error when operating in a mixed-language environment.

Visual Basic Note:

Numeric types in C# include both signed and unsigned versions, which are not available in VB. Be careful when mixing these types, especially in comparisons. Also, C# does not automatically convert between numeric types in expressions, so you need to take care when rounding is important. For example, float f = 1 / 3 will return zero, while float f = 1.0f / 3.0f will return 0.33333 as expected.

As all types in .NET are objects, we can even invoke methods on literals. This may seem strange and you'd probably never want to do it, but if we consider a line of code like string s = 32.ToString();, compile and run it, it should help fix in your mind the "everything is an object" message.

The following simple console application illustrates the use of primitive types in C#:

    using System;    class MyClass    {      static void Main()      {        int i = 100;     // use a primitive C# type        Int32 j = i;     // use the equivalent .NET Framework type 

We can use primitive C# data types (such as int) interchangeably with the equivalent .NET Framework types (such as System.Int32). For the sake of simplicity and familiarity, you should stick to one set of declarations in your code.

Note

Microsoft is keenly advocating mixed-language programming using any combination of .NET Framework languages. If you are developing a multi-language solution, you might prefer to use the .NET Framework structure types explicitly, to emphasize the commonality across these languages. For example, short in C# is the same as Short in Visual Basic .NET and short in Managed Extensions for C++; the equivalent .NET Framework type is System.Int16 in all languages.

        Console.WriteLine("int:      {0}", typeof(int).FullName);        Console.WriteLine("Int32:    {0}", typeof(Int32).FullName); 

We use the typeof operator to obtain information about data types at run time and write this information to the console. The following code asks the user for a numerator and a denominator and then calculates the quotient.

        Console.Write("\nEnter a double: ");        string input = Console.ReadLine();        double num = Double.Parse(input);        Console.Write("Enter another double: ");        input = Console.ReadLine();        double denom = Double.Parse(input);        double res = num / denom;        if (Double.IsNaN(res))          Console.WriteLine("Not a Number.");        else if (Double.IsPositiveInfinity(res))          Console.WriteLine("Positive infinity.");        else if (Double.IsNegativeInfinity(res))          Console.WriteLine("Negative infinity.");        else          Console.WriteLine("Result is {0}.", res);      }    } 

We use various methods defined in the Double type to read and process double values in our application. The Parse method extracts a double value from a string; IsNaN() tests for "is not a number"; IsPositiveInfinity() tests for positive infinity (for example, dividing 100 by 0); and IsNegativeInfinity() tests for negative infinity (for example, dividing -100 by 0).

When the application runs, it displays the types for int and Int32 as System.Int32; this confirms that the int type in C# .NET is just another name for System.Int32. The application also asks us to enter two floating-point numbers; if we enter some arbitrary numbers, we can see the output on the console.

Save the code into a file called primitive_types.cs, and compile it. Now enter the following at the command prompt:

      C:\Class Design\Ch01> primitive_types      int:     System.Int32      Int32:   System.Int32      Enter a double: 432.33      Enter another double: 4576.33      Result is 0.0944708969851387. 

Viewing the Output from the Compiler

The .NET Framework SDK includes several useful tools for examining files generated when we build a project. One of the most important tools is the MSIL Disassembler; ildasm.exe. This tool enables us to see how the compiler has translated our C# source code into MSIL byte code. It also enables us to view detailed metadata for our types, which can help us understand how the Common Language Runtime works. This in turn can help us use C# more effectively. We'll look in detail at metadata in Chapter 8.

Some developers dismiss the MSIL Disassembler as being irrelevant and over-hyped, but that's not the case. We'll be using the MSIL Disassembler extensively in this book, to investigate how the C# .NET compiler has compiled our code.

To run the MSIL Disassembler tool, open a command prompt (if you are using Visual Studio .NET, make sure you start a Visual Studio .NET command prompt), then move to the folder that contains the executable file, and run ildasm as follows:

      > ildasm assembly-filename 

The name and location of the executable file depends on how we built the application:

  • If we built the application using Visual Studio .NET, the executable file will have the same name as the project – although with an .exe or .dll extension – and will be located in the bin\Debug or bin\Release sub-folder. Also, Visual Studio .NET adds a namespace, which is the same as the project name.

  • If we built the application using the command-line C# compiler, the executable file will have the same name as the source file, again with an .exe or .dll extension, and will be located in the same folder as the source file.

For example, if we built the primitive_types application using the command-line compiler, we could load up primitive_types.exe into the MSIL Disassembler. When you expand the MyClass icon, the MSIL Disassembler window displays the following information:

click to expand

Double-click the Main icon, to open a view of the MSIL code for the Main method:

click to expand

In the MSIL code, the Main() method is marked with the MSIL managed keyword. This indicates code that runs in the managed environment provided by the .NET Framework Common Language Runtime. All code we write in C# will be managed code. A variety of local variables can be found described with MSIL data types such as float64, int32, and string.

Let's look at the end of the IL code for the Main() method:

click to expand

Each line of IL code consists of a command, followed by any data the command needs to operate on. Data that the program is working with is stored on the stack. Items are loaded onto the stack using IL commands that begin ld for load. Each variable on the stack takes up a fixed amount of memory defined by its type. For reference type objects, the stack contains a reference to the location on the managed heap where the actual object is stored. The first line in the screenshot loads a reference to a string onto the stack. The next loads the contents of the variable V_5 (which contains the result of the division operation) onto the stack. When an item is placed on the stack, it goes on top of any previous stack items. When items are taken off the stack, the top item is removed first.

We'll ignore the box command for a moment, and instead look at the call command. This call tells .NET to call a method, called WriteLine(), belonging to a class called System.Console, found in the mscorlib.dll assembly, which takes as arguments a string and an object. .NET looks up this method, takes the two items from the top of the stack and passes them to the method being called. The top item on the stack is our floating-point value, which is a result of the division we performed. This is not an object, it's a value type.

We'll look at boxing in depth later as there are some important performance considerations. For now, we just need to know that the box instruction in the IL code takes the item on the top of the stack, copies it to the managed heap, and places on the top of the stack a reference to the boxed value. This allows us to treat the value as an object and pass it in to this method call. So, when the call to the method comes, the items on the top of the stack are a boxed value and then a string, and these two values are passed to the Console.WriteLine method.

Note

Close the MSIL Disassembler windows when you have finished. If you forget to close the MSIL Disassembler windows, the EXE file will remain locked by the MSIL Disassembler. If you try to recompile the application with these windows open, you'll get a compiler error because the EXE file cannot be overwritten.

User-Defined Value Types (Structures)

Applications often require types to encapsulate essentially numeric quantities such as currencies, screen coordinates, and temperatures, which are not represented by the available primitive types. Using classes in these scenarios would be like using a hammer to crack a nut; the run-time overhead for garbage-collecting these simple objects would be unnecessarily high.

The .NET Framework provides user-definable value types as a solution to this problem. In C#, a value type is written as a struct. Remember that like value types, instances of structs are stored wherever they are used.

Value types are messy because they leave copies of themselves everywhere. However, they are very easy to clean up. .NET doesn't need to keep track of each copy of the value – if we need the value elsewhere, we'll just send a copy there. Thus if a value type is no longer reachable, the memory it was taking up is immediately available for use. With reference types though, we need the garbage collector to sweep up behind us. All the copies of a reference might have gone out of scope or been overwritten, but the memory on the managed heap is still being taken up with the referenced object. We'll see how the garbage collector handles that problem later on.

Because of the way value instances are passed around, value types should ideally be small. If we define large value types, inefficiencies start to creep in when we pass the value instances between methods in our application because of the amount of data that has to be copied into and out of the method. Large value types will slow down the allocation, management, and cleanup of objects and stack frames that use them.

In this section, we'll see how to define and use value types effectively in C# and how to use inheritance with value types.

Defining and Using Value Types

The rules for defining value types are essentially the same as for defining a class. For example, a value type can have fields, properties, constants, events, methods, and constructors. As we'll see in Chapters 3 and 4, we can also override methods and operators and provide indexers just as we can in classes.

Value types, or structures, are cut-down classes, although there are some important differences:

  • Structures must have at least one field or event declaration.

  • Structures automatically have a default parameterless constructor. The compiler will give an error if you try to define your own. This constructor performs default initialization for any fields you've declared. It initializes numeric fields to zero, Boolean flags to false, and sets object references to null.

  • Although we cannot define our own parameterless constructor, we can (and should) provide parameterized constructors. Feel free to provide several parameterized constructors where appropriate, so that users of your value type can initialize their objects in a variety of useful ways.

  • We cannot provide initializers for fields in a structure; we must perform initialization in a constructor (this is different from a class, where we can initialize fields at the point of declaration). However, we are allowed to provide initializers for const fields; that is, we can initialize const fields at the point of definition. When we look at consts in the next chapter, the reason for this will become clear.

  • Structure objects have a much simpler deallocation mechanism than class objects. The garbage collector disposes of the class object, and then it calls the object's destructor just before the object disappears. Structure objects are deallocated when they go out of scope, or are overwritten with another value, rather than being garbage-collected. Therefore, it is not permitted to define a destructor, although we can define a Dispose method in the same way as we might for a class. However, since we can't guarantee it will be called, as we can in classes, it's best to avoid building value types that require their resources to be disposed. We'll see an example of this later, although object disposal is discussed in a little more depth in Chapter 5.

  • Many examples show structures with public fields. This seems to contradict a basic rule of object-oriented development: "don't declare data public". The tradeoff is one of speed versus encapsulation; public data is (marginally) faster to access because it avoids the overhead of method calls to get at the data, but it clearly breaks the encapsulation of the structure. If in doubt, err on the side of caution and declare all your fields as private. If private fields are exposed as properties, the accessor code is usually inlined by the compiler, resulting in an efficient implementation anyway. Don't optimize your code at the expense of maintainability; let the compiler optimize it for you.

  • The .NET Framework does not allow us to inherit from a structure. Therefore, the methods defined in a structure cannot be overridden by methods in a subclass. One logical consequence of this is that it is not permitted to declare any member of a structure as virtual, abstract, or sealed. However, the compiler can predict with certainty which methods will be invoked when we use structure objects in our code. This insight enables the compiler to optimize the method invocation for efficiency; for example, the compiler can choose to expand the method body inline rather than executing a traditional method call. The net result is that method calls on structure objects can be less expensive than method calls on class objects.

  • As value types, structures are always allocated where they are used. It doesn't make any difference whether you use the new operator when defining a variable containing a struct, or not. With reference types, if you declare a variable but do not use the new operator, the runtime will not allocate any storage on the managed heap, but will allocate a reference on the stack containing the value null. With value types, the runtime will allocate the space on the stack and call the default constructor to initialize the state of the object.

The following example, value_types.cs, illustrates some of these rules:

    using System;    struct Money    {      // private instance field      private int centsAmount;      // private class field      private const string currencySymbol = "$";      // public constructor      public Money(int dollars, int cents)      {        centsAmount = (dollars * 100) + cents;      }      // another public constructor      public Money(double amount)      {        centsAmount = (int)((amount * 100.0) + 0.5);      }    }    class MyClass    {      static void Main()      {        Money freebie;        Money salary = new Money(20000, 0);        Money carPrice = new Money(34999.95);      }    } 

Note the following in this example:

  • The fields in the structure are declared as private, to maximize encapsulation.

  • The currencySymbol field is initialized at the point of declaration. This is allowable because currencySymbol is a const field.

  • There are two constructors in the structure, to initialize structures in two different ways. Note the rounding adjustment needed in the second constructor when we cast from double to int.

  • The Main() method in the separate class MyClass creates three structure objects, to show how to call the available constructors (including the compiler-generated parameterless constructor).

At the moment, this program doesn't provide any evidence that it's working. We'll add some more functionality to it in the next section.

Using Inheritance with Value Types

When we define a structure in C#, we cannot explicitly specify a base class. All value types implicitly inherit from System.ValueType, which is a standard type in the .NET Framework library. System.ValueType inherits from System.Object, and overrides some of the methods from System.Object (System.ValueType does not introduce any additional methods).

When we define our own value types, we can override some of the methods inherited from System.ValueType or System.Object. One of the most commonly overridden methods is ToString(), which returns a string representation of the object. For more information about System.Object, see Chapter 2.

Structures cannot be used as base classes for other classes to inherit; they are not extensible through inheritance. Structures can be very sophisticated types but they are not classes. This language restriction enables the compiler to minimize the amount of administrative code it has to generate to support structures.

Although structures cannot explicitly inherit from an arbitrary class, they can implement interfaces. For example, it is quite common to implement standard .NET Framework interfaces such as IComparable, which allows us to specify how objects should be compared, and so enables sorting. Value types will often implement this to interoperate well with other classes in the .NET Framework.

We'll be covering interfaces in more detail later in this chapter, but the following preview demonstrates how easily we can change our Money value type to implement an interface, and override the ToString() method inherited from System.Object:

      // value_type_inheritance.cs      using System;      struct Money : IComparable      {        // private fields        private int centsAmount;        private const string currencySymbol = "$";        // public constructors        public Money(int dollars, int cents)        {          centsAmount = (dollars * 100) + cents;        }        public Money(double amount)        {          centsAmount = (int)((amount * 100.0) + 0.5);        }        // compare with another Money        public int CompareTo(object other)        {          Money m2 = (Money)other;          if (centsAmount < m2.centsAmount)            return -1;          else if (centsAmount == m2.centsAmount)            return 0;          else            return 1;        }      // return value as a string      public override string ToString()      {        return currencySymbol + (centsAmount / 100.0).ToString();      }    } 

The Money structure now implements the IComparable interface. The CompareTo() method, as specified by the IComparable interface, compares the value of this Money instance against another Money instance, and returns an integer to indicate the result of the comparison.

Money also overrides the ToString() method, which is defined in the System.Object base class. This returns a string representation of this Money instance. Let's see what effect these changes have:

    class MyClass    {      static void Main()      {        // create an array of 5 items        Money[] salaries = new Money[5];        salaries[0] = new Money(9.50);        salaries[1] = new Money(4.80);        salaries[2] = new Money(8.70);        salaries[3] = salaries[2];        salaries[4] = new Money(6.30);        // display unsorted array        Console.WriteLine("Unsorted array:");        foreach (Money salary in salaries)        {          Console.WriteLine("{0}", salary);        }        // sort the array        Array.Sort(salaries);        // display sorted array        Console.WriteLine("Sorted array:");        foreach (Money salary in salaries)        {          Console.WriteLine("{0}", salary);        }      }    } 

In the above example, the Main() method creates an array of Money instances, and when array element 3 is assigned the value of element 2, it obtains a copy of the value of element 2. Each Money instance in the array is displayed using Console.WriteLine, which implicitly invokes our overridden ToString() method. The Array.Sort() method sorts the array. For this to work, the array elements must implement the IComparable interface. Array.Sort() calls the CompareTo() method repeatedly on the array elements, to sort them in the specified order. Finally, the sorted array is displayed on the console.

The application displays the following output:

    C:\Class Design\Ch01> valuetype_inheritance    Unsorted array:    $9.5    $4.8    $8.7    $8.7    $6.3    Sorted array:    $4.8    $6.3    $8.7    $8.7    $9.5 

Enumerations

Enumerations are .NET value types that represent integral types with a limited set of permitted values. They may also be used to map bit flags onto an integer type to allow a convenient way to represent a combination of options using a single variable. Enumerations are present in many programming languages, but in .NET they are also object-oriented. This means that developers now have access to additional features, which are not present in other languages.

Enumerated Types

To declare an enumerated type, we use the enum keyword and specify symbolic names to represent the allowable values. We can also specify an underlying integral data type to be used for the enumeration (byte, short, int, or long), and optionally assign a specific number to each of the names.

The following example, enumerations.cs, declares a simple enumeration to represent medals in a competition:

    // enumerations.cs    using System;    enum Medal : short    {      Gold,      Silver,      Bronze    } 

Enumerations inherit implicitly from System.Enum, and so inherit all of its members. Having defined an enumerated type, we can use it in our code as follows:

    class MyClass    {      static void Main()      {        Medal myMedal = Medal.Bronze;        Console.WriteLine("My medal: " + myMedal.ToString());      }    } 

We have created an enumeration instance named myMedal and assigned it the value Medal.Bronze. The scope of enumeration members is limited to the enumeration declaration body.

If you compile and run this code, you will see that the output written to the console contains the symbolic name Bronze instead of the equivalent numeric value. This is one advantage of .NET enumerations over traditional C++ or VB style equivalents. This facility is also used by the Visual Studio .NET IDE during development and debugging, and it presents a much more meaningful picture to a developer.

As well as the ubiquitous ToString method, we can use the static Format() method defined in System.Enum to convert a numeric value to a symbolic name without needing an instance of the enumerated type. For example, the following statement would produce the same output as the previous example:

    Console.WriteLine("My medal: " + Enum.Format(typeof(Medal), 2, "G")); 

As System.Enum implements the IFormattable interface, we can use format options in combination with other .NET classes to access details of our enumerated type. For example, we can create an array containing one element for each symbolic name and display the numeric values together with the symbolic names:

          Medal[] medals = (Medal[])Enum.GetValues(typeof(Medal));          foreach (Medal m in medals)          {            Console.WriteLine("{0:D}\t{1:G}", m, m);          } 

Replacing the body of the Main() method in the earlier example with the above code produces the following output:

    C:\Class Design\Ch01> enumerations2    0       Gold    1       Silver    2       Bronze 

If we need to convert the other way, perhaps using a symbolic name entered by a user at run time to initialize an enumerated type, we can use the Parse() method. Replace the Main() method with the following code:

         Console.Write("\nEnter a medal: ");         string input = Console.ReadLine();         Medal myMedal = (Medal)Enum.Parse(typeof(Medal), input, true);         Console.WriteLine("You entered: " + myMedal.ToString()); 

The Boolean parameter in the Parse() method controls case-sensitivity. In this example, case is not checked. We accept input of a matching symbolic name and echo this to the user:

    C:\Class Design\Ch01> enumerations3    Enter a medal: gold    You entered: Gold 

Note that entering a string that does not match any of the symbolic names defined will produce an ArgumentException, which may be trapped at run time using a trycatch block. Alternatively you might choose to use the IsDefined() method from System.Enum to validate user input before proceeding.

Bit Flags

Many developers are already familiar with bit flags. In essence, you start with a numeric type, say a byte consisting of 8 bits, and use a bit-mask (another binary number) together with some combination of logical operators to set, unset, or query individual bits in the variable. In this way you can carry around a combination of up to eight on/off settings in a single byte, which is a compact use of storage.

In C# we can define an enumerated type to do the same thing, which makes it much easier to read. We do this by defining an enumeration using the [Flags] attribute, and associating each symbolic name with a particular combination of bits. For example, the following code defines a single-byte enumerated type with four individual options:

    using System;    [Flags]    enum Permit : byte    {      Create = 0x01,      Read = 0x02,      Update = 0x04,      Delete = 0x08    } 

The C# compiler allows us to use [Flags] or [FlagsAttribute]; they are synonymous. We can show how to use this enumerated type in the following code:

    class TestBitFlag    {      static void Main()      {        Permit perm1 = Permit.Create;        Console.WriteLine(perm1.ToString());        Permit perm = Permit.Create | Permit.Read;        Console.WriteLine(perm.ToString());        perm |= Permit.Delete;        Console.WriteLine(perm.ToString());      }    } 

Notice how we can set individual bits in the variable by using the bitwise OR operator (|). We're also using the ToString() method to produce a readable representation of the variable. When we compile and run this code, it produces the following output:

    C:\Class Design\Ch01> bitflags    Create    Create, Read    Create, Read, Delete 

We can see how the System.Enum type produces different output once we identify our enumerated type with the [Flags] attribute. As before, we could use the Parse() method, this time with a comma-delimited set of options, in order to initialize a variable from user input.

The symbolic names we define don't have to map to individual bits. We can introduce values that represent a combination of bits and the functionality provided by System.Enum remains consistent. For example, redefine the enumerated type as follows:

    [Flags]    enum Permit : byte    {      Create = 0x01,      Read = 0x02,      Update = 0x04,      Delete = 0x08,      AllExceptUpdate = 0x0B    } 

Now, when you compile and run the example, should get the following:

    C:\Class Design\Ch01> bitflags2    Create    Create, Read    AllExceptUpdate 

You can see that the [Flags] attribute affects how the ToString(), Parse(), and Format() methods work within System.Enum and that the results still make sense. If the combination of bits set in the enumerated type matches one of the symbolic names exactly, then that name is output; if not then a combination of symbolic names is output in a comma-delimited list. We need to be a little wary though because it is perfectly legal to specify more than one symbolic name with the same value. In this situation, System.Enum will use one of these names in any converted output although which one it chooses will be indeterminate.

Inside an Enumerated Type

To sum up, there are two good reasons for using enumerations in your code:

  • Enumerations use meaningful names to represent allowable values. These names make it easier for other developers to understand the purpose of these values.

  • Enumerations are strongly typed. You cannot assign integer values to an enumeration instance unless you use an explicit cast; otherwise you must use one of the enumeration member names. This helps to avoid programming errors that might arise if you used raw integer values.

Now, C++ and Visual Basic developers will already be familiar with enums, even though the equivalent constructs in those languages are not strongly typed. Certainly, if you are new to C#, you will have noticed how enumerations are much more developer-friendly during debugging. The reason for this is because a .NET enumeration is a value type, which inherits directly from System.Enum. You don't see this relationship in the source code but it's obvious if you look at the MSIL with the IL disassembler. For example, here's the first sample application we coded at the beginning of this section:

click to expand

Java doesn't have an enumeration construct. Java developers commonly use a class as the type, and various static instances of the class as the variable. If we observe the ILDasm output above we can see that an enumeration in C# isn't really a new idea, it's just a shorthand method for creating a specialized value type that provides certain limited but well defined behaviors. Unlike other value types though, it is not permissible to define methods, properties, or events within an enumeration.

There's nothing at all wrong with this except that there are cases where enumerations are not as appropriate as a custom-made value type. You can imagine that, with such a type, we could significantly enhance the facilities provided by an enum. We could add methods to an enum, and even have different enum members exhibiting different behaviors.

The point here is to think carefully about your design, especially if you are coding long switch statements that are based on values in an enumeration.

One final caveat with .NET enumerations: it's possible to cast any value of the underlying integral type to the type of the enumeration – whether it represents a legitimate value of the enumeration or not.

For this reason, you should never assume that an enumeration value passed into a method could only possibly be one of the allowed values. Always perform validity checking on enumeration values.




C# Class Design Handbook(c) Coding Effective Classes
C# Class Design Handbook: Coding Effective Classes
ISBN: 1590592573
EAN: 2147483647
Year: N/A
Pages: 90

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net