Team LiB
Previous Section Next Section

Arrays As Simple Collections

One simple type of collection that you may already be familiar with from your work with other programming languages is the array. We can think of an array as a series of compartments, with each compartment sized appropriately for whatever data type the array as a whole is intended to hold. Arrays typically hold items of like type: for example, int (eger)s, or char (acter)s, or, in an OO language, object references: Student objects, or Course objects, or Professor objects, etc.

Declaring and Instantiating Arrays

In C#, arrays are objects (as are all C# collections). The Array class of the System namespace is the basis for all C# arrays. The official C# syntax for declaring that a variable x will serve as a reference to an array containing items of a particular data type is as follows:

  datatype[] x;

For example:

  int[] x;

which is to be read "int (eger) array x" (or, alternatively, "x is an array of ints").

Because C# arrays are objects, they must be instantiated using the new operator; we also specify how many items the array is capable of holding, i.e., its size in terms of its number of compartments, when we first instantiate the array. Here is a code snippet that illustrates the somewhat unusual syntax for constructing an array; in this particular example, we're constructing an array designed to hold Student object references, depicted in Figure 6-3:

// Here, we are instantiating an array object that will be used to store 20
// Student object references, and are maintaining a handle on the array object
// via reference variable x.
Student[] x = new Student[20];
Click To expand
Figure 6-3: Array x is designed to hold up to 20 Student references.

This use of the new operator is unusual, in that we don't see a typical constructor call (with optional arguments being passed in via parentheses) following the new keyword, the way we do when we're constructing other types of objects. Despite its unconventional appearance, however, this line of code is indeed instantiating a new Array object, just the same.

Accessing Individual Array Elements

