Complex Variable Types


So far you've looked at all the simple variable types that C# has to offer. There are three slightly more complex (but very useful) sorts of variable that you will look at here:

  • Enumerations

  • Structures

  • Arrays

Enumerations

Each of the types you've seen so far (with the exception of string) has a clearly defined set of allowed values. Admittedly, this set is so large in types such as double that it can practically be considered a continuum, but it is a fixed set nevertheless. The simplest example of this is the bool type, which can only take one of two values: true or false.

There are many other situations in which you might want to have a variable that can take one of a fixed set of results. For example, you might want to have an orientation type that can store one of the values north, south, east, or west.

In situations like this, enumerations can be very useful. Enumerations do exactly what you want in this orientation type: they allow the definition of a type that can take one of a finite set of values that you supply.

What you need to do, then, is create your own enumeration type called orientation that can take one of the four possible values shown above.

Note that there is an additional step involved here — you don't just declare a variable of a given type, you declare and detail a user-defined type and then you declare a variable of this new type.

Defining Enumerations

Enumerations can be defined using the enum keyword as follows:

 enum typeName { value1, value2, value3, ... valueN } 

Next, you can declare variables of this new type with

 typeName varName; 

and assign values using:

 varName = typeName.value; 

Enumerations have an underlying type used for storage. Each of the values that an enumeration type can take is stored as a value of this underlying type, which by default is int. You can specify a different underlying type by adding the type to the enumeration declaration:

 enum typeName : underlyingType { value1, value2, value3, ... valueN }

Enumerations can have underlying types of byte, sbyte, short, ushort, int, uint, long, and ulong.

By default, each value is assigned a corresponding underlying type value automatically according to the order in which it is defined, starting from zero. This means that value1 will get the value 0, value2 will get 1, value3 will get 2, and so on. You can override this assignment by using the = operator and specifying actual values for each enumeration value:

enum typeName : underlyingType { value1 = actualVal1, value2 = actualVal2, value3 = actualVal3, ... valueN = actualValN }

In addition, you can specify identical values for multiple enumeration values by using one value as the underlying value of another:

enum typeName : underlyingType { value1 = actualVal1, value2 = value1, value3,    ... valueN = actualValN }

Any values left unassigned will be given an underlying value automatically, where the values used are in a sequence starting from 1 greater than the last explicitly declared one. In the preceding code, for example, value3 will get the value value1 + 1.

Note that this can cause problems, with values specified after a definition such as value2 = value1 being identical to other values. For example, in the following code value4 will have the same value as value2:

enum typeName : underlyingType { value1 = actualVal1, value2, value3 = value1, value4,    ... valueN = actualValN }

Of course, if this is the behavior you want then this code is fine.

Note also that assigning values in a circular fashion will cause an error, for example:

enum typeName : underlyingType { value1 = value2, value2 = value1 } 

The following Try It Out is an example of all of this. The code defines an enumeration called orientation and then demonstrates its use.

Try It Out – Using an Enumeration

image from book
  1. Create a new console application called Ch05Ex02 in the directory C:\BegVCSharp\Chapter5.

  2. Add the following code to Program.cs:

    namespace Ch05Ex02 { enum orientation : byte { north = 1, south = 2, east  = 3, west  = 4 }        class Program    {       static void Main(string[] args)       { orientation myDirection = orientation.north; Console.WriteLine("myDirection = {0}", myDirection); Console.ReadKey();       }    } }

  3. Execute the application. You should see the output shown in Figure 5-6.

    image from book
    Figure 5-6

  4. Quit the application and modify the code as follows:

     byte directionByte; string directionString; orientation myDirection = orientation.north; Console.WriteLine("myDirection = {0}", myDirection); directionByte = (byte)myDirection; directionString = Convert.ToString(myDirection); Console.WriteLine("byte equivalent = {0}", directionByte); Console.WriteLine("string equivalent = {0}", directionString); Console.ReadKey();

  5. Execute the application again. The output is shown in Figure 5-7.

    image from book
    Figure 5-7

How It Works

