Chapter 9. Creating value types with enumerations and structures

After completing this chapter, you will be able to:

Chapter 8 covers the two fundamental types that exist in Microsoft Visual C#: value types and reference types. Recall that a value type variable holds its value directly on the stack, whereas a reference type variable holds a reference to an object on the heap. Chapter 7 demonstrates how to create your own reference types by defining classes. In this chapter, you’ll learn how to create your own value types.

C# supports two kinds of value types: enumerations and structures. We’ll look at each of them in turn.

Working with enumerations

Suppose that you want to represent the seasons of the year in a program. You could use the integers 0, 1, 2, and 3 to represent spring, summer, fall, and winter, respectively. This system would work, but it’s not very intuitive. If you used the integer value 0 in code, it wouldn’t be obvious that a particular 0 represented spring. It also wouldn’t be a very robust solution. For example, if you declare an int variable named season, there is nothing to stop you from assigning it any legal integer value outside of the set 0, 1, 2, or 3. C# offers a better solution. You can create an enumeration (sometimes called an enum type), whose values are limited to a set of symbolic names.

Declaring an enumeration

You define an enumeration by using the enum keyword, followed by a set of symbols identifying the legal values that the type can have, enclosed between braces. Here’s how to declare an enumeration named Season whose literal values are limited to the symbolic names Spring, Summer, Fall, and Winter:

enum Season { Spring, Summer, Fall, Winter }

Using an enumeration

After you have declared an enumeration, you can use it in exactly the same way as any other type. If the name of your enumeration is Season, you can create variables of type Season, fields of type Season, and parameters of type Season, as shown in this example:

enum Season { Spring, Summer, Fall, Winter }
class Example
{
    public void Method(Season parameter) // method parameter example
    {
        Season localVariable; // local variable example
        ...
    }
    private Season currentSeason; // field example
}

Before you can read the value of an enumeration variable, it must be assigned a value. You can assign a value that is defined by the enumeration only to an enumeration variable, as is illustrated here:

Season colorful = Season.Fall;
Console.WriteLine(colorful);  // writes out 'Fall'

Note

As you can with all value types, you can create a nullable version of an enumeration variable by using the ? modifier. You can then assign the null value, as well the values defined by the enumeration, to the variable:

Season? colorful = null;

Notice that you have to write Season.Fall rather than just Fall. All enumeration literal names are scoped by their enumeration type. This is useful because it makes it possible for different enumerations to coincidentally contain literals with the same name.

Also, notice that when you display an enumeration variable by using Console.WriteLine, the compiler generates code that writes out the name of the literal whose value matches the value of the variable. If needed, you can explicitly convert an enumeration variable to a string that represents its current value by using the built-in ToString method that all enumerations automatically contain, as demonstrated in the following example:

string name = colorful.ToString();
Console.WriteLine(name);      // also writes out 'Fall'

Many of the standard operators that you can use on integer variables you can also use on enumeration variables (except the bitwise and shift operators, which are covered in Chapter 16). For example, you can compare two enumeration variables of the same type for equality by using the equality operator (==), and you can even perform arithmetic on an enumeration variable (although the result might not always be meaningful!).

Choosing enumeration literal values

Internally, an enumeration type associates an integer value with each element of the enumeration. By default, the numbering starts at 0 for the first element and goes up in steps of 1. It’s possible to retrieve the underlying integer value of an enumeration variable. To do this, you must cast it to its underlying type. The discussion in Chapter 8 on unboxing instructs that casting a type converts the data from one type to another, as long as the conversion is valid and meaningful. The following code example writes out the value 2 and not the word Fall (remember, in the Season enumeration Spring is 0, Summer 1, Fall 2, and Winter 3):

enum Season { Spring, Summer, Fall, Winter }
...
Season colorful = Season.Fall;
Console.WriteLine((int)colorful); // writes out '2'

If you prefer, you can associate a specific integer constant (such as 1) with an enumeration literal (such as Spring), as in the following example:

enum Season { Spring = 1, Summer, Fall, Winter }

Important

The integer value with which you initialize an enumeration literal must be a compile-time constant value (such as 1).

