Chapter 5. Arrays and Collections

An array is a collection of related items, either value or reference types. Arrays are immutable, such that the number of dimensions and size of the array are fixed at instantiation. C# supports single-dimensional, multidimensional, and jagged arrays. Single-dimensional arrays, sometimes called vectors, consist of a single row. Multidimensional arrays are rectangular and consist of rows, columns, and so on. A jagged array also consists of rows and columns but is irregular in shape.

Arrays are available in most programming languages, and therefore, most developers have some familiarity with this concept. Arrays are employed in a variety of ways. A personnel program, for example, might have an array of employees. A graphics program might have one array for each type of geometric object, such as ellipses, rectangles, or triangles. An accounting and scheduling application for automobile repair likely would have an array of automobile repair tickets. An application for NASA’s space shuttle program might have an array of astronauts.

Arrays are intrinsic to the language. Other collections, such as Stack, Queue, and Hashtable, are not native to the language. As such, ease of use is one of the benefits of arrays compared to other collections. Another benefit is familiarity. Arrays are available and are functionally similar to arrays in other programming languages.

As a collection, an array contains zero or more items, which are called elements. Elements of an array are always related, such as an array of apples or an array of oranges. An array might consist of SalariedEmployee, HourlyEmployee, and CommissionedEmployee instances. However, an array consisting of both apples and employees is nonsensical because apples and employees are unrelated.

Arrays are reference types and are instances of System.Array. As such, memory for an array is allocated from the managed heap. Even an array of value types is allocated on the managed heap and not on the stack. The elements of an array reside in contiguous memory. An array of 30 integer values would have 32 four-byte elements allocated in contiguous memory. With arrays of reference types, the references are allocated in contiguous memory. The objects themselves are stored in noncontiguous memory, which is pointed to by the references. Figure 5-1 shows the difference in memory allocation between arrays of reference versus value types.

Array of reference types versus value types

Figure 5-1. Array of reference types versus value types

Elements are identified with indexes that are relative to the beginning of the array. Indexes are either integer types or long types. Indexes also are commonly called indices or subscripts, and are placed inside the indexing operator ([ ]). Arrays are zero-based so that the index is actually an offset. Array indexes are offsets from the beginning of the array to a particular element. Therefore, the first element is at the start of the array, which is an offset of zero. For an array of five elements, a proper index is from zero to four. Therefore, the last element of the array of n elements is at index n-1. This is a common cause of fencepost errors. Fencepost errors are references to indexes that lie outside the bounds of an array. If an array has five elements, using an index of six would cause a fencepost error.

As mentioned, arrays are immutable. This means that an array is statically sized, and the dimensions cannot be changed at run time. The System.Array.Resize method, which is a generic method, seemingly resizes an array. However, appearances can be deceiving. Array.Resize creates an entirely new array of the stipulated size. The elements of the original array are then copied to the elements of the new array. Afterwards, the array reference is updated to point to the new array. Look at the following code:

int[] obj = new int[] { 4, 5, 6 };
Console.WriteLine(obj.GetHashCode().ToString());
Array.Resize(ref obj, 10);
Console.WriteLine(obj.GetHashCode().ToString());

The hash code returns the identity of an object. The preceding code displays a different identity for the obj reference after Array.Resize is called.

Single-dimensional arrays are indigenous to the Common Language Runtime (CLR). There are specific Microsoft Intermediate Language (MSIL) instructions for vectors, including newarr, ldelem, ldlen, and stelem. There are no built-in instructions for multidimensional arrays. This direct manipulation of single-dimensional arrays makes them more efficient. In addition, some of the members of the System.Array type cannot be applied to multidimensional arrays. Conversely, all the methods and properties of the System.Array type are applicable to single-dimensional arrays.

The System.Array type is the underlying type of all arrays. Instances of arrays are always instances of the System.Array type. Thus, all arrays are implicitly reference types. Arrays can directly access the public interface and properties of the System.Array type. System.Array implements a series of interfaces. Table 5-1 lists those interfaces.

Table 5-1. Interfaces implemented by System.Array

Interface

Description

ICloneable

This interface defines the ability to clone an array.

ICollection

This interface defines methods to add, count, and iterate the elements of an array.

IEnumerable

This interface defines a method that returns an enumerator, which can be used to enumerate the array elements.

IList

This interface defines methods to safely access an index-based collection, which includes an array.

Arrays also can be used as fields, local variables, method parameters, and return values. Because an array is a reference type, it is passed by reference when used as a parameter or a return value. This allows you to change the array values in the called method.

Arrays

The following is the syntax for declaring a one-dimensional array. A vector has a single index. When declaring an array, the empty square brackets define an array, while the indexing operator on the right-hand side of the assignment sets the size of the array:

      typea[] arrayname1;      typea[] arrayname2 = new typeb[n];      typea[] arrayname3 = new typeb[n] { ilist };      typea[] arrayname4 = new typeb[] { ilist };      typea[] arrayname5 = { ilist };

The first syntax declares a reference to an array that is not initialized. You can initialize the reference to an instance of an array later. The following is sample code of the first syntax for declaring an array. It declares an integer array named zArray. The array is then assigned a new array of 10 integers, which initializes the reference. The array elements of 10 integers also are initialized. Array elements default to zero or null: zero for value type elements and null for reference type elements. Because integers are value types, the array elements of the following array are set to zero:

int[] zArray;
zArray = new int[10];