This code defines and uses an enumeration type called orientation. The first thing to notice is that the type definition code is placed in your namespace, Ch05Ex02, but not in the same place as the rest of your code. This is because definitions are not executed as such; that is, at runtime you don't step through the code in a definition as you do the lines of code in your application. Application execution starts in the place you're used to and has access to your new type because it belongs to the same namespace.

The first iteration of the example demonstrates the basic method of creating a variable of your new type, assigning it a value and outputting it to the screen.

Next, you modified your code to show the conversion of enumeration values into other types. Note that you must use explicit conversions here. Even though the underlying type of orientation is byte, you still have to use the (byte) cast to convert the value of myDirection into a byte type:

directionByte = (byte)myDirection;

The same explicit casting is necessary in the other direction too; if you want to convert a byte into an orientation. For example, you could use the following code to convert a byte variable called myByte into an orientation and assign this value to myDirection:

 myDirection = (orientation)myByte; 

Of course, care must be taken here because not all permissible values of byte type variables map to defined orientation values. the orientation type can store other byte values, so you won't get an error straight away, but this may break logic later in the application.

To get the string value of an enumeration value you can use Convert.ToString():

directionString = Convert.ToString(myDirection);

Using a (string) cast won't work, because the processing required is more complicated than just placing the data stored in the enumeration variable into a string variable.

Alternatively, you can use the ToString() command of the variable itself. The following code gives you the same result as using Convert.ToString():

 directionString = myDirection.ToString(); 

Converting a string to an enumeration value is also possible, except that here the syntax required is slightly more complex. A special command exists for this sort of conversion, Enum.Parse(), which is used in the following way:

 (enumerationType)Enum.Parse(typeof(enumerationType), enumerationValueString); 

This uses another operator, typeof, which obtains the type of its operand. You could use this for your orientation type as follows:

 string myString = "north"; orientation myDirection = (orientation)Enum.Parse(typeof(orientation),                                                    myString); 

Of course, not all string values will map to an orientation value! If you pass in a value that doesn't map to one of your enumeration values, you will get an error. Like everything else in C#, these values are case-sensitive, so you still get an error if your string agrees with a value in everything but case (for example, if myString is set to North rather than north).

image from book

Structs

The next sort of variable that you will look at is the struct (short for structure). Structs are just that — data structures are composed of several pieces of data, possibly of different types. They allow you to define your own types of variables based on this structure. For example, suppose that you want to store the route to a location from a starting point, where the route consists of a direction and a distance in miles. For simplicity you can assume that the direction is one of the compass points (such that it can be represented using the orientation enumeration from the last section) and that distance in miles can be represented as a double type.

Now, you could use two separate variables for this using code you've seen already:

 orientation myDirection; double      myDistance; 

There is nothing wrong with using two variables like this, but it is far simpler (especially where multiple routes are required) to store this information in one place.

Defining Structs

Structs are defined using the struct keyword as follows:

 struct <typeName> {    <memberDeclarations> } 

The <memberDeclarations> section contains declarations of variables (called the data members of the struct) in almost the same format as usual. Each member declaration takes the form:

 <accessibility> <type> <name>; 

To allow the code that calls the struct to access the struct's data members, you use the keyword public for <accessibility>. For example:

 struct route { public orientation direction; public double      distance; } 

Once you have a struct type defined, you use it by defining variables of the new type

 route myRoute; 

and have access to the data members of this composite variable via the period character:

 myRoute.direction = orientation.north; myRoute.distance = 2.5; 

This is illustrated in the following Try It Out, where the orientation enumeration from the last Try It Out is used with the route struct shown previously. This struct is then manipulated in code to give you a feel for how structs work.

Try It Out – Using a Struct

