Creating Enumerated Types and Structures - C# Fundamentals - Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Part I: C# Fundamentals

Hour 6. Creating Enumerated Types and Structures


What You’ll Learn in This Hour

Enumerated types

Working with structures


In the last few hours, you have been introduced to the basics of how types in C# work, including the predefined types and nullable types, and how classes in C# provide language syntax for object-oriented programming. You also saw how classes enable you to create your own types to represent real-world data. There are still two classifications of value types in C# that we have not talked about: the enumerated type and the structure.

Although classes are the primary mechanism you use to create new types, they do not provide a way to create types that are restricted in value nor do they enable you to create new value types. There are already many predefined enumerated types and structures in C#, but you have the ability to create your own. You have actually already used structures without knowing it because every value type is a structure.

In this hour, you explore enumerated types, looking at what they are and why they are useful. Next, you discover the benefits (and drawbacks) to structures, including how to create your own structures. You are also introduced to some of the structures included in the base class library and see how to use them.

Enumerated Types

An enumerated type, also called an enumeration (or just an enum for short), is simply a way to create a numeric type restricted to a predetermined set of valid values with meaningful names for those values. Although this might sound simple, enums are actually powerful. By defining a set of valid values, enumerations easily enable you to represent real-world concepts and information in such a way that the compiler understands the underlying values, whereas the programmer understands the higher-level meaning. This enables code that is self-describing and unambiguous.

Many things in the real world conceptually represent enumerations: the days of the week, months of the year, seasons, oceans, and compass directions, to name a few. Let’s use the days of the week as an example.

You could represent the days of the week in your code using the integer values 0 through 6 to represent Sunday through Saturday, respectively. Unfortunately, those integer values don’t convey much meaning to someone who looks at your code for the first time, or even to you later on when you need to maintain that code. It is also ambiguous because those integer values now have multiple meanings, and it is not always clear if you mean the integer value 0 or Sunday when you see it in code.

Using an integer to represent the days of the week also allows you to assign any valid integer value, not just the ones you expect. Enumerations solve these problems by enabling you to define the set of valid values and give them symbolic names. Think of enumerations as defining a discrete set of constants that are available only through a “container” name.

To define an enumeration, you must use the enum keyword followed by an identifier. You then define the set of legal values inside the body of the enumeration, each separated by a comma (,). Keep in mind that the identifiers used for the named values must follow the same rules defined for variable identifiers. The days of the week enum would look like what is shown in Listing 6.1.


Note: Enum Values

The comma after the last value is optional, but it’s a good idea to include it so that it is easier to add values to the enum at a later time.


Listing 6.1. A Simple Enumeration


public enum Days
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}


Whenever you need to refer to a day of the week, you can refer to it by name using the enumeration name and value. For instance, to refer to Wednesday, you would use Days.Wednesday and not just Wednesday.


Note: Multiple Named Values

You can also have more than one named value represent the same numeric value. This is useful in situations where multiple names could represent the same concept. To do this, you simply add that name as a new enumeration value and set it equal to the named value it represents, as shown here:

public enum Days
{
Sunday,
Monday,
Tuesday,
Wednesday,
HumpDay = Wednesday,
Thursday,
Friday,
Saturday,
}


Remember, enumerations are a form of named constants restricted to numeric values only, so it makes sense that each of the named values defined corresponds to a numeric value. In Listing 6.1, those numeric values were not defined. By default, when you define an enumeration, the compiler assigns the first value of the enumeration the integer value 0. The remaining values get a sequentially increasing number from the previous value. The Days enumeration could have been written and is equivalent to the code shown in Listing 6.2.

Listing 6.2. A Simple Enumeration Specifying Values


public enum Days
{
Sunday = 0,
Monday = 1,
Tuesday = 2,
Wednesday = Sunday + 3,
Thursday = Sunday + 4,
Friday = 5,
Saturday = 6,
}


Enumerations also support most of the standard operators that you can use on integer values, although not all of them are actually meaningful. The most common operations you perform with enumerations are equality and inequality tests. Because enumerations are value types, you can declare a nullable enumeration as well.


Note: The Zero Value