If you don’t explicitly give an enumeration literal a constant integer value, the compiler gives it a value that is one greater than the value of the previous enumeration literal, except for the very first enumeration literal, to which the compiler gives the default value 0. In the preceding example, the underlying values of Spring, Summer, Fall, and Winter are now 1, 2, 3, and 4.

You are allowed to give more than one enumeration literal the same underlying value. For example, in the United Kingdom, Fall is referred to as Autumn. You can cater to both cultures as follows:

enum Season { Spring, Summer, Fall, Autumn = Fall, Winter }

Choosing an enumeration’s underlying type

When you declare an enumeration, the enumeration literals are given values of type int. You can also choose to base your enumeration on a different underlying integer type. For example, to declare that Season’s underlying type is a short rather than an int, you can write this:

enum Season : short { Spring, Summer, Fall, Winter }

The main reason for doing this is to save memory; an int occupies more memory than a short, and if you do not need the entire range of values available to an int, using a smaller data type can make sense.

You can base an enumeration on any of the eight integer types: byte, sbyte, short, ushort, int, uint, long, or ulong. The values of all the enumeration literals must fit within the range of the chosen base type. For example, if you base an enumeration on the byte data type, you can have a maximum of 256 literals (starting at 0).

Now that you know how to declare an enumeration, the next step is to use it. In the following exercise, you will work with a console application to declare and use an enumeration that represents the months of the year.

Create and use an enumeration
  1. Start Microsoft Visual Studio 2013 if it is not already running.

  2. Open the StructsAndEnums project, which is located in the Microsoft PressVisual CSharp Step By StepChapter 9Windows XStructsAndEnums folder in your Documents folder.

  3. In the Code and Text Editor window, display the Month.cs file.

    The source file is empty apart from the declaration of a namespace called StructsAndEnums and a // TODO: comment.

  4. Delete the // TODO: comment and add an enumeration named Month for modeling the months of the year within the StructsAndEnums namespace, as shown in bold in the code that follows. The 12 enumeration literals for Month are January through December.

    namespace StructsAndEnums
    {
        enum Month
        {
            January, February, March, April,
            May, June, July, August,
            September, October, November, December
        }
    }
  5. Display the Program.cs file in the Code and Text Editor window.

    As in the exercises in previous chapters, the Main method calls the doWork method and traps any exceptions that occur.

  6. In the Code and Text Editor window, add a statement to the doWork method to declare a variable named first of type Month and initialize it to Month.January. Add another statement to write the value of the first variable to the console.

    The doWork method should look like this:

    static void doWork()
    {
        Month first = Month.January;
        Console.WriteLine(first);
    }

    Note

    When you type the period following Month, Microsoft IntelliSense automatically displays all the values in the Month enumeration.

  7. On the Debug menu, click Start Without Debugging.

    Visual Studio 2013 builds and runs the program. Confirm that the word January is written to the console.

  8. Press Enter to close the program and return to the Visual Studio 2013 programming environment.

  9. Add two more statements to the doWork method to increment the first variable and display its new value to the console, as shown in bold here:

    static void doWork()
    {
        Month first = Month.January;
        Console.WriteLine(first);
        first++;
        Console.WriteLine(first);
    }
  10. On the Debug menu, click Start Without Debugging.

    Visual Studio 2013 builds and runs the program. Confirm that the words January and February are written to the console.

    Notice that performing a mathematical operation (such as the increment operation) on an enumeration variable changes the internal integer value of the variable. When the variable is written to the console, the corresponding enumeration value is displayed.

  11. Press Enter to close the program and return to the Visual Studio 2013 programming environment.

  12. Modify the first statement in the doWork method to initialize the first variable to Month.December, as shown in bold here:

    static void doWork()
    {
        Month first = Month.December;
        Console.WriteLine(first);
        first++;
        Console.WriteLine(first);
    }
  13. On the Debug menu, click Start Without Debugging.

    Visual Studio 2013 builds and runs the program. This time, the word December is written to the console, followed by the number 12.

    A screenshot showing the output generated by the StructsAndEnums application.

    Although you can perform arithmetic on an enumeration, if the results of the operation are outside the range of values defined for the enumerator, all the runtime can do is interpret the value of the variable as the corresponding integer value.

  14. Press Enter to close the program and return to the Visual Studio 2013 programming environment.