The second syntax declares and initializes a reference to an array. The array reference is immediately assigned a reference to the new instance of an array. An array must be initialized to an array of the same or related type as the declaration. The array declaration and the array instantiation must be related. For example, bytes are not related to integers. You can cast a byte to an integer, but this does not mean that the types are related. Because bytes and integers are not related types, a byte array cannot be assigned to an integer array declaration.

Here is sample code of the second syntax:

byte aValue = 10;
int bValue = aValue;         // valid
int[] zArray = new byte[5];  // invalid
int[] yArray = new int[5];   // valid

In the following example, XBase and XDerived classes are related. Therefore, you can assign an array of the derived type to an array of the base type:

public class Starter {
    public static void Main() {
        XBase[] obj = new XDerived[5];  // base    <- derived
        XDerived[] obj2 = new XBase[5]; // derived <- base [invalid]
    }
}

public class XDerived : XBase {
}

public class XBase {
}

The third syntax declares an array, initializes the reference, and assigns values to the array elements. The initialization list (ilist) contains the initial values for the elements of the array, where the values are comma-delimited. The number of values in the list must match the number of elements in the array exactly—no more and no less.

This code shows an example of the third syntax for declaring an array:

int[] zArray = new int[3] {1,2,3};  // valid
int[] yArray = new int[3] {1,2};    // invalid
ZClass[] xArray = new ZClass[3] {   // valid
    new ZClass(5), new ZClass(10),
    new ZClass(15) };

The fourth and third syntax are identical except that the initialization list sets the number of elements. The array size is not set explicitly in the assignment. The compiler counts the number of items in the initialization list to set the length of the array.

This is an example of the fourth syntax:

int[] zArray = new int[] {1,2,3,4,5};  // 5 elements

The fifth syntax is an abbreviation of the fourth syntax, where the array type and number of elements are inferred from the initialization list.

This is an example of the fifth syntax:

int[] yArray = {1,2,3,4,5};

Local variable and field definitions can be defined simultaneously. Multiple declarations of arrays can also be combined, as shown here:

public class ZClass{
    private int[] first = {1,2,3},
                  second = {4,5,6},
                  third = {7,8,9};

    // Remainder of class...
}

Array Elements

Array indexing operators refer to elements of an array. With an index, the indexing operator returns a specific element of the array. When an indexing operator is used on the left-hand side, you can assign a value to the array element. On the right-hand side, the indexing operator returns the value of the element.

The following for loop lists the elements of an array. The indexing operator is used to return the element value. The program calculates the total value of elements in the array. It is also used on the right-hand side to assign a value of the array element to a local variable:

int[] zArray = {1,2,3,4,5};
int total = 0;
for (int count = 0; count < zArray.Length; ++count) {
    total += zArray[count];              // r-value
    int number = zArray[count];          // r-value
    Console.WriteLine(number);
}
Console.WriteLine("
The total is {0}.",
    total);

Multidimensional Arrays

You are not limited to one-dimensional arrays. Multidimensional arrays are rectangular arrays and have multiple dimensions and indices. Two-dimensional arrays, which consist of rows and columns, are the most common kind of multidimensional array. Each row contains the same number of columns, thus making the array rectangular. From a memory perspective, the x-axis is the rows and the y-axis defines the columns. Multidimensional arrays are stored and retrieved in row-major order.

The total number of elements in a multidimensional array is the product of the indices. For example, an array of 5 rows and 6 columns has 30 elements. The Array.Length property returns the total number of elements in the array. The Array.GetLength method returns the number of elements per indice. The indices are numbered from zero. For a two-dimensional array, the row is the zero dimension, whereas the column is the one dimension. GetLength(0) would then return the number of rows in the multidimensional array.

The following is the syntax to declare a two-dimensional array. Notice the indexing operator. Row (r) and column (c) indexes in the indexing operator are delimited with a comma. In the declaration, there is a comma in the array operator ([,]). In general when declaring an array there are n-1 commas for n number of indices. For a two-dimensional array, then, there would be one comma:

      typea[,] arrayname1;      typea[,] arrayname2 = new typeb[r,c];      typea[,] arrayname3 = new typeb[r,c] { ilist };      typea[,] arrayname4 = new typeb[,] { ilist };      typea[,] arrayname5 = { ilist };

The initialization list of a multidimensional array includes nested initialization lists for each row. If an array has two rows, the initialization list includes two nested initialization lists. This is the syntax of a nested initialization list:

{ { ilist }, { ilist }, { ilist } ...}

The following sample code demonstrates the various declaration syntaxes, including nested initialization lists:

int[,] array1;                // syntax 1
array1 = new int[2,2];
int[,] array2 = new int[2,3]; // syntax 2
int[,] array3 = new int[2,3]  // syntax 3
    { {1,2,3}, {4,5,6} };
int[,] array4 = new int[,]    // syntax 4
    { {1,2,3}, {4,5,6} };
int[,] array5 =               // syntax 5
    { {1,2,3}, {4,5,6} };

To access an element of a multidimensional array, specify a row and column index in the indexing operator. It can be used on the left-hand side or right-hand side to set or get the value of an element, respectively. The following code calculates the total of the elements. This requires enumerating all the elements of a multidimensional array:

int[,] zArray = new int[2,3]
    { {1,2,3}, {4,5,6} };
int total = 0;
for (int row = 0; row < zArray.GetLength(0); ++row) {
    for (int col = 0; col < zArray.GetLength(1); ++col) {
        total += zArray[row, col];              // r-value
        int number = zArray[row, col];          // r-value
        Console.WriteLine(number);
    }
}
Console.WriteLine("
The total is {0}.",
    total);