image from book
  1. Create a new console application called Ch05Ex03 in the directory C:\BegVCSharp\Chapter5.

  2. Add the following code to Program.cs:

    namespace Ch05Ex03 { enum orientation : byte { north = 1, south = 2, east  = 3, west  = 4 } struct route { public orientation direction; public double      distance; }        class Program    {       static void Main(string[] args)       { route myRoute; int myDirection = -1; double myDistance; Console.WriteLine("1) North\n2) South\n3) East\n4) West"); do { Console.WriteLine("Select a direction:"); myDirection = Convert.ToInt32(Console.ReadLine()); } while ((myDirection < 1) || (myDirection > 4)); Console.WriteLine("Input a distance:"); myDistance = Convert.ToDouble(Console.ReadLine()); myRoute.direction = (orientation)myDirection; myRoute.distance = myDistance; Console.WriteLine("myRoute specifies a direction of {0} and a " + "distance of {1}", myRoute.direction, myRoute.distance); Console.ReadKey();       }    } }

  3. Execute the code, select a direction, and then enter a distance. The result is shown in Figure 5-8.

    image from book
    Figure 5-8

How It Works

Structs, like enumerations, are declared outside of the main body of the code. You declare your route struct just inside the namespace declaration, along with the orientation enumeration that it uses:

enum orientation : byte {    north = 1,    south = 2,    east  = 3,    west  = 4 } struct route {    public orientation direction;    public double      distance; }

The main body of the code follows a similar structure to some of the example code you've already seen, requesting input from the user and displaying it. You perform some simple validation of user input by placing the direction selection in a do loop, rejecting any input that isn't an integer between 1 and 4 (with values chosen such that they map onto the enumeration members for easy assignment).

The interesting point to note is that when you refer to the members of route they are treated in exactly the same way as variables of the same type as the member would be. The assignment is as follows:

myRoute.direction = (orientation)myDirection; myRoute.distance = myDistance; 

You could simply take the input value directly into myRoute.distance with no ill effects as follows:

myRoute.distance = Convert.ToDouble(Console.ReadLine());

The extra step allows for more validation, although none is performed in this code.

Any access to members of a structure is treated in the same way. Expressions of the form structVar.memberVar can be said to evaluate to a variable of the type of memberVar.

image from book

Arrays

All the types you've seen so far have one thing in common: each of them stores a single value (or a single set of values in the case of structs). Sometimes, in situations where you want to store a lot of data, this isn't very convenient. Sometimes, you want to store several values of the same type at the same time, without having to use a different variable for each value.

For example, you might want to perform some processing that involves the names of all of your friends. You could use simple string variables such as:

 string friendName1 = "Robert Barwell"; string friendName2 = "Mike Parry"; string friendName3 = "Jeremy Beacock"; 

But this looks like it will need a lot of effort, especially because you need to write different code to process each variable. You couldn't, for example, iterate through this list of strings in a loop.

The alternative is to use an array. Arrays are indexed lists of variables stored in a single array type variable. For example, you might have an array that stores the three names shown above, called friendNames. You can access individual members of this array by specifying their index in square brackets, as shown here:

 friendNames[<index>] 

This index is simply an integer, starting with 0 for the first entry, using 1 for the second, and so on. This means that you can go through the entries using a loop, for example:

 int i; for (i = 0; i < 3; i++) { Console.WriteLine("Name with index of {0}: {1}", i, friendNames[i]); } 

Arrays have a single base type, that is, individual entries in an array are all of the same type. This friendNames array has a base type of string, as it is intended for storing string variables.

Array entries are often referred to as elements.

Declaring Arrays

Arrays are declared in the following way:

 <baseType>[] <name>; 

Here, <baseType> may be any variable type, including the enumeration and struct types you've seen in this chapter.

Arrays must be initialized before you have access to them. You can't just access or assign values to the array elements like this:

 int[] myIntArray; myIntArray[10] = 5; 

Arrays can be initialized in two ways. You can either specify the complete contents of the array in a literal form, or you can specify the size of the array and use the new keyword to initialize all array elements.

Specifying an array using literal values simply involves providing a comma-separated list of element values enclosed in curly braces, for example:

 int[] myIntArray = {5, 9, 10, 2, 99}; 

Here myIntArray has five elements, each with an assigned integer value.

The other method requires the following syntax:

 int[] myIntArray = new int[5]; 

Here, you use the new keyword to explicitly initialize the array and a constant value to define the size. This method results in all the array members being assigned a default value, which is 0 for numeric types. You can also use nonconstant variables for this initialization, for example:

 int[] myIntArray = new int[arraySize]; 