Generally it is best to always provide a zero value named None. If that’s not appropriate for the context of the enumeration, the most common default value should be assigned the zero value.



Tip: Underlying Enumeration Types

All the values contained in an enumeration must be of the same data type, called the underlying type. By default, the underlying type for enumerations is int, but any of the predefined integer types can be used: byte, short, int, long, sbyte, ushort, uint, or ulong.

To give your enumeration a different underlying type, you specify it after the identifier, like this:

enum Days : byte
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}

Keep in mind, however, that the underlying type determines the overall range of valid values you can choose from when defining the enumeration. In this case, the Days enumeration can have only values from 0 to 255.



Try It Yourself: Working with Enumerations

To implement the Days enumeration and explore how to use it, follow these steps. Keep Visual Studio open at the end of this exercise because you will use this application later.

1. Create a new console application.

2. Add a new code file named Days.cs, and implement the Days enumeration.

3. In the Main method of the Program.cs file, declare a local variable of type Days named days and assign an initial value of Days.Sunday.

4. Enter a Console.WriteLine statement that displays the enumerated value and integer value of the days variable. To get the integer value, you need to cast the variable to an int.

5. Run the application by pressing Ctrl+F5, and observe that the output matches what is shown in Figure 6.1.

Image

Figure 6.1. Displaying a named enumerated value.

6. Now set days to Days.Saturday + 1.

7. Run the application again by pressing Ctrl+F5, and observe that the output matches what is shown in Figure 6.2.

Image

Figure 6.2. Displaying an enumerated value that is not in the enumerated type.

You should notice that both the enumerated value and the numeric value are 7. This is because there is no named value that represents the numeric value 7, so the runtime has no choice but to treat it directly as a number. This is a good reason why you should avoid performing arithmetical operations on enumerations.


Understanding Flags Enumerated Types

So far you have looked at enumerations that represent discrete values, but that isn’t always the case in the real world. Taking the days of the week example a bit further, you might decide that the users of your application need to specify any combination of the days of the week.

Enumerations also provide the capability to combine values in flags enumerations. When using a flags enumeration, you can create new combined values by using the logical OR operation.

Taking the Days enumeration, you can turn it into a flags enumeration, as shown in Listing 6.3.

Listing 6.3. A Flags Enumeration


[Flags]
public enum Days
{
None = 0,
Sunday = 0x001,
Monday = 0x002,
Tuesday = 0x004,
Wednesday = 0x008,
Thursday = 0x010,
Friday = 0x020,
Saturday = 0x040,
}


To allow the values of a flags enumeration to be combined, all the values must be powers of two. This is necessary because when multiple values are combined, there must still be a way to identify which discrete values make up that combination. As a result, when defining a flags enumeration, you must always specify the values.


Note: The Flags Attribute

Another difference between a regular enum and a flags enum is the use of the Flags attribute, which specifies additional metadata about the enumeration.

The Flags attribute also changes the string representation of the enumeration value (from the ToString method) when used with a value made by combining other values.

Although you are not required to use the Flags attribute, it is strongly recommended because it provides a clear indication of intent, not just to the compiler but also to other programmers.


Unlike simple enumerations in which the zero value can be either None or the most common default, flags enumerations should always name the zero value None, and it should always mean that all flags are not set.


Try It Yourself: Working with Flags Enumerations

By following these steps, you modify the Days enumeration to be a flags enumeration and add some of the commonly used combinations. If you closed Visual Studio, repeat the previous exercise first.

1. Modify the Days enumeration to the one shown in Listing 6.3.

2. Add the following combinations:

a. Weekend = Sunday ¦ Saturday
b. Workdays = Monday ¦ Tuesday ¦ Wednesday ¦ Thursday ¦ Friday

3. Set the value of the days variable in the Main method to the combination:

Days.Saturday ¦ Days.Sunday

4. Enter the following statements to determine if the days variable has a specific flag set:

Console.WriteLine(days.HasFlag(Days.Saturday));
Console.WriteLine(days.HasFlag(Days.Friday));

5. Run the application by pressing Ctrl+F5, and observe that the output matches what is shown in Figure 6.3.