We have been focusing thus far on two-dimensional arrays. However, arrays can have more than two dimensions. In fact, there is no limit to the number of dimensions an array can have. Three-dimensional and four-dimensional arrays are less common than two-dimensional arrays, but are seen nonetheless. Arrays with more than four dimensions are rarely seen. Most developers find multidimensional arrays beyond two indices mind-numbing to manage and manipulate. Additional dimensions require added comma-delimited indexes when the array is declared, defined, and used.

This is an example of a four-dimensional array:

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

How is the preceding code interpreted? A multidimensional array can be viewed as a hierarchical array that consists of layers. Each layer represents a different level of array nesting. The previous example defines a single-dimensional array with only one element, which is itself a single-dimension array that contains two nested arrays. The two nested arrays each contain three arrays. Each of these arrays contains two elements.

This is a diagram of the array hierarchy, where the numbers indicate the index into that node in the hierarchy:

{                                   1                                    };    layer 1
{ {               1                }, {                2               } };    layer 2
{ {   {  1   }, {  2   }, {  3   } }, { {  1   }, {  2   }, {  3   }   } };    layer 3
{ { { { 1, 2 }, { 1, 2 }, { 1, 2 } }, { { 1, 2 }, { 1, 2 }, { 1, 2 } } } };    layer 4

The following code demonstrates a practical use of a multidimensional array. The program maintains the grades of students. Each student attends two classes, where each class has a name and grade. This is an array of object types, which means each element can contain anything. Strings, integers, reference types, or anything else can be placed in an array of objects. Everything in the .NET Framework Class Library (FCL) is derived (related) to System.Object. The downside is boxing and unboxing of value types placed in the array. In the example, grades are value types:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {

            string[] names = {"Bob", "Ted", "Alice"};
            object[,,] grades = new object[3,2,2]
                { { {"Algebra", 85},  { "English", 75} },
                  { {"Algebra", 95},  { "History", 70} },
                  { {"Biology", 100}, { "English", 92} } };

            for (int iName = 0; iName < names.Length; ++iName) {
                Console.WriteLine("
{0}
", names[iName]);
                for (int iCourse = 0; iCourse < 2; ++iCourse) {
                    Console.WriteLine("{0} {1}",
                        grades[iName, iCourse, 0],
                        grades[iName, iCourse, 1]);
                }
            }
        }
    }
}

Jagged Arrays

The most frequent way of explaining a jagged array is to say that it is an array of arrays. More specifically, a jagged array is an array of vectors. Although other two-dimensional arrays are rectangular, a jagged array, as the name implies, is jagged. Each vector of the jagged array can be of a different length. With jagged arrays, first define the number of rows or vectors in the jagged array. Second, declare the number of elements in each row.

The syntax for declaring a jagged array is similar to a two-dimensional array. Instead of a single bracket ([,]), jagged arrays have two brackets ([][]).You initialize the jagged array by specifying the number of rows but omitting the number of columns ([r][]). Later, each row of the jagged array is individually initialized to a vector, each of which can be of varying sizes. This is the syntax of a jagged array:

     typea[][] arrayname1;     typea[][] arrayname2 = new typeb[r][];

     typea[][] arrayname3 = new typeb[r][] { ilist };     typea[][] arrayname4 = new typeb[][] { ilist };     typea[][] arrayname5 = { ilist };

Here is sample code for declaring or initializing jagged arrays:

int[][] zArray;                        // syntax 1
int[][] yArray = new int[3][];         // syntax 2
int[][] xArray = new int[3][]          // syntax 3
    {new int[] {1,2,3},
     new int[] {1,2},
     new int[] {1,2,3,4}};
int[][] wArray = new int[][]{          // syntax 4
     new int[] {1,2,3},
     new int[] {1,2},
     new int[] {1,2,3,4}};
int[][] wArray = {                     // syntax 5
     new int[] {1,2,3},
     new int[] {1,2},
     new int[] {1,2,3,4}};

After initialization, each row of the jagged array is set to a one-dimensional array. Because each row is a distinct array, the length of each row can vary—hence, the array is jagged:

jarray[row] = new type[elements];

Here is sample code that employs a jagged array. In the example, each successive row has one more element in its vector. The first nested loop creates the vector for each row and initializes the new values. The final loop totals the values of each row and displays the result:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            int[][] jagged = new int[7][];
            int count = 0;
            for (int row = 0; row < jagged.GetLength(0); ++row) {
                Console.Write("
Row {0}:", row);
                jagged[row] = new int[row + 1];
                for (int index = 0; index < row + 1; ++index) {
                    ++count;
                    jagged[row][index] = count;
                    Console.Write(" {0}", count);
                }
            }
            Console.WriteLine("

Totals");
            for (int row = 0; row < jagged.GetLength(0); ++row) {
                int total = 0;
                for (int index = 0; index < jagged[row].GetLength(0);
                        ++index) {
                    total += jagged[row][index];
                }
                Console.Write("
Row {0}: {1}",
                    row, total);
            }
        }
    }
}

System.Array

The System.Array type implements the fundamental methods and properties that are essential to an array. This includes sorting, reversing, element count, synchronization, and much more. Table 5-2 lists the methods of the System.Array type. Many of the methods are static, which is noted in the syntax. In addition, some methods are for single-dimensional arrays and are not usable with multidimensional arrays. This fact is noted in the description of the method in Table 5-2. Some of these methods are covered in more detail later in this chapter.

Table 5-2. System.Array members

Description

Syntax