You can also combine these two methods of initialization if you wish:

 int[] myIntArray = new int[5] {5, 9, 10, 2, 99}; 

With this method the sizes must match. You can't, for example, write:

 int[] myIntArray = new int[10] {5, 9, 10, 2, 99}; 

Here, the array is defined as having 10 members, but only 5 are defined, so compilation will fail. A side effect of this is that if you define the size using a variable that variable must be a constant, for example:

 const int arraySize = 5; int[] myIntArray = new int[arraySize] {5, 9, 10, 2, 99}; 

If you omit the const keyword, this code will fail.

As with other variable types, there is no need to initialize an array on the same line that you declare it. The following is perfectly legal:

 int[] myIntArray; myIntArray = new int[5]; 

You've done enough to look at an example, so it's time to try out some code. In the following Try It Out you create and use an array of strings, using the example from the introduction to this section.

Try It Out – Using an Array

image from book
  1. Create a new console application called Ch05Ex04 in the directory C:\BegVCSharp\Chapter5.

  2. Add the following code to Program.cs:

    static void Main(string[] args) { string[] friendNames = {"Robert Barwell", "Mike Parry", "Jeremy Beacock"}; int i; Console.WriteLine("Here are {0} of my friends:", friendNames.Length); for (i = 0; i < friendNames.Length; i++) { Console.WriteLine(friendNames[i]); } Console.ReadKey(); }
  3. Execute the code. The result is shown in Figure 5-9.

    image from book
    Figure 5-9

How It Works

This code sets up a string array with three values and lists them in the console in a for loop. Note that you have access to the number of elements in the array using friendNames.Length:

Console.WriteLine("Here are {0} of my friends:", friendNames.Length);

This is a handy way to get the size of an array.

Outputting values in a for loop is easy to get wrong. For example, try changing < to <= as follows:

 for (i = 0; i <= friendNames.Length; i++) {    Console.WriteLine(friendNames[i]); }

Compiling this results in the dialog shown in Figure 5-10 popping up.

image from book
Figure 5-10

Here, you have attempted to access friendNames[3]. Remember, array indices start from 0, so the last element is friendNames[2]. If you attempt to access elements outside of the array size, the code will fail.

It just so happens that there is a more resilient method of accessing all the members of an array, using foreach loops.

image from book

foreach Loops