Working with structures

Chapter 8 illustrated that classes define reference types that are always created on the heap. In some cases, the class can contain so little data that the overhead of managing the heap becomes disproportionate. In these cases, it is better to define the type as a structure. A structure is a value type. Because structures are stored on the stack, as long as the structure is reasonably small, the memory management overhead is often reduced.

Like a class, a structure can have its own fields, methods, and (with one important exception discussed later in this chapter) constructors.

Declaring a structure

To declare your own structure type, you use the struct keyword followed by the name of the type, followed by the body of the structure, between opening and closing braces. Syntactically, the process is similar to declaring a class. For example, here is a structure named Time that contains three public int fields named hours, minutes, and seconds:

struct Time
{
    public int hours, minutes, seconds;
}

As with classes, making the fields of a structure public is not advisable in most cases; there is no way to control the values held in public fields. For example, anyone could set the value of minutes or seconds to a value greater than 60. A better idea is to make the fields private and provide your structure with constructors and methods to initialize and manipulate these fields, as shown in this example:

struct Time
{
    private int hours, minutes, seconds;
    ...
    public Time(int hh, int mm, int ss)
    {
        this.hours = hh % 24;
        this.minutes = mm % 60;
        this.seconds = ss % 60;
    }
    public int Hours()
    {
        return this.hours;
    }
 }

Note

By default, you cannot use many of the common operators on your own structure types. For example, you cannot use operators such as the equality operator (==) and the inequality operator (!=) on your own structure type variables. However, you can use the built-in Equals() method exposed by all structures to compare them, and you can also explicitly declare and implement operators for your own structure types. The syntax for doing this is covered in Chapter 21.

When you copy a value type variable, you get two copies of the value. In contrast, when you copy a reference type variable, you get two references to the same object. In summary, use structures for small data values for which it’s just as or nearly as efficient to copy the value as it would be to copy an address. Use classes for more complex data that is too big to copy efficiently.

Tip

Use structures to implement simple concepts whose main feature is their value rather than the functionality that they provide.

Understanding structure and class differences

A structure and a class are syntactically similar, but there are a few important differences. Let’s look at some of these variances:

  • You can’t declare a default constructor (a constructor with no parameters) for a structure. The following example would compile if Time were a class, but because Time is a structure, it does not:

    struct Time
    {
        public Time() { ... } // compile-time error
        ...
    }

    The reason you can’t declare your own default constructor for a structure is that the compiler always generates one. In a class, the compiler generates the default constructor only if you don’t write a constructor yourself. The compiler-generated default constructor for a structure always sets the fields to 0, false, or null—just as for a class. Therefore, you should ensure that a structure value created by the default constructor behaves logically and makes sense with these default values. This has some ramifications that you will explore in the next exercise.

    You can initialize fields to different values by providing a nondefault constructor. However, when you do this, your nondefault constructor must explicitly initialize all fields in your structure; the default initialization no longer occurs. If you fail to do this, you’ll get a compile-time error. For example, although the following example would compile and silently initialize seconds to 0 if Time were a class, because Time is a structure, it fails to compile:

    struct Time
    {
        private int hours, minutes, seconds;
        ...
        public Time(int hh, int mm)
        {
            this.hours = hh;
            this.minutes = mm;
        }   // compile-time error: seconds not initialized
    }
  • In a class, you can initialize instance fields at their point of declaration. In a structure, you cannot. The following example would compile if Time were a class, but because Time is a structure, it causes a compile-time error:

    struct Time
    {
        private int hours = 0; // compile-time error
        private int minutes;
        private int seconds;
        ...
    }

The following table summarizes the main differences between a structure and a class.

Question

Structure

Class

Is this a value type or a reference type?

A structure is a value type.

A class is a reference type.

Do instances live on the stack or the heap?

Structure instances are called values and live on the stack.

Class instances are called objects and live on the heap.

Can you declare a default constructor?

No.

Yes.

If you declare your own constructor, will the compiler still generate the default constructor?

Yes.

No.

If you don’t initialize a field in your own constructor, will the compiler automatically initialize it for you?

No.

Yes.

Are you allowed to initialize instance fields at their point of declaration?

No.