AsReadOnly

This is a generic method that returns a read-only wrapper for an array.

static ReadOnlyCollection<T>
    AsReadOnly<T>(
    T[] sourceArray)

BinarySearch

This method conducts a binary search for a specific value in a sorted one-dimensional array and returns an index. If the value is not found, a negative value is returned.

There are several overloads for this method. The two most common overloads are shown.

static int BinarySearch(
     Array sourceArray,
     object searchValue)
static int BinarySearch<T>(
     T[] sourceArray,
     T value)

Clear

This method sets a range of elements to a default value: zero, null, or false.

static void Clear(Array sourceArray,
    int index, int length)

Clone

This method clones the current array.

object Clone()

ConstrainedCopy

This method copies a range of elements from the source array into a destination array. You set the source index and destination index, indicating where the copy is started in both arrays. Changes are guaranteed to be undone if the copy does not complete successfully.

static void ConstrainedCopy{
    Array sourceArray,
    int sourceIndex
    Array destinationArray,
    int destinationIndex,
    int length)

ConvertAll

This is a generic method that converts the type of an array. The results are returned in an array of the destination type.

static <destinationType>
    ConvertAll<sourceType,
    destinationType>(
    sourceType sourceArray,
    Converter<sourceType,
    destinationType> converter)

Copy

This method copies elements from the source array to the destination array. The specified number of elements is copied. This method does not make the same guarantees as ConstrainedCopy.

There are four overloads to this method. The two most common overloads are listed in this table.

static void Copy(
    Array sourceArray,
    Array destinationArray,
    int length)
static void Copy(
    Array sourceArray,
    int sourceIndex,
    Array destinationArray,
    int destinationIndex,
    int length)

CopyTo

This method copies the current one-dimensional array to the destination array starting at the specified index.

void CopyTo(Array destinationArray,
    int index)
void CopyTo(Array destinationArray,
    long index)

CreateInstance

This method creates an instance of an array at run time.

This method has several overloads. One-dimensional and two-dimensional versions of the method are listed in this table.

static Array CreateInstance(
   Type arrayType,
   int length)
static Array CreateInstance(
   Type arrayType,
   int rows,
   int cols)

Exists

This is a generic method that confirms that at least one element matches the conditions set in the predicate function.

static bool Exist<T> {
    T [] sourceArray,
    Predicate<T> match)

Find

This is a generic method that finds the first element that matches the conditions set in the predicate function. If not found, the default value of the T type is returned.

static T Find<T>(
    T[] sourceArray,
    Predicate<T> match)

FindAll

This is a generic method that returns all the elements that match the conditions set in the predicate function.

static T[] FindAll<T>(
    T[] sourceArray,
    Predicate<T> match)

FindIndex

This is a generic method that returns the index to the first element that matches the conditions set in the predicate function. If no match is found, -1 is returned.

static int FindIndex<T>(
    T[] sourceArray,
    Predicate<T> match)
static int FindIndex<T>(
    T[] sourceArray,
    int startingIndex,
    Predicate<T> match)
static int FindIndex(
    T[] sourceArray,
    int startingIndex,
    int count,
    Predicate<T> match)

FindLast

This is a generic method that returns the last element that matches the conditions set in the predicate function. If no match is found, the default value of the T type is returned.

static T FindLast<T>(
    T[] sourceArray,
    Predicate<T> match)

FindLastIndex

This is a generic method that returns the index to the last element that matches the conditions set in the predicate function. If no match is found, -1 is returned.

static int FindLastIndex(
    T[] sourceArray,
    Predicate<T> match)
static int FindLastIndex(
    T[] sourceArray,
    int startingIndex,
    Predicate<T> match)
static int FindLastIndex(
    T[] sourceArray,
    int startingIndex,
    int count,
    Predicate<T> match)

ForEach

This is a generic method that performs an action on each element of the array, where the action refers to a function.

public static void ForEach<T>(
    T[] array,
    Action<T> action)

GetEnumerator

This method returns an enumerator that implements the enumerator pattern for collections. You can enumerate the elements of the array with the enumerator object.

sealed IEnumerator GetEnumerator()

GetLength

This method returns the number of elements for any dimension of an array.

int GetLength(int dimension)

GetLongLength

This method returns the number of elements, as a 64-bit integer, for any dimension of an array.

long GetLongLength(int dimension)

GetLowerBound

This method returns the lower bound of a dimension. In C#, the lower bound of an array is zero.

int GetLowerBound(int dimension)

GetUpperBound

This method returns the upper bound of a dimension.

int GetUpperBound(int dimension)

GetValue

This method returns the value of an element at the specified index.

This method has several overloads. A one-dimensional version and a multidimensional version of the method are shown in this table.

object GetValue(int index)
object GetValue(params int[] indices)

IndexOf

This method returns the index of the first element in a one-dimensional array that matches the specified value.

This method has several overloads. A generic and a non-generic version of the method are listed in this table.

static int IndexOf(Array sourceArray,
    object find)
static int IndexOf<T>(T[] sourceArray,
    T value)

Initialize

This method initializes every element of the array. The default constructor, if any, of each element is called.

void Initialize()

LastIndexOf

This method returns the index of the last element that matches the specified value in a one-dimensional array.

This method has several overloads. A generic version and a non-generic version are listed in this table.

static int LastIndexOf(Array sourceArray,
    object value)
static int LastIndexOf<T>(T[] sourceArray,
    T value)

Resize

This is a generic method that changes the size of a one-dimensional array.