Image

Figure 6.3. Result of using HasFlag and a named enumerated value.

6. Set the value of the days variable to the combination:

Days.Weekend ¦ Days.Friday

7. Run the application by pressing Ctrl+F5, and observe that the output matches what is shown in Figure 6.4.

Image

Figure 6.4. Result of using HasFlag and an unnamed enumerated value.


Working with Structures

Structures, also called structs, are intended to be lightweight alternatives to classes when you need a simple user-defined type. Structures are similar to classes and can contain all the same members as a class but are value types rather than reference types. Structures are different, however, from classes in the following ways:

• Structures don’t support inheritance. Structures implicitly inherit from System.ValueType (which, in turn, inherits from System.Object). Structures can inherit from interfaces, just as classes can.

• Structures are implicitly sealed, which means you cannot inherit from a structure.

• A structure cannot have a destructor or declare a default constructor and initialize instance fields. If a structure provides any constructors, all the fields must be assigned in that constructor call.


Note: Structures Included in the Base Class Library

All the primitive data types except for string and object are implemented as structures. The .NET Framework provides more than 200 public structures. Some of the commonly used structures follow:

• System.DateTime

• System.DateTimeOffset

• System.Guid

• System.TimeSpan

• System.Drawing.Color

• System.Drawing.Point

• System.Drawing.Rectangle

• System.Drawing.Size


In C#, structures are declared in the same way as classes, except the struct keyword is used in place of the class keyword.

Defining Struct Methods

Just as classes can define methods, structs can as well. These methods can be either static or instance methods; although, it is more common for structs to contain static public methods and private instance methods.

Operator Overloading

Because structs are user-defined value types, you cannot use most of the common operators, such as the equality operator, on variables defined as one of your own structs. This is a significant limitation, but fortunately, C# provides a way to resolve this through the concept of operator overloading.

If you think of operators simply as specially named methods, operator overloading is simply a special form of method overloading. To declare an overloaded operator, you define a public static method whose identifier is the keyword operator and the actual operator symbol you are declaring. In addition, at least one parameter of the operator you are overloading must be the same as the containing type. The overloadable operators are defined in Table 6.1.

Table 6.1. Overloadable Operators

Image


Caution: Language Interoperability

Not all .NET languages support operator overloading, so if you create types that you want to use from other languages, they should be CLS-compliant and should provide alternatives that correspond to any overloaded operators defined.


The compound operators are conspicuously missing from this list, but if you recall that these operators perform both an arithmetic action and an assignment together, by overloading the appropriate arithmetic operator, you allow the corresponding compound assignment operator to use your new overloaded operator.

Typically, operators should always be overloaded in symmetrical groups. For example, if you overload the equality operator, you should also overload the inequality operator. The only exceptions to this symmetrical overloading guideline are the one’s complement operator (~) and the not operator (!). The logical symmetrical groups that should be followed for operator overloading are shown in Table 6.2.

Table 6.2. Symmetric Operator Overload Groups

Image


Note: Operator Overloading in Classes

Classes also support operator overloading and all of the same “rules” apply. Operators should be overloaded in symmetrical groups and, if your class is CLS-compliant, you should provide alternatives to the overloaded operators as well.


Conversion Operators

In much the same manner as user-defined structs enable you to overload operators to support common operations on your data, you can also influence the casting and conversion process by creating overloaded conversion operators. Again, if you think of conversion and casting as specially named functions, conversion overloading is also a special form of method overloading.


Caution: Implicit or Explicit Conversion

Implicit conversions are widening conversions because nothing from the original value is lost because of the conversion. Explicit conversions are narrowing conversions because there is the possibility that data from the original value can be lost because of the conversion.

When defining your own conversion operators, you should keep these behaviors in mind. If the conversion you define has the possibility of losing data as a result, it should be defined as an explicit conversion. If the conversion is safe, meaning there is no possibility of losing data, it should be defined as an implicit conversion.


You have already seen how the built-in numeric types have both implicit conversions, which require no special syntax, and explicit conversions, which do. You can overload these implicit and explicit conversions for your own types by declaring your own conversion operators, which follow similar rules to declaring operator overloads.