Yes.

There are other differences between classes and structures concerning inheritance. These differences are covered in Chapter 12.

Declaring structure variables

After you have defined a structure type, you can use it in exactly the same way as any other type. For example, if you have defined the Time structure, you can create variables, fields, and parameters of type Time, as shown in this example:

struct Time
{
    private int hours, minutes, seconds;
    ...
}
class Example
{
    private Time currentTime;
    public void Method(Time parameter)
    {
        Time localVariable;
        ...
    }
}

Note

As with enumerations, you can create a nullable version of a structure variable by using the ? modifier. You can then assign the null value to the variable:

Time? currentTime = null;

Understanding structure initialization

Earlier in this chapter, you saw how you can initialize the fields in a structure by using a constructor. If you call a constructor, the various rules described earlier guarantee that all the fields in the structure will be initialized:

Time now = new Time();

The following graphic depicts the state of the fields in this structure:

A diagram showing how the fields in the Time structure are initialized by using a constructor.

However, because structures are value types, you can also create structure variables without calling a constructor, as shown in the following example:

Time now;

This time, the variable is created but its fields are left in their uninitialized state. The following graphic depicts the state of the fields in the now variable. Any attempt to access the values in these fields will result in a compiler error:

A diagram showing how the fields in the Time structure are left uninitialized if the constructor is not used.

Note that in both cases, the Time variable is created on the stack.

If you’ve written your own structure constructor, you can also use that to initialize a structure variable. As explained earlier in this chapter, a structure constructor must always explicitly initialize all its fields. For example:

struct Time
{
    private int hours, minutes, seconds;
    ...
    public Time(int hh, int mm)
    {
        hours = hh;
        minutes = mm;
        seconds = 0;
    }
}

The following example initializes now by calling a user-defined constructor:

Time now = new Time(12, 30);

The following graphic shows the effect of this example:

A diagram showing how the fields of the Time structure are initialized after calling a user-defined constructor.

It’s time to put this knowledge into practice. In the following exercise, you will create and use a structure to represent a date.