static void Resize<T>(
    ref T[] sourceArray,
    int newSize)

Reverse

This method reverses the order of elements in a one-dimensional array.

static void Reverse(Array sourceArray)
static void Reverse(Array sourceArray,
    int index, int length)

SetValue

This method sets the value of a specific element of the current one-dimensional array.

This method has several overloads. Two of the more common overloads are listed in this table.

void SetValue(object value, int index)
void SetValue(object value,
    params int[] indices)

Sort

This method sorts the elements of a one-dimensional array using the IComparable interface of the element type.

This method has several overloads. A non-generic version and a generic version of the method are listed in this table.

static void Sort(Array sourceArray)
static void Sort<T>(
    T[] sourceArray)

TrueForAll

This is a generic method that returns true if all elements of an array match the conditions set in the predicate function.

static bool TrueForAll<T>(
    T[] array,
    Predicate<T> match)

The following sections offer further descriptions and sample code for some of the System.Array methods.

Array.AsReadOnly Method

The following code creates an integer array and initializes the values. The second element of the array is then modified, which confirms the read-write capability of the array. Array.AsReadOnly is then called to wrap the array in a read-only collection. The ReadOnlyCollection type is found in the System.Collections.ObjectModel namespace. After displaying the elements of the read-only collection, the program attempts to modify an element of the collection. Because the collection is read-only, a compiler error occurs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            int[] zArray = {1,2,3,4};
            zArray[1] = 10;
            ReadOnlyCollection<int> roArray = Array.AsReadOnly(zArray);
            foreach (int number in roArray) {
                Console.WriteLine(number);
            }
            roArray[1] = 2;   //  compiler error
        }
    }
}

Array.Clone Method

In the following code, the CommissionedEmployee class inherits from the Employee class. An array of commissioned employees is defined and then cloned with the Clone method. The result is cast to an array of Employees. Because Clone returns an object array, which is unspecific, you should cast to a specific array type. The cast from System.Object[] is not type-safe, and an incorrect cast could cause an exception later in the application. Polymorphism is employed in the subsequent foreach loop to call the correct Pay method on the derived instance:

using System;
using System.Collections.Generic;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            CommissionedEmployee[] salespeople =
                { new CommissionedEmployee("Bob"),
                  new CommissionedEmployee("Ted"),
                  new CommissionedEmployee("Sally") };

            Employee[] employees =
                (Employee[])salespeople.Clone();

            foreach(Employee person in
                    employees) {
                person.Pay();
            }
        }
    }

    public class Employee {
        public Employee(string name) {
            m_Name = name;
        }

        public virtual void Pay() {
            Console.WriteLine("Paying {0}", m_Name);
        }

        private string m_Name;
    }

    public class CommissionedEmployee : Employee {
        public CommissionedEmployee(string name) :
            base(name) {
        }

        public override void Pay() {
            base.Pay();
            Console.WriteLine("Paying commissions");
        }
    }
}

Array.CreateInstance Method

The following code demonstrates both the CreateInstance and SetValue methods. CreateInstance creates a new array at run time. This requires a degree of reflection, which is discussed in Chapter 13. This code first creates an array at run time using Activator.CreateInstance. The type of array is input from the command line. In the for loop, values are assigned to each element of the array. The values also are input from the command line. Activator.CreateInstance creates the values, which are then assigned to elements of the array. In the subsequent foreach loop, the elements of the array are enumerated. A member function is then invoked on the element. The name of the method is read from the command line:

using System;
using System.Reflection;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main(string[] argv) {
            Assembly executing=Assembly.GetExecutingAssembly();
            Type t = executing.GetType(argv[0]);
            Array zArray = Array.CreateInstance(
                t, argv.Length - 2);
            for (int count = 2; count < argv.Length; ++count) {
                System.Object obj = Activator.CreateInstance(t, new object[] {
                    argv[count]});
                zArray.SetValue(obj, count - 2);
            }
            foreach (object item in zArray) {
                MethodInfo m = t.GetMethod(argv[1]);
                m.Invoke(item, null);
            }
        }
    }

    public class ZClass {
        public ZClass(string info) {
            m_Info = "ZClass " + info;
        }

        public void ShowInfo() {
            Console.WriteLine(m_Info);
        }

        private string m_Info;
    }

    public class YClass {
        public YClass(string info) {
            m_Info = "YClass " + info;
        }

        public void ShowInfo() {
            Console.WriteLine(m_Info);
        }

        private string m_Info;
    }

    public class XClass {
        public XClass(string info) {
            m_Info = "XClass " + info;
        }

        public void ShowInfo() {
            Console.WriteLine(m_Info);
        }

        private string m_Info;
    }
}

A typical command line and results of the application are shown in Figure 5-2.

A command line and the results from running the application

Figure 5-2. A command line and the results from running the application

System.Array and Predicates

Several System.Array methods use predicates, including the Exists, Find, FindAll, and FindLastIndex methods. Predicates are essentially delegates. The predicate function is called for each element of the array. An array predicate performs a test on some condition. If the condition is met, true is returned. If not, false is returned.

This section makes references to generics, which is discussed in Chapter 7. Refer to Chapter 7 for a complete explanation on generics and related topics.

This is the syntax of the Predicate delegate:

delegate bool Predicate<T>(T obj)

Predicate methods are generic methods. The type parameter indicates the element type. The return value is the result of the test.

The following code finds all elements equal to 3. MethodA is the predicate method, which compares each value to 3. The Boolean value true (result == 0) is returned in MethodA if the element equals 3:

public static void Main() {
    int[] zArray = {1,2,3,1,2,3,1,2,3};
    Predicate<int> match = new Predicate<int>(MethodA<int>);
    int[] answers = Array.FindAll(zArray, match);
    foreach (int answer in answers) {
        Console.WriteLine(answer);
    }
}

public static bool MethodA<T>(T number) where T : IComparable {
    int result = number.CompareTo(3);
    return result == 0;
}

Array.Resize Method

The Resize method resizes a one-dimensional array.

Here is sample code for resizing an array. The elements added, if any, to the array are initialized to a default value:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            int[] zArray = {1,2,3,4};
            Array.Resize<int>(ref zArray, 8);
            foreach (int number in zArray) {
                Console.WriteLine(number);
            }
        }
    }
}

System.Array Properties

System.Array has several properties that are useful when working with arrays. Table 5-3 lists the various properties.

Table 5-3. System.Array properties

Description

Syntax

IsFixedSize

This property returns true if the array is a fixed size. Otherwise, it returns false. This is always true for arrays.

virtual bool IsFixedSize {
    get; }

IsReadOnly

This property returns true if the array is read-only. Otherwise, it returns false. This is always false for arrays.

virtual bool IsReadOnly {
    get; }

IsSynchronized

This property returns true if the array is thread-safe. Otherwise, it returns false. This is always false for arrays.

virtual bool IsSynchronized{
    get; }

Length

This property returns the number of elements in the array.

int Length {
    get; }

LongLength

As a 64-bit value, this property returns the number of elements in the array.

long LongLength {
    get; }

Rank

This property returns the rank of the array, which is the number of dimensions. For example, a two-dimensional array has a rank of two.

int Rank {
    get; }

SyncRoot

This property returns a synchronization object for the current array. Arrays are not inherently thread-safe. You can synchronize access to the array with this synchronization object.

virtual object SyncRoot {
    get; }

Many of the array properties are used in the sample code previously shown in this chapter. The SyncRoot property is not included in a previous example and is particularly important.

Array.SyncRoot Property

The purpose of the SyncRoot object is to synchronize access to an array. Arrays are not thread-safe. As documented in Table 5-3, the IsSynchronized property always returns false for an array. When accessed from multiple threads, arrays are easily synchronized with the lock statement, where the SyncRoot object is the parameter.

In the following code, the array is a field in the Starter class. The DisplayForward and DisplayReverse methods list array elements in forward and reverse order, respectively. The functions are invoked in separate threads, where DisplayForward and DisplayReverse might be called at the same time. This overlapping execution and access to the array could cause incorrect output. The SyncLock property of the array field is used to prevent simultaneous access by the concurrent threads:

using System;
using System.Threading;

namespace Donis.CSharpBook {
    public class Starter {

        public static void Main() {
            Array.Sort(zArray);
            Thread t1 = new Thread(
                new ThreadStart(DisplayForward));
            Thread t2 = new Thread(
                new ThreadStart(DisplayReverse));
            t1.Start();
            t2.Start();
        }

        private static int[] zArray = {1,5,4,2,4,2,9,8};

        public static void DisplayForward() {
            lock(zArray.SyncRoot) {
                Console.Write("
Forward: ");
                foreach (int number in zArray) {
                    Console.Write(number);
                }
            }
        }

        public static void DisplayReverse() {
            lock(zArray.SyncRoot) {
                Array.Reverse(zArray);
                Console.Write("
Reverse: ");
                foreach (int number in zArray) {
                    Console.Write(number);
                }
                Array.Reverse(zArray);
            }
        }
    }
}

Comparable Elements

The following System.Array methods compare elements to a value or another element:

  • Array.IndexOf

  • Array.LastIndexOf

  • Array.Sort

  • Array.Reverse

  • Array.BinarySearch

As such, the elements must be instances of comparable types. Comparable types implement the IComparable interface, which requires the implementation of the CompareTo method.

The IComparable.CompareTo method returns zero when the current and target instances are equal. If the current instance is less than the target, a negative value is returned, and if the current instance is greater than the target, a positive value is returned. The previously listed System.Array methods call IComparable.CompareTo to perform the necessary comparisons for sorting, retrieving elements on value, or otherwise accessing the array in an ordered manner.

A run-time error occurs in the following code when Array.Sort is called. Why? The XClass instances, which are the array elements, do not implement the IComparable interface:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            XClass[] objs = {new XClass(5), new XClass(10),
                new XClass(1)};
            Array.Sort(objs);
        }
    }

    public class XClass {
        public XClass(int data) {
            propNumber = data;
        }

        private int propNumber;
        public int Number {
            get {
                return propNumber;
            }
        }
    }
}

Here is the proper code, where the XClass implements the IComparable interface. This program runs successfully:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            XClass[] objs = { new XClass(5), new XClass(10),
                new XClass(1) };
            Array.Sort(objs);
            foreach (XClass obj in objs) {
                Console.WriteLine(obj.Number);
            }
        }
    }

    public class XClass : IComparable {
        public XClass(int data) {
            propNumber = data;
        }

        private int propNumber;
        public int Number {
            get {
                return propNumber;
            }
        }

        public int CompareTo(object obj) {
            XClass comp = (XClass) obj;
            if (this.Number == comp.Number) {
                return 0;
            }
            if (this.Number < comp.Number) {
                return -1;
            }
            return 1;
        }
    }
}

Many of the methods and properties of System.Array are required from interfaces that System.Array inherits. The following section lists those interfaces and methods.

ICollection Interface