A foreach loop allows you to address each element in an array using this simple syntax:

 foreach (<baseType> <name> in <array>) {    // can use <name> for each element } 

This loop will cycle through each element, placing each one in the variable <name> in turn, without danger of accessing illegal elements. You don't have to worry about how many elements there are in the array, and you can be sure that you'll get to use each one in the loop. Using this approach, you can modify the code in the last example as follows:

static void Main(string[] args) {    string[] friendNames = {"Robert Barwell", "Mike Parry",                            "Jeremy Beacock"};    Console.WriteLine("Here are {0} of my friends:",                      friendNames.Length); foreach (string friendName in friendNames) { Console.WriteLine(friendName); }    Console.ReadKey(); }

The output of this code will be exactly the same that of the previous Try It Out.

The main difference between using this method and a standard for loop is that foreach gives you read- only access to the array contents, so you can't change the values of any of the elements. You couldn't, for example, do the following:

foreach (string friendName in friendNames) { friendName = "Rupert the bear"; }

If you try this, compilation will fail. If you use a simple for loop, however, you can assign values to array elements.

Multidimensional Arrays

From the title of this section, you would be forgiven for thinking that you are about to discover some low-budget science fiction addition to the C# language. In actual fact, a multidimensional array is simply one that uses multiple indices to access its elements.

For example, consider the situation in which you want to plot the height of a hill against the position measured. You might specify a position using two coordinates, x and y. You want to use these two coordinates as indices, such that an array called hillHeight would store the height at each pair of coordinates. This involves using multidimensional arrays.

A two-dimensional array such as this is declared as follows:

 <baseType>[,] <name>; 

Arrays of more dimensions simply require more commas; for example:

 <baseType>[,,,] <name>; 

This would declare a four-dimensional array.

Assigning values also uses a similar syntax, with commas separating sizes. To declare and initialize the two-dimensional array hillHeight, discussed previously, with a base type of double, an x size of 3, and a y size of 4 requires the following:

 double[,] hillHeight = new double[3,4]; 

Alternatively, you can use literal values for initial assignment. Here, you use nested blocks of curly braces, separated by commas, for example:

 double[,] hillHeight = {{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}}; 

This array has the same dimensional sizes as the previous one, but has values explicitly defined.

To access individual elements of a multidimensional array, you simply specify the indices separated by commas; for example:

 hillHeight[2,1] 

You can then manipulate this element just as you can other elements.

This expression will access the second element of the third nested array as defined previously (the value will be 4). Remember that you start counting from 0 and that the first number is the nested array. In other words, the first number specifies the pair of curly braces, and the second number specifies the element within that pair of braces. You can represent this array visually, as shown in Figure 5-11.

image from book
Figure 5-11

The foreach loop allows you access to all elements in a multidimensional way just as with single- dimensional arrays, for example:

 double[,] hillHeight = {{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}}; foreach (double height in hillHeight) { Console.WriteLine("{0}", height); } 

The order in which the elements are output is the same as the order used to assign literal values

hillHeight[0,0] hillHeight[0,1] hillHeight[0,2] hillHeight[0,3] hillHeight[1,0] hillHeight[1,1] hillHeight[1,2]

and so on.

Arrays of Arrays

Multidimensional arrays, as discussed in the last section, are said to be rectangular. This is so because each "row" is the same size. Using the last example, you can have a y coordinate of 0 to 3 for any of the possible x coordinates.

It is also possible to have jagged arrays, where "rows" may be different sizes. To do this, you need to have an array where each element is another array. You could also have arrays of arrays of arrays if you want, or even more complex situations. However, note that all this is only possible if the arrays have the same base type.

The syntax for declaring arrays of arrays involves specifying multiple sets of square brackets in the declaration of the array, for example:

 int[][] jaggedIntArray; 

Unfortunately, initializing arrays such as this isn't as simple as initializing multidimensional arrays. You can't, for example, follow this declaration with:

 jaggedIntArray = new int[3][4]; 

Even if you could do this, it wouldn't be that useful, because you can achieve the same effect with simple multidimensional arrays with less effort. You also can't use code such as:

 jaggedIntArray = {{1, 2, 3}, {1}, {1, 2}}; 

You have two options. You can initialize the array that contains other arrays (I'll call these subarrays for clarity) and then initialize the subarrays in turn

 jaggedIntArray = new int[2][]; jaggedIntArray[0] = new int[3]; jaggedIntArray[1] = new int[4]; 

or you can use a modified form of the preceding literal assignment:

 jaggedIntArray = new int[3][] {new int[] {1, 2, 3}, new int[] {1}, new int[] {1, 2}}; 

This can be simplified if the array is initialized on the same line as it is declared, as follows:

 int[][] jaggedIntArray = {new int[] {1, 2, 3}, new int[] {1}, new int[] {1, 2}}; 

You can use foreach loops with jagged arrays, but you often need to nest these to get to the actual data. For example, say you have the following jagged array that contains 10 arrays, each of which contains an array of integers that are divisors of an integer between 1 and 10:

 int[][] divisors1To10 = {new int[] {1}, new int[] {1, 2}, new int[] {1, 3}, new int[] {1, 2, 4}, new int[] {1, 5}, new int[] {1, 2, 3, 6}, new int[] {1, 7}, new int[] {1, 2, 4, 8}, new int[] {1, 3, 9}, new int[] {1, 2, 5, 10}}; 

The following code will fail:

 foreach (int divisor in divisors1To10) { Console.WriteLine(divisor); } 

This is because the array divisors1To10 contains int[] elements, not int elements. Instead, you have to loop through every subarray as well as through the array itself:

 foreach (int[] divisorsOfInt in divisors1To10) { foreach(int divisor in divisorsOfInt) { Console.WriteLine(divisor); } } 

As you can see, the syntax for using jagged arrays can quickly become complex! In most cases, it is easier to use rectangular arrays or a simpler storage method. However, there may well be situations in which you are forced to use this method, and a working knowledge can't hurt!




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

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