To declare a conversion operator, you define a public static method whose identifier is the keyword operator and whose return type is the type to which you are converting. The type you convert from is the single parameter to the conversion operator.

If you want to declare an implicit conversion, use the keyword implicit before the operator keyword; otherwise, use the keyword explicit to declare an explicit conversion. Conversion operators are sometimes used with operator overloading to reduce the number of operator overloads you must define.


Try It Yourself: Operator Overloading in Structs

By following these steps, you implement a custom struct to represent degrees in Celsius and Fahrenheit:

1. Create a new console application.

2. Add a new code file named Celsius.cs, which defines a struct that looks like this:

struct Celsius
{
private float degrees;

public float Degrees
{
get
{
return this.degrees;
}
}

public Celsius(float temperature)
{
this.degrees = temperature;
}

public static Celsius operator +(Celsius x, Celsius y)
{
return new Celsius(x.Degrees + y.Degrees);
}

public static implicit operator float(Celsius c)
{
return c.Degrees;
}
}

3. Add another code file named Fahrenheit.cs that defines a struct similar to Celsius but is named Fahrenheit.

4. In both structs, define an operator overload for the - operator. Follow the same pattern as the + operator overload.

5. In both structs, define an implicit conversion operator that converts from float to the appropriate struct type. Follow the same pattern as the implicit conversion operator that converts from the struct type to float.

6. Next, define an explicit conversion operator in each struct that converts from one to the other. Use the following formulas for the conversion:

• Celsius to Fahrenheit: (9.0f / 5.0f) * c.Degrees + 32

• Fahrenheit to Celsius: (5.0f / 9.0f) * (f.Degrees - 32)

7. In the Main method of Program.cs, enter the following:

Fahrenheit f = new Fahrenheit(100.0f);
Console.WriteLine("{0} fahrenheit = {1} celsius", f.Degrees, (Celsius)f);

Celsius c = 32f;
Console.WriteLine("{0} celsius = {1} fahrenheit", c.Degrees, (Fahrenheit)c);

Fahrenheit f2 = f + (Fahrenheit)c;
Console.WriteLine("{0} + {1} = {2} fahrenheit", f.Degrees, (Fahrenheit)c,
f2.Degrees);

Fahrenheit f3 = 100f;
Console.WriteLine("{0} fahrenheit", f3.Degrees);

8. Run the application using Ctrl+F5, and observe that the output matches what is shown in Figure 6.5.

Image

Figure 6.5. Results of working with a custom struct.

Clearly, this is not useful because some of the values display using the type name.

9. Add an override for the ToString() method that returns the result of calling this.Degrees.ToString().

10. Run the application using Ctrl+F5 and observe that the output matches what is shown in Figure 6.6.

Image

Figure 6.6. Results after overriding the ToString method.


Construction and Initialization

Just as classes must be given an initial state, so must structs. For classes, this must always occur through a class constructor; however, because structs are value types, you can create a struct variable without calling a constructor. For example, we could have created a new NumberStructvariable like this:

NumberStruct ns1;

This creates the new variable but leaves the fields in an uninitialized state. The result is that if you try to access a struct field, you receive a compiler error. By calling the constructor, as you see in Listing 6.4, you guarantee that the fields will be initialized.

Another aspect of struct initialization is that you are not allowed to assign one struct variable to another if the source (the one on the right side of the assignment) is not fully initialized. This means that the following is legal:

NumberStruct ns1 = new NumberStruct();
NumberStruct ns2 = ns1;

However, the following is not:

NumberStruct ns1;
NumberStruct ns2 = ns1;


Caution: Custom Default Constructors

Unlike classes, structs cannot have a custom default constructor, and you cannot initialize fields in a struct outside of a constructor. As a result, when a struct is created, all the fields are initially set to a zero value.

You can provide overloaded constructors and make use of constructor chaining. However, when you provide overloaded constructors, all the fields must be initialized by your constructor either explicitly or through a chained constructor.

The interesting thing to note is that you can still chain in the default constructor if the zero value is acceptable for the other fields.