The ICollection interface defines behavior to return the count of elements, copy a collection, and provide synchronization support for collections. The members of the ICollection interface are as follows:

  • CopyTo method

  • Count property

  • IsSynchronized property

  • SyncRoot property

ICloneable Interface

System.Array also implements the ICloneable interface. This is the interface for duplicating an object, including an array. The only member of this interface is the Clone method.

IEnumerable

System.Array implements the IEnumerable interface to support enumeration. IEnumerable.GetEnumerator is the sole member of this interface. GetEnumerator returns an enumerator object that implements the IEnumerator interface. The enumerator provides a consistent interface for enumerating any collection. Enumerable objects are also convenient. For example, the foreach statement can enumerate the elements of an array easier than the more generic for statement can. Incorrectly iterating an array is a common error in programs. The foreach statement makes array iteration trivial.

Chapter 8, will focus more on enumerators.

Here is sample code that enumerates an array using an enumerator object:

using System;
using System.Collections;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            int[] numbers = { 1, 2, 3, 4, 5 };
            IEnumerator e = numbers.GetEnumerator();

            // first iteration
            while (e.MoveNext()) {
                Console.WriteLine(e.Current);
            }

            // second iteration
            foreach (int number in numbers) {
                Console.WriteLine(number);
            }
        }
    }
}

IList Interface

System.Array type implements the IList interface. However, only part of this implementation is publicly available. Some members of the IList interface are contrary to the array paradigm, such as the RemoveAt method. Arrays are immutable. You cannot remove elements from the middle of an array. For this reason, RemoveAt is implemented privately. Other methods of the IList interface are implemented privately for similar reasons.

Table 5-4 lists the IList interface and whether the implementation in System.Array is public or private.

Table 5-4. List members

Member

Public or private?

Add

Private

Clear

Public

Contains

Private

IndexOf

Public

Insert

Private

Remove

Private

RemoveAt

Private

Indexers

Indexers are properties with the outward appearance of an array. They allow you to access an object as an array. For types that contain a collection as a member, indexers are helpful because they allow you to provide safe and convenient access to the internal collection. The backing data of an indexer is typically an array or a collection. The indexer defines a set and get method, which is common for all properties. However, the get and set method of an indexer are for the this reference. Indexers are considered a default property because the indexer is a nameless property. Internally, the compiler uses the get_Item and set_Item method to support indexers.

Here are some of the similarities and differences between indexers and standard properties:

  • Indexers can be overloaded.

  • Indexers can be overridden.

  • Indexers can be members of interfaces.

  • Indexers support the standard access modifiers.

  • Indexers cannot be static.

  • Indexers are nameless and associated with the this reference.

  • Indexers’ parameters are indices, while properties do not have indices.

  • Indexers are accessed using indices.

  • Indexers in a base class are accessed as base[indices], while a property in a base class is accessed as base.Property.

  • Indexers support numeric and non-numeric indices, while arrays support only integral indices.

Using an indexer is functionally similar to using an array. However, there are a couple of important differences:

  • Indexers typically have a data store, whereas an array is the data store.

  • Indexers can perform automatic data validation, and an array cannot.

Here is the syntax of an indexer:

accessibility modifier type this[parameters]{ attributes get {getbodyattributes set {setbody} }

Except for static accessibility, indexers have the same accessibility and modifiers as a normal property. Indexers cannot be static. At declaration, the parameters of an indexer are a comma-delimited list of indexes. The list includes the type and name of each parameter. Indexer indices can be nonintegral types, such as a string type or any reference type.

The following is example code for indexers. The Names type is a wrapper of an array of names and ages. The indexer for the Names type will provide access to this array. In this example, the indexer is read-only providing only read access to the internal array. The indexer in the example has a single parameter, which is an integer index. Flexibility is one of the benefits of indexers versus standard arrays. With a single indice, the indexer returns both the name and age of the employee. This task would be harder to accomplish with an array:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            Names obj = new Names();
            Console.WriteLine(obj[1]);
        }
    }

    public class Names {
        object[,] _names = {
             { "Valerie", 27 },
             { "Ben", 35 },
             { "Donis", 29 } };

        public object this[int index] {
            get {
                return _names[index,0] + " " + _names[index,1];
            }
        }
    }
}

Indexers can be overloaded based on the parameter list. Overloaded indexers should have a varying number of parameters, varying parameter types, or both. The following code overloads an indexer twice. The first indexer is read-only and returns the name and age information. The second indexer is a read-write property that sets and gets the age of a person. This indexer, which has a string parameter, demonstrates a non-numerical index:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            Names obj = new Names();
            obj["Donis"] = 42;
            Console.WriteLine(obj["Donis"]);
        }
    }

    public class Names {
        object [,] _names = {
             { "Valerie", 27 },
             { "Ben", 35 },
             { "Donis", 29 } };

        public object this[int index] {
            get {
                return _names[index,0] + " " + _names[index,1];
            }
        }

        public object this[string sIndex] {

            get {
                int index = FindName(sIndex);
                return _names[index, 1];
            }
            set {
                int index = FindName(sIndex);
                _names[index, 1] = value;
            }
        }

        private int FindName(string sIndex) {
            for (int index = 0; index < _names.GetLength(0);
                    ++index) {
                if ((string)(_names[index,0]) == sIndex) {
                    return index;
                }
            }
            throw new Exception("Name not found");
        }
    }
}

params Keyword

