The Array class is the implicit base class for all single and multidimensional arrays,
and it is one of the most fundamental types implementing the standard collection
interfaces. The Array class provides type unification, so a common set of methods
is available to all arrays, regardless of their declaration or underlying element type.
Since arrays are so fundamental, C# provides explicit syntax for their declaration
and initialization, described in Chapters 2 and 3. When an array is declared using
C#’s syntax, the CLR implicitly subtypes the Array class—synthesizing a pseudotype appropriate to the array’s dimensions and element types. This pseudotype
implements the typed generic collection interfaces, such as IList<string>.
The CLR also treats array types specially upon construction, assigning them a contiguous
space in memory. This makes indexing into arrays highly efficient, but
prevents them from being resized later on.
Array implements the collection interfaces up to IList<T> in both their generic and
nongeneric forms. IList<T> itself is implemented explicitly, though, to keep
Array’s public interface clean of methods such as Add or Remove, which throw an
exception on fixed-length collections such as arrays. The Array class does actually
offer a static Resize method, although this works by creating a new array and then
copying over each element. As well as being inefficient, references to the array elsewhere
in the program will still point to the original version. A better solution for
resizable collections is to use the List<T> class (described in the following section).
An array can contain value type or reference type elements. Value type elements are
stored in place in the array, so an array of three long integers (each 8 bytes) will
occupy 24 bytes of contiguous memory. A reference type element, however, occupies
only as much space in the array as a reference (4 bytes in a 32-bit environment or 8
bytes in a 64-bit environment). Figure 7-2 illustrates the effect, in memory, of the
following program:
StringBuilder[] builders = new StringBuilder [5];
builders [0] = new StringBuilder ("builder1");
builders [1] = new StringBuilder ("builder2");
builders [2] = new StringBuilder ("builder3");
long[] numbers = new long [3];
numbers [0] = 12345;
numbers [1] = 54321;
Because Array is a class, arrays are always (themselves) reference types—regardless
of the array’s element type. This means that the statement arrayB = arrayA results
in two variables that reference the same array. Similarly, two distinct arrays will
always fail an equality test—unless you use a custom equality comparer. Framework
4.0 provides one for the purpose of comparing elements in arrays or tuples which
you can access via the StructuralComparisons type:
object[] a1 = { "string", 123, true };
object[] a2 = { "string", 123, true };
Console.WriteLine (a1 == a2); // False
Console.WriteLine (a1.Equals (a2)); // False
Console.WriteLine (a1.Equals (a2,
StructuralComparisons.StructuralEqualityComparer)); // True
Arrays can be duplicated with the Clone method: arrayB = arrayA.Clone(). However,
this results in a shallow clone, meaning that only the memory represented by
the array itself is copied. If the array contains value type objects, the values themselves
are copied; if the array contains reference type objects, just the references are
copied (resulting in two arrays whose members reference the same objects). Figure
7-3 demonstrates the effect of adding the following code to our example:
StringBuilder[] builders2 = builders;
StringBuilder[] shallowClone = (StringBuilder[]) builders.Clone();
To create a deep copy—where reference type subobjects are duplicated—you must
loop through the array and clone each element manually. The same rules apply to
other .NET collection types.
Although Array is designed primarily for use with 32-bit indexers, it also has limited
support for 64-bit indexers (allowing an array to theoretically address up to 264
elements) via several methods that accept both Int32 and Int64 parameters. These
overloads are useless in practice, because the CLR does not permit any object—
including arrays—to exceed 2GB in size (whether running on a 32- or 64-bit
environment).
Many of the methods on the Array class that you expect to be
instance methods are in fact static methods. This is an odd design
decision, and means you should check for both static and
instance methods when looking for a method on Array.
Construction and Indexing
The easiest way to create and index arrays is through C#’s language constructs:
int[] myArray = { 1, 2, 3 };
int first = myArray [0];
int last = myArray [myArray.Length - 1];
Alternatively, you can instantiate an array dynamically by calling Array.CreateIn
stance. This allows you to specify element type and rank (number of dimensions)
at runtime, as well as allowing nonzero-based arrays through specifying a lower
bound. Nonzero-based arrays are not CLS (Common Language Specification)-
compliant.
The static GetValue and SetValue methods let you access elements in a dynamically
created array (they also work on ordinary arrays):
// Create a string array 2 elements in length:
Array a = Array.CreateInstance (typeof(string), 2);
a.SetValue ("hi", 0); // → a[0] = "hi";
a.SetValue ("there", 1); // → a[1] = "there";
string s = (string) a.GetValue (0); // → s = a[0];
// We can also cast to a C# array as follows:
string[] cSharpArray = (string[]) a;
string s2 = cSharpArray [0];
Zero-indexed arrays created dynamically can be cast to a C# array of a matching or
compatible type (compatible by standard array-variance rules). For example, if
Apple subclasses Fruit, Apple[] can be cast to Fruit[]. This leads to the issue of why
object[] was not used as the unifying array type rather the Array class. The answer
is that object[] is incompatible with both multidimensional and value-type arrays
(and nonzero-based arrays). An int[] array cannot be cast to object[]. Hence, we
require the Array class for full type unification.
GetValue and SetValue also work on compiler-created arrays, and they are useful
when writing methods that can deal with an array of any type and rank. For multidimensional
arrays, they accept an array of indexers:
public object GetValue (params int[] indices)
public void SetValue (object value, params int[] indices)
The following method prints the first element of any array, regardless of rank:
void WriteFirstValue (Array a)
{
Console.Write (a.Rank + "-dimensional; ");
// The indexers array will automatically initialize to all zeros, so
// passing it into GetValue or SetValue will get/set the zero-based
// (i.e., first) element in the array.
int[] indexers = new int[a.Rank];
Console.WriteLine ("First value is " + a.GetValue (indexers));
}
void Demo()
{
int[] oneD = { 1, 2, 3 };
int[,] twoD = { {5,6}, {8,9} };
WriteFirstValue (oneD); // 1-dimensional; first value is 1
WriteFirstValue (twoD); // 2-dimensional; first value is 5
}
For working with arrays of unknown type but known rank, generics
provide an easier and more efficient solution:
void WriteFirstValue<T> (T[] array)
{
Console.WriteLine (array[0]);
}
SetValue throws an exception if the element is of an incompatible type for the array.
When an array is instantiated, whether via language syntax or Array.CreateIn
stance, its elements are automatically initialized. For arrays with reference type elements,
this means writing nulls; for arrays with value type elements, this means
calling the value type’s default constructor (effectively “zeroing” the members). The
Array class also provides this functionality on demand via the Clear method:
public static void Clear (Array array, int index, int length);
This method doesn’t affect the size of the array. This is in contrast to the usual use
of Clear (such as in ICollection<T>.Clear ), where the collection is reduced to zero
elements.