Listing 6.4 shows the similarities and differences between structs and classes.

Listing 6.4. Comparing Structs and Classes


struct NumberStruct
{
public int Value;
}

class NumberClass
{
public int Value = 0;
}

class Test
{
static void Main()
{
NumberStruct ns1 = new NumberStruct ();
NumberStruct ns2 = ns1;
ns2.Value = 42;

NumberClass nc1 = new NumberClass ();
NumberClass nc2 = nc1;
nc2.Value = 42;

Console.WriteLine("Struct: {0}, {1}", ns1.Value, ns2.Value);
Console.WriteLine("Class: {0}, {1}", nc1.Value, nc2.Value);
}
}


Because both ns1 and ns2 are of the NumberStruct value type, they each have their own storage location, so the assignment of ns2.Number does not affect the value of ns1.Number. However, because nc1 and nc2 are both reference types, the assignment of nc2.Number does affect the value ofnc1.Number because they both contain the same reference.


Caution: Properties or Public Fields

There is some debate concerning structs and properties. Some feel that properties should always be used, even in simple types like structs, whereas others feel it is acceptable for structs to simply make their fields public.

Although using public fields is easier, it allows your value type to be mutable, which is usually not desirable. When defining your own structs, remember that they are value types and, just as strings are immutable, they should be immutable as well. To do this, you should provide constructors allowing the private fields to be set and read-only properties for retrieving the values.


The output produced is shown in Figure 6.7.

Image

Figure 6.7. The differences between a class and a struct.

Summary

Enumerated types and structures complete a significant portion of your programming foundation. Now that you know about classes, structures, and enumerated types, you have all the tools to create your own specialized data types for your business applications. Enumerated types enable you to define a set of discrete named values, whereas structures enable you to define your own lightweight value types.

Q&A

Q. Why are enumerated types useful?

A. An enumerated type enables you to define a discrete set of numeric values that can be given easily understood names (identifiers). This enables your code to be more self-documenting but also helps to ensure correctness.

Q. Can enumerated type values be combined to identify values not present in the original enumerated type?

A. This can be accomplished using a flags enumeration, in which all explicit values must be unique powers of two. Values can be combined using the bitwise or operator to form new values that are not present in the original enumeration.

Q. Are structures value or reference types?

A. Even though structures can be thought of as lightweight classes, they are actually value types.

Workshop

Quiz

1. What are the possible underlying types of an enumerated type?

2. What is the default type for an enumeration if one is not explicitly specified?

3. What does providing the Flags attribute do?

4. Do structures support inheritance?

5. Can you overload operators for a structure?

6. Can structures have a custom default constructor?

Answers

1. Enumerations can be based only on the following types: byte, short, int, long, sbyte, ushort, uint, or ulong.

2. If you don’t explicitly specify the type for an enumeration, it is based on the int type.

3. By providing the Flags attribute on an enumeration, you clearly communicate that it is permissible for the values of the enumeration to be combined to form new values. It also changes the runtime behavior of the ToString method to display the constituent values when called on a combined value.

4. Structures do not support class inheritance, but they do support interface implementation inheritance.

5. Yes, structures support operator overloading.

6. No, structs do not enable a custom default constructor. You can provide additional overloaded constructors, but all fields must be initialized when the constructor is finished.

Exercises

1. Add a public enum named ColorRepresentation to the Exif project folder. This enum should have the following named values:

Uncalibrated
sRGB

2. Add the following code to the ExifMetadata struct found in the Exif project folder.

public ColorRepresentation ColorRepresentation
{
get
{
ColorRepresentation value = Exif.ColorRepresentation.Uncalibrated;
if (this.colorRepresentation.HasValue)
{
if (!String.IsNullOrWhiteSpace(Enum.GetName(typeof(ColorRepresentation),
this.colorRepresentation)))
{
value = (ColorRepresentation)this.colorRepresentation;
}
}

return value;
}
}

3. Add a private field to the Photo class named exifMetadata and whose data type is PhotoViewer.Exif.ExifMetadata. Add a read-only property named ExifMetadata to the interface and provide a class implementation that returns the exifMetadata field.