The params keyword is a parameter modifier. It indicates that the target parameter is a one-dimensional array of variable length. By extension, the keyword defines a variable-length parameter list. The params modifier can be applied only to the last parameter of a parameter list. Unlike standard parameters, the ref and out modifiers cannot be used with a params parameter.

Initialize the params argument with an implicit or explicit array. This is done at the call site. For implicit initialization, the C# compiler consumes for the array the optional parameters that follow the fixed parameter list. The fixed parameters are the parameters that precede the params-modified parameter in the function signature. The optional arguments are rolled into an array. If there are six optional arguments after the fixed arguments, an array of six elements is created and initialized. Alternatively, an explicit array can be given as the params argument. Finally, the params argument can be omitted in the method call. When omitted, the params argument is treated as an empty array. An empty array is different from a null array. Empty arrays have no elements but are valid instances.

In the following code, Names is a static method, which has a params parameter. The employees parameter is a single-dimensional string array. Consequently, the Names method has a variable-length parameter list:

public static void Names(string company,
    params string[] employees) {
    Console.WriteLine("{0} employees: ",
        company);
    foreach (string employee in employees) {
        Console.WriteLine("  {0}", employee);
    }
}

For a variable-length parameter list, the number of parameters is set at the call site. The following code shows the Names method being called with varying numbers of arguments. In both calls, the first parameter is consumed by the company parameter. The remaining arguments are used to create an array, which is assigned to the last parameter. A three-argument array is created for the first method call, whereas the second method has a six-argument array as the param argument:

Names("Fabrikam", "Fred", "Bob", "Alice");
Names("Contoso", "Sally", "Al", "Julia",
    "Will", "Sarah", "Terri");

The following code calls the Names method with an explicit array. This is identical to calling the method with four arguments:

Names("Fabrikam", new string[] {"Fred", "Bob",
             "Alice"});

In the following statement, the Names method is called without a params argument. For the omitted parameter, the compiler creates an array with no elements, which is subsequently passed to the method as the last parameter:

Names("Fabrikam");

Like any method, variable-length methods can be overloaded. You even can overload a method having a fixed number of parameters with a method having a variable number of parameters. Where there is ambiguity, the method with the fixed number of parameters is preferred and will be called.

In the following code, the Names method is overloaded with three methods. The first two overloads have a variable-length parameter list, whereas the final method has a fixed-length parameter list. In Main, the first two calls of the Names method are not ambiguous. The final call is ambiguous and can resolve to either the first or third overloaded method. The first overload of the method has a variable-length parameter list. Because the third overload has a fixed-length parameter list, it is preferred and will be called:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            Names("Fabrikam", "Fred", "Bob", "Alice");
            Names("Fabrikam", 1234, 5678, 9876, 4561);
            Names("Fabrikam", "Carter", "Deborah");
        }

        public static void Names(string company,
            params string[] employees) {
            Console.WriteLine("{0} employees: ",
                company);
            foreach(string employee in employees) {
                Console.WriteLine("  {0}", employee);
            }
        }

        public static void Names(string company,
            params int[] emplid) {
            Console.WriteLine("{0} employees: ",
                company);
            foreach (int employee in emplid) {
                Console.WriteLine("  {0}", employee);
            }
        }

        public static void Names(string company,
            string empl1, string empl2) {
            Console.WriteLine("{0} employees: ",
                company);
            Console.WriteLine("  {0}", empl1);
            Console.WriteLine("  {0}", empl2);
        }
    }
}

Array Conversion

You can cast between arrays. Arrays are implicit System.Array types, and therefore, regardless of type or the number of dimensions, any array can be cast to System.Array. All arrays are compatible with their underlying type.

When casting or converting between arrays, the source and destination array are required to have the same dimensions. In addition, an array of value types is convertible only to arrays of the same dimension and type. Arrays of reference types are somewhat more flexible. Arrays of reference types can be cast to arrays of the same or ascendant type. This is called array covariance. Array reference types are covariant, whereas arrays of value types are not.

Arrays can be used as function parameters and returns. In these circumstances, implicit casting may occur. Array covariance may also occur at this time.

Arrays as Function Returns and Parameters

Arrays used as a function argument are passed by reference. This is more efficient than placing a potentially large number of elements on the stack. As a reference type, the array state can be changed in the called method. Arrays used as parameters are normal parameters and support the regular assortment of modifiers. Of course, the array instance must be convertible to the type of array parameter.

In the following code, ZClass has two static methods, which both have an array parameter. The ListArray method has a System.Array parameter, which accepts any array as an argument. This allows the ListArray method to be called in Main with different types of array arguments. The Total method also has an array argument, which is an integer array. This means the argument must be an integer array:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            int[] zArray = { 10,9,8,7,6,5,4,3,2,1 };
            string[] xArray = { "a", "b", "c", "d" };
            Console.WriteLine("List Numbers");
            ZClass.ListArray(zArray);
            Console.WriteLine("List Letters");
            ZClass.ListArray(xArray);
            Console.WriteLine("Total Numbers");
            ZClass.Total(zArray);
        }
    }

    public class ZClass {

        public static void ListArray(Array a) {
            foreach( object element in a) {
                Console.WriteLine(element);
            }
        }

        public static void Total(int[] iArray) {
            int total = 0;
            foreach (int number in iArray) {
                total += number;
            }
            Console.WriteLine(total);
        }
    }
}

Arrays also can be returned from functions, which is one way to return more than a single item from a function. You can return multiple items as elements of an array. Arrays are not returned on the stack. Returning an array gives the calling function a reference to the array, which provides direct access to the array values.

..................Content has been hidden....................

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