Create and use a structure type
  1. In the StructsAndEnums project, display the Date.cs file in the Code and Text Editor window.

  2. Add a structure named Date inside the StructsAndEnums namespace.

    This structure should contain three private fields: one named year of type int, one named month of type Month (using the enumeration you created in the preceding exercise), and one named day of type int. The Date structure should look exactly as follows:

    struct Date
    {
        private int year;
        private Month month;
        private int day;
    }

    Consider the default constructor that the compiler will generate for Date. This constructor sets the year to 0, the month to 0 (the value of January), and the day to 0. The year value 0 is not valid (because there was no year 0), and the day value 0 is also not valid (because each month starts on day 1). One way to fix this problem is to translate the year and day values by implementing the Date structure so that when the year field holds the value Y, this value represents the year Y + 1900 (or you can pick a different century if you prefer), and when the day field holds the value D, this value represents the day D + 1. The default constructor will then set the three fields to values that represent the date 1 January 1900.

    If you could override the default constructor and write your own, this would not be an issue, because you could then initialize the year and day fields directly to valid values. You cannot do this, though, and so you have to implement the logic in your structure to translate the compiler-generated default values into meaningful values for your problem domain.

    However, although you cannot override the default constructor, it is still good practice to define nondefault constructors to allow a user to explicitly initialize the fields in a structure to meaningful nondefault values.

  3. Add a public constructor to the Date structure. This constructor should take three parameters: an int named ccyy for the year, a Month named mm for the month, and an int named dd for the day. Use these three parameters to initialize the corresponding fields. A year field with the value Y represents the year Y + 1900, so you need to initialize the year field to the value ccyy – 1900. A day field with the value D represents the day D + 1, so you need to initialize the day field to the value dd – 1.

    The Date structure should now look like this (with the constructor shown in bold):

    struct Date
    {
        private int year;
        private Month month;
        private int day;
        public Date(int ccyy, Month mm, int dd)
        {
            this.year = ccyy - 1900;
            this.month = mm;
            this.day = dd - 1;
        }
    }
  4. Add a public method named ToString to the Date structure after the constructor. This method takes no arguments and returns a string representation of the date. Remember, the value of the year field represents year + 1900, and the value of the day field represents day + 1.

    Note

    The ToString method is a little different from the methods you have seen so far. Every type, including structures and classes that you define, automatically has a ToString method whether or not you want it. Its default behavior is to convert the data in a variable to a string representation of that data. Sometimes, the default behavior is meaningful; other times, it is less so. For example, the default behavior of the ToString method generated for the Date class simply generates the string “StructsAndEnums.Date”. To quote Zaphod Beeblebrox in The Restaurant at the End of the Universe by Douglas Adams (Pan Macmillan, 1980), this is “shrewd, but dull.” You need to define a new version of this method that overrides the default behavior by using the override keyword. Overriding methods are discussed in more detail in Chapter 12.

    The ToString method should look like this:

    struct Date
    {
        ...
        public override string ToString()
        {
            string data = String.Format("{0} {1} {2}", this.month, this.day + 1,
                                                       this.year + 1900);
            return data;
        }
    }

    The Format method of the String class makes it possible for you to format data. It operates in a similar manner to the Console.WriteLine method, except that rather than displaying data to the console, it returns the formatted result as a string. In this example, the positional parameters are replaced with the text representations of the values of the month field, the expression this.day + 1, and the expression this.year + 1900. The ToString method returns the formatted string as its result.

  5. Display the Program.cs file in the Code and Text Editor window.

  6. In the doWork method, comment out the existing four statements.

  7. Add statements to the doWork method that declare a local variable named defaultDate, and initialize it to a Date value constructed by using the default Date constructor. Add another statement to doWork to display the defaultDate variable on the console by calling Console.WriteLine.

    Note

    The Console.WriteLine method automatically calls the ToString method of its argument to format the argument as a string.

    The doWork method should now look like this:

    static void doWork()
    {
        ...
        Date defaultDate = new Date();
        Console.WriteLine(defaultDate);
    }

    Note

    When you type the new keyword, IntelliSense automatically detects that there are two constructors available for the Date type.

  8. On the Debug menu, click Start Without Debugging to build and run the program. Verify that the date January 1 1900 is written to the console.

  9. Press the Enter key to return to the Visual Studio 2013 programming environment.

  10. In the Code and Text Editor window, return to the doWork method, and add two more statements. In the first statement, declare a local variable named weddingAnniversary and initialize it to July 4 2013. (I actually did get married on Independence Day, although it was many years ago.) In the second statement, write the value of weddingAnniversary to the console.

    The doWork method should now look like this:

    static void doWork()
    {
        ...
        Date weddingAnniversary = new Date(2013, Month.July, 4);
        Console.WriteLine(weddingAnniversary);
    }
  11. On the Debug menu, click Start Without Debugging, and then confirm that the date July 4 2013 is written to the console below the previous information.

  12. Press Enter to close the program and return to Visual Studio 2013.

Copying structure variables

You’re allowed to initialize or assign one structure variable to another structure variable, but only if the structure variable on the right side is completely initialized (that is, if all its fields are populated with valid data rather than undefined values). The following example compiles because now is fully initialized. The graphic shows the results of performing such an assignment (this image was created on March 19, 2013).

Date now = new Date();
Date copy = now;
A diagram showing how the fields of a structure are copied when one structure variable is assigned to another.

The following example fails to compile because now is not initialized:

Date now;
Date copy = now; // compile-time error: now has not been assigned

When you copy a structure variable, each field on the left side is set directly from the corresponding field on the right side. This copying is done as a fast, single operation that copies the contents of the entire structure and that never throws an exception. Compare this behavior with the equivalent action if Time were a class, in which case both variables (now and copy) would end up referencing the same object on the heap.

Note

If you are a C++ programmer, you should note that this copy behavior cannot be customized.

In the final exercise in this chapter, you will contrast the copy behavior of a structure with that of a class.