Individual array elements are accessed by appending square brackets, enclosing the index of the element to be accessed, to the end of the array name. This syntax is known as an element access expression. Note that when we refer to individual items in an array based on their position, or index, relative to the beginning of the array, we start counting at 0. (As it turns out, the vast majority of collection types in C# as well as in other languages are zero-based.) So, the items stored in array Student[] x in our previous example would be individually referenced as x[0], x[1], , x[19].

Consider the following code snippet:

int[] data = new int[3];
data[0] = 4; // setting an element's value
int temp = data[1]; // getting an element's value

In the first line of code, we're declaring and instantiating an int (eger) array of size 3. In the second line of code, we're assigning the int value 4 to the "zeroeth"(first) element of the array. In the last line of code, we're obtaining the value of the second element of the array (element number 1) and assigning it to an int variable named temp.

In the next snippet, we're populating an array named squareRoot of type double to serve as a look-up table of square root values, where the value stored in cell squareRoot[i] represents the square root of i. (We declare the array to be one element larger than we need it to be so that we may skip over the zeroeth cell; for ease of look-up, we want the square root of 1 to be contained in cell 1 of the array, not in cell 0.)

double[] squareRoot = new double[11]; // we'll effectively ignore cell 0

// Note that we're skipping cell 0.
for (int i = 1; i <= 10; i++) {
  squareRoot[i] = Math.Sqrt(i);

Console.WriteLine("The square root of 5 is " + squareRoot[5]);

The Math.Sqrt() method computes the square root of a double argument passed to the method; we're passing in an int in the preceding example, which automatically gets cast to a double.We'll revisit the Math class again in Chapter 7.

Initializing Array Contents

Values can be assigned to individual elements of an array using indexes as shown earlier, or we can initialize an array with a complete set of values when the array is first instantiated. In the latter case, initial values are provided as a comma-separated list enclosed in braces. This syntax replaces the normal right-hand side of the array instantiation statement. For example, the following code instantiates and initializes a three-element string array:

string[] names = { "Lisa", "Jackson", "Zachary" };

Note that C# automatically counts the number of initial values that we're providing, and sizes the array appropriately. The preceding approach is much more concise than the equivalent alternative shown here:

string[] names = new string[3];
names[0] = "Lisa";
names[1] = "Jackson";
names[2] = "Zachary";

although the result in both cases is the same: the zeroeth (first) element of the array will reference the string "Lisa", the next element will reference "Jackson", and so on.

Note that it isn't possible to "bulk load" an array in this fashion after the array has been instantiated, as a separate line of code; that is, the following won't work:

string[] names = new string[5];
// This next line won't compile.
names = {"Steve", "Jacquie", "Chloe", "Shylow", "Baby Grode" };

If a set of comma-separated initial values aren't provided when an array is first instantiated, the elements of the array are automatically initialized to their zero-equivalent values. For example, int[] data as declared earlier would be initialized to contain 3 integer zeroes (0s), and double[] squareRoot as declared earlier would be initialized to contain 11 floating point zeroes (0.0s). If we declare and instantiate an array intended to hold references to objects, as in

Student[] studentBody = new Student[100];

then we'd wind up with an Array object containing 100 null values (recall that null, a C# keyword, is the zero equivalent value for an object reference). If we think of an array as a simple type of collection, and we in turn think of a collection as an "egg carton," then we've just created an empty egg carton with 100 egg compartments, but no "eggs."

Manipulating Arrays of Objects

To fill our Student array with values other than null, we'd have to individually store Student object references in each cell of the array. For example, if we wanted to create brand-new Student objects to store in our array, we may write code as follows:

studentBody[0] = new Student();
studentBody[1] = new Student();
// etc.

or alternatively:

Student s = new Student();
studentBody[0] = s;
// Reuse s!
s = new Student();
studentBody[1] = s;

In the latter example, note that we're "recycling" the same reference variable, s, to create many different Student objects. This works because, after each instantiation, we store a handle on the newly created object in an array compartment, thus allowing s to let go of its handle on that same object, as depicted in Figure 6-4. This technique is used frequently, with all collection types, in virtually all OO programming languages.

Click To expand
Figure 6-4: Handing new objects one by one into a collection

When we've created an array to hold objects, as we did for the studentBody array previously, then assuming we've populated the array with object references, an indexed reference to any populated compartment in the array represents an object reference, and can be used accordingly:

studentBody[0].GetName();  // We're using dot notation to call a method on
                           // studentBody[0], the first Student object
                           // reference in the array.

The syntax of this message may seem a bit peculiar at first, so let's study it a bit more carefully. Since studentBody is declared to be an array capable of holding Student object references, then studentBody[n] represents the contents of the nth compartment of the array—namely, a reference to a Student object! So, the "dot" in the preceding message is separating an expression representing an object reference from the method call being made on that object, and is no different than any of the other dot notation messages that we've seen up until now.

By using a collection such as an array, we don't have to invent a different variable name for each Student object, which means we can step through them all quite easily using a for loop. Here is the syntax for doing so with an array:

// Step through all 100 compartments of the array.
for (int i = 0; i <= 99; i++) {
  // Access the "ith" element of the array -- a Student object (reference) -- so
  // that we may print each student's name in turn; in effect, we're printing
  // a student roster.

We'll examine a more sophisticated way of stepping through collections in general, using a special type of object called an IEnumerator, in Chapter 13.

Note that we have to take care when stepping through an array to avoid "land mines" due to empty compartments. That is, if we're executing the preceding for loop, but the array isn't completely filled with Student objects, then our invocation of the GetName method will fail as soon as we hit the first empty/null compartment, because in essence we'd be trying to talk to an object that wasn't there! If we modify our code by inserting an if test to guard against null values, however, we'd be OK:

// Step through all 100 compartments of the array.
for (int i = 0; i <= 99; i++) {
  // Avoiding "land mines"!
  if (studentBody[i] != null) {

We'll learn in Chapter 13 that this type of failure—namely, attempting to talk to a nonexistent object, or null reference—results in an exception being thrown.

Other Array Considerations

Some other facts concerning C# arrays:

  • Once the size of an array has been declared, its size can't be changed. This can become an issue in situations where we don't know what the size of an array should be at the time that we're declaring it. For this reason, arrays aren't always the best choice of collection type for a given application, as we'll discuss later in this chapter.

  • We can't mix and match data types in an array; we're constrained to inserting values whose data type matches the type with which the array was first declared. For instance, we can't assign a string as an element of an integer array. The only exception is that if we can cast some value of type A into type B, then we can of course make such an assignment. For example, if we wish to assign the value of a double variable to an element of a float array, we would cast the variable into a float and then assign it to the array element as shown here:

          float[] a = new float[10];
          double d = 10.37;
          a[0] = (float) d; // note cast

Multidimensional Arrays

So far we've been discussing one-dimensional arrays. It's also possible to declare and use arrays of two or more dimensions. There are two types of multidimensional arraysrectangular and jagged.

Rectangular Arrays

A rectangular array is one in which every row has the same number of columns:

3    2    37    8    4
7    4     9    0    3
1   11    99   13    5

This represents a three-row by five-column two-dimensional rectangular integer array.

The syntax for declaring and instantiating a two-dimensional rectangular array is as follows:

ArrayType [,] arrayName = new ArrayType[numRows, numCols];

For example:

double[,] values = new double[3, 5]; // three rows of five columns each

We place one comma between the brackets on the left-hand side to signal that the array will have two dimensions, and must then specify the size of each of the two dimensions, separated by a comma, on the right-hand side. For a three-dimensional rectangular array, we would use two commas between the brackets on the left-hand side, and would specify the sizes of three dimensions on the right-hand side:

ArrayType[,,] arrayName = new ArrayType[dim1, dim2, dim3];

and so forth.

To access the elements of a multidimensional rectangular array, we use indexes, but now must specify an index for each dimension of the element to be accessed. For example, consider the following code snippet, in which we instantiate a two-dimensional array of type double :

double[,] data = new double[2, 3];
data[0, 1] = 23.4; // insert value into the FIRST row, SECOND column
// details omitted ...
double temp = data[1, 2]; // retrieve value from the SECOND row, THIRD column

The array has two rows and three columns. In the second line of code, the element in the first row and second column ([0, 1]—remember that we start counting from 0) of the array is assigned the value 23.4. In the third line, we're accessing the value of the element in the second row and third column of the array, and assigning that value to a double variable named temp.

The elements of a multidimensional rectangular array can be initialized at the time that the array is declared by placing the initial values in braces { }, with one set of braces for each dimension of the array. For example, the following syntax would create and initialize a two-row, three-column array of integer values:

int[,] data = { {7, 22, 3},
                {48, 5, 10} };

Jagged Arrays

A jagged array is one where each row can have a different number of entries:

                      14     3     85    2
                     100    11
                       3    24    106

This represents a three-row, two-dimensional jagged integer array.

The syntax for declaring a jagged multidimensional array is different from that of a rectangular array.

  • A separate set of empty braces is provided in the declaration for every dimension in the array: for a two-dimensional jagged array, we use two sets of braces; for a three-dimensional jagged array, we use three; etc. For example, the following syntax would declare a two-dimensional jagged string array:

             string[][] names; // note two sets of empty braces
  • In the array instantiation statement, we only specify the size of the first dimension of such an array. For example, the following syntax would instantiate a two-dimensional jagged string array with three rows, allowing for a variable number of columns per row:

             string[][] names = new string[3][];

In effect, when we create a two-dimensional jagged array, we have in essence created an array of one-dimensional arrays of varying sizes, as illustrated in Figure 6-5.

Click To expand
Figure 6-5: A jagged two-dimensional array as an array of one-dimensional arrays

The next step in the process is to initialize the length of each row in the array; for example:

names[0] = new string[4]; // first row has 4 columns (numbered 0 ... 3)
names[1] = new string[2]; // second row has 2 columns (numbered 0 ... 1)
names[2] = new string[3]; // third row has 3 columns (numbered 0 ... 2)

To access the elements of a multidimensional jagged array, we use indexes with a separate set of brackets to specify each dimension. For example, to assign a value to the element in the second row and second column of the names array, we'd use the following syntax:

names[1][1] = "Mel";

It's also possible to initialize the elements of a multidimensional jagged array when the array is first declared, but the syntax is a bit complicated. The elements of every one-dimensional array within the multidimensional array can be initialized by placing the initial values inside braces when the one-dimensional array is declared. The new keyword must be used, and the array type must also be specified. For example, the following code creates and initializes a two-dimensional jagged integer array that has three columns in its first row and four columns in the second row:

int[][] data = new int[2][];
data[0] = new int[] { 17, 3, 24 };
data[1] = new int[] { 6, 37, 108, 99 };

Team LiB
Previous Section Next Section