Compare the behavior of a structure and a class
  1. In the StructsAndEnums project, display the Date.cs file in the Code and Text Editor window.

  2. Add the following method to the Date structure. This method advances the date in the structure by one month. If, after advancing the month, the value of the month field has moved beyond December, this code resets the month to January and advances the value of the year field by 1.

    struct Date
    {
        ...
        public void AdvanceMonth()
        {
            this.month++;
            if (this.month == Month.December + 1)
            {
                this.month = Month.January;
                this.year++;
            }
        }
    }
  3. Display the Program.cs file in the Code and Text Editor window.

  4. In the doWork method, comment out the first two uncommented statements that create and display the value of the defaultDate variable.

  5. Add the following code shown in bold to the end of the doWork method. This code creates a copy of the weddingAnniversary variable called weddingAnniversaryCopy and prints out the value of this new variable.

    static void doWork()
    {
        ...
        Date weddingAnniversaryCopy = weddingAnniversary;
        Console.WriteLine("Value of copy is {0}", weddingAnniversaryCopy);
    }
  6. Add the following statements shown in bold to the end of the doWork method. These statements call the AdvanceMonth method of the weddingAnniversary variable and then display the value of the weddingAnniversary and weddingAnniversaryCopy variables:

    static void doWork()
    {
        ...
        weddingAnniversary.AdvanceMonth();
        Console.WriteLine("New value of weddingAnniversary is {0}", weddingAnniversary);
        Console.WriteLine("Value of copy is still {0}", weddingAnniversaryCopy);
    }
  7. On the Debug menu, click Start Without Debugging to build and run the application. Verify that the console window displays the following messages:

    July 4 2013
    Value of copy is July 4 2013
    New value of weddingAnniversary is August 4 2013
    Value of copy is still July 4 2013

    The first message displays the initial value of the weddingAnniversary variable (July 4 2013). The second message displays the value of the weddingAnniversaryCopy variable. You can see that it contains the same date held in the weddingAnniversary variable (July 4 2013). The third message displays the value of the weddingAnniversary variable after changing the month to August (August 4 2013). The final statement displays the value of the weddingAnniversaryCopy variable. Notice that it has not changed from its original value of July 4 2013.

    If Date were a class, creating a copy would reference the same object in memory as the original instance. Changing the month in the original instance would therefore also change the date referenced through the copy. You will verify this assertion in the following steps.

  8. Press Enter and return to Visual Studio 2013.

  9. Display the Date.cs file in the Code and Text Editor window.

  10. Change the Date structure into a class, as shown in bold in the following code example:

    class Date
    {
        ...
    }
  11. On the Debug menu, click Start Without Debugging to build and run the application again. Verify that the console window displays the following messages:

    July 4 2013
    Value of copy is July 4 2013
    New value of weddingAnniversary is August 4 2013
    Value of copy is still August 4 2013

    The first three messages are the same as before. However, the fourth message shows that the value of the weddingAnniversaryCopy variable has changed to August 4 2013.

  12. Press Enter and return to Visual Studio 2013.

Summary

In this chapter, you saw how to create and use enumerations and structures. You learned some of the similarities and differences between a structure and a class, and you saw how to define constructors to initialize the fields in a structure. You also saw how to represent a structure as a string by overriding the ToString method.

  • If you want to continue to the next chapter, keep Visual Studio 2013 running, and turn to Chapter 10.

  • If you want to exit Visual Studio 2013 now, on the File menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Quick reference

To

Do this

Declare an enumeration

Write the keyword enum, followed by the name of the type, followed by a pair of braces containing a comma-separated list of the enumeration literal names. For example:

enum Season { Spring, Summer, Fall, Winter }

Declare an enumeration variable

Write the name of the enumeration on the left followed by the name of the variable, followed by a semicolon. For example:

Season currentSeason;

Assign an enumeration variable to a value

Write the name of the enumeration literal in combination with the name of the enumeration to which it belongs. For example:

currentSeason = Spring;        // error
currentSeason = Season.Spring; // correct

Declare a structure type

Write the keyword struct, followed by the name of the structure type, followed by the body of the structure (the constructors, methods, and fields). For example:

struct Time
{
    public Time(int hh, int mm, int ss)
    { ... }
    ...
    private int hours, minutes, seconds;
}

Declare a structure variable

Write the name of the structure type, followed by the name of the variable, followed by a semicolon. For example:

Time now;

Initialize a structure variable to a value

Initialize the variable to a structure value created by calling the structure constructor. For example:

Time lunch = new Time(12, 30, 0);
..................Content has been hidden....................

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