Most of the examples in previous chapters have dealt with one object at a time. In many applications, however, you want to work with a collection of objects all at the same time. The simplest collection in C# is the array, the only collection type for which C# provides built-in support. The other collection types, such as stack and queue, are not part of the language; they are part of the Framework Class Library. The collection classes are covered in detail in Chapter 14. In this chapter, you will learn to work with three types of arrays : one-dimensional arrays, multidimensional rectangular arrays, and jagged arrays.
Before we get to generic syntax, it makes sense to begin with the simplest collection: the array. An array is an indexed collection of objects, all of the same type (all ints, all strings, and so on). C# provides native syntax for the declaration of arrays. To picture a one-dimensional array, imagine a series of mailboxes, all lined up one after the other, as shown in Figure 10-1. Each mailbox can hold exactly one object (letter, box, and so on). Each mailbox also has a number, so you can identify which item is in which box. Unlike real-world mailboxes, though, all the mailboxes must hold the same kind of object; you declare the type of object that the mailboxes will hold when you declare the array.
The important thing about arrays is that you can treat the entire array (the set of mailboxes) as a single entity, with a single name. As you’ll see, using loops, you can easily perform an operation on every element within an array in turn.
Declare a C# array with the following syntax:
type
[]array-name
;
For example:
int[] myIntArray;
You are not actually declaring an array. Technically, you are declaring a variable
(myIntArray
) that will hold a
reference to an array of integers. As always, we’ll use the
shorthand and refer to myIntArray
as the array, knowing that what we really mean is that it is a
variable that holds a reference to an (unnamed) array.
The square brackets ([]
) tell
the C# compiler that you are declaring an array, and the type
specifies the type of the elements it will contain. In the previous example, myIntArray
is an array of integers.
Instantiate an array using the new
keyword. For example:
myIntArray = new int[5];
This statement creates and intializes an array of five integers, all of which are initialized to the value zero.
It is important to distinguish between the array itself (which
is a collection) and the elements held in the array (which can be of
any type, so long as all the elements in the array are the same type).
myIntArray
is the array (or, more
accurately, the variable that holds the reference to the array); its
elements are the five integers it holds.
C# arrays are reference types, created on the heap. Thus, the
array to which the variable myIntArray
refers is allocated on the heap.
The elements of an array are allocated based on
their own type. Because integers are value types, the elements in
myIntArray
will be value types, and
thus all the elements will be created inside the block of memory
allocated for the array.
The block of memory allocated to an array of reference types will contain references to the actual elements, which are themselves created on the heap in memory separate from that allocated for the array.
When you create an array of value types, each element initially contains the default value for the type stored in the array (see Table 3-1). The statement:
myIntArray = new int[5];
creates an array of five integers, each of whose value is set to 0, which is the default value for integer types.
Unlike arrays of value types, the reference types in an array are not initialized to their default value. Instead, the references held in the array are initialized to null. If you attempt to access an element in an array of reference types before you have specifically initialized the elements, you will generate an exception.
Assume you have created a Button
class. Declare an array of Button
objects with the following
statement:
Button[] myButtonArray;
and instantiate the actual array like this:
myButtonArray = new Button[3];
You can shorten this to:
Button[] myButtonArray = new Button[3];
This statement does not create an array
with references to three Button
objects. Instead, this creates the array myButtonArray
with three null references. To
use this array, you must first construct and assign the Button
objects for each reference in the
array. You can construct the objects in a loop that adds them one by
one to the array, as you’ll see later in the chapter.
You can access the elements of an array using the index
operator ([]
). Arrays are
zero-based, which means that the index of the first element is always
zero—in this case, myArray[0]
.
The Length
property of the array tells you how many objects the
array holds. Therefore, objects are indexed from 0
to Length-1
.
Example 10-1
illustrates the array concepts covered so far. In this example, a
class named Tester
creates an array
of Employees
and an array of
integers, populates the Employee
array, and then prints the values of both.
using System; namespace Learning_CSharp { // a simple class to store in the array public class Employee { public Employee(int empID) { this.empID = empID; } public override string ToString( ) { return empID.ToString( ); } private int empID; } public class Tester { static void Main( ) { int[] intArray; Employee[] empArray; intArray = new int[5]; empArray = new Employee[3]; // populate the arrays for (int i = 0;i<intArray.Length;i++) { intArray[i] = i*2; } for (int i = 0;i<empArray.Length;i++) { empArray[i] = new Employee(i+1005); } // output array values Console.WriteLine("intArray values:"); for (int i = 0;i<intArray.Length;i++) { Console.WriteLine(intArray[i].ToString( )); } Console.WriteLine("\nemployee IDs:"); for (int i = 0;i<empArray.Length;i++) { Console.WriteLine(empArray[i].ToString( )); } } } }
The output looks like this:
intArray values: 0 2 4 6 8 employee IDs: 1005 1006 1007
The example starts with the definition of an Employee
class that implements a constructor
that takes a single integer parameter. Employee
implements the ToString( )
method to print the value of the
Employee
object’s employee
ID.
The test method declares and then instantiates a pair of arrays.
Initially, the integer array is automatically filled with integers
whose value is set to 0, but then you populate it with a for
loop. The loop has a counter, i
, and you iterate
through the loop by setting each element of the array as the index
value (i
) increases. You set the
value in intArray
with this
statement:
intArray[i] = i*2;
The first time through the loop, i
is equal to 0, and intArray[0]
is set to 0*2
, which is 0. The next time through,
i
is 1, and the second element
(intArray[1]
) is set to the value 2
(1*2
). The loop runs until i
is equal to intArray.Length
, at which point it stops.
Note that i
is equal to intArray.Length - 1
on the last time through
the loop, which is the index of the last element in the array.
It’s not hard to remember that arrays start at 0, but it can
be tricky to remember that the highest index is actually
one less than Length
. When you use a loop to iterate
through your array, always make sure that your condition for ending
the loop is counter < Array.Length
, not counter <= Array.Length
.
C# is smarter than its C++ and C ancestors; if you do try to write past the end of the array, rather than trampling on random memory, you’ll get an “IndexOutOfRange” exception, which will help you find the error quickly and reliably.
In any case, you want always to be on the lookout for what programmers call the “off-by-one” error. A famous example of this is the fence-post error. Imagine you are building a picket fence, where each horizontal board is one foot long. How many uprights do you need to build a 10-foot fence?[6]
The Employee
array contents
must be constructed by hand, because the values of reference types are
not initialized when the array is created (the array is filled with
null values). You assign values to the elements of empArray
the same
way you did intArray
, with a
for
loop. This time, though, you
have to create a new Employee
object to add to each index in the array
Finally, the contents of the arrays are displayed to ensure that
they are filled as intended, using the same for
loop technique. The five integers print
their value first, followed by the three Employee
objects.
The foreach
statement
allows you to iterate through all the items in an array or other
collection, examining each item in turn. The syntax for the foreach
statement is:
foreach (type identifier
inexpression
)statement
You can update Example
10-1 to replace the final two for
statements (that iterate over the contents
of the populated array) with foreach
statements, as shown in Example
10-2.
foreach ( int i in intArray ) { Console.WriteLine( i.ToString( ) ); } foreach ( Employee e in empArray ) { Console.WriteLine( e.ToString( ) ); }
The output will be identical.
You can initialize the contents of an array at the time it is
instantiated by providing a list of values delimited by curly brackets
({}
). C# provides a longer and a
shorter syntax:
int[] myIntArray = new int[5] { 2, 4, 6, 8, 10 }; int[] myIntArray = { 2, 4, 6, 8, 10 };
In the shorter syntax, C# automatically creates an array of the proper size for the number of elements in the brackets. There is no practical difference between these two statements, and most programmers will use the shorter syntax.
The params
keyword allows you to pass in a variable number of
parameters of the same type to a method. What the method receives is an
array of that type.
In the next example, you create a method, DisplayVals( )
, that takes a variable number of integer
arguments:
public void DisplayVals(params int[] intVals)
You are free to iterate over the array as you would over any other array of integers:
foreach (int i in intVals) { Console.WriteLine("DisplayVals {0}",i); }
The calling method, however, need not explicitly create an array:
it can simply pass in integers, and the compiler will assemble the
parameters into an array for the DisplayVals( )
method:
t.DisplayVals(5,6,7,8);
You are free to pass in an array if you prefer:
int [] explicitArray = new int[5] {1,2,3,4,5}; t.DisplayVals(explicitArray);
You can only use one params
argument for each method you create, and the params
argument must be the last argument in
the method’s signature.
Example 10-3
illustrates using the params
keyword.
using System; namespace UsingParams { public class Tester { static void Main( ) { Tester t = new Tester( ); t.DisplayVals(5,6,7,8); int [] explicitArray = new int[] {1,2,3,4,5}; t.DisplayVals(explicitArray); } public void DisplayVals(params int[] intVals) { foreach (int i in intVals) { Console.WriteLine("DisplayVals {0}",i); } } } }
The output looks like this:
DisplayVals 5 DisplayVals 6 DisplayVals 7 DisplayVals 8 DisplayVals 1 DisplayVals 2 DisplayVals 3 DisplayVals 4 DisplayVals 5
Arrays can be thought of as long rows of slots into which values can be placed. Once you have a picture of a row of slots, imagine 10 rows, one on top of another. This is the classic two-dimensional array of rows and columns. The rows run across the array and the columns run up and down the array, as shown in Figure 10-2.
A third dimension is a bit harder to imagine. Okay, now imagine four dimensions. Now imagine 10.
Those of you who are not string-theory physicists have probably given up, as have I. Multidimensional arrays are useful, however, even if you can’t quite picture what they would look like.
C# supports two types of multidimensional arrays: rectangular and jagged. In a rectangular array, every row is the same length. A jagged array, however, is an array of arrays, each of which can be a different length.
A rectangular array is an array of two (or more) dimensions. In the classic two-dimensional array, the first dimension is the number of rows and the second dimension is the number of columns.
To declare a two-dimensional array, use the following syntax:
type
[,]array-name
For example, to declare and instantiate a two-dimensional
rectangular array named myRectangularArray
that contains two rows
and three columns of integers, you would write:
int [,] myRectangularArray = new int[2,3];
Example 10-4
declares, instantiates, initializes, and prints the contents of a
two-dimensional array. In this example, a for
loop is used to initialize the elements
of the array.
using System; namespace RectangularArray { public class Tester { static void Main( ) { const int rows = 4; const int columns = 3; // declare a 4x3 integer array int[,] rectangularArray = new int[rows, columns]; // populate the array for ( int i = 0; i < rows; i++ ) { for ( int j = 0; j < columns; j++ ) { rectangularArray[i, j] = i + j; } } // report the contents of the array for ( int i = 0; i < rows; i++ ) { for ( int j = 0; j < columns; j++ ) { Console.WriteLine( "rectangularArray[{0},{1}] = {2}", i, j, rectangularArray[i, j] ); } } } } }
The output looks like this:
rectangularArray[0,0] = 0 rectangularArray[0,1] = 1 rectangularArray[0,2] = 2 rectangularArray[1,0] = 1 rectangularArray[1,1] = 2 rectangularArray[1,2] = 3 rectangularArray[2,0] = 2 rectangularArray[2,1] = 3 rectangularArray[2,2] = 4 rectangularArray[3,0] = 3 rectangularArray[3,1] = 4 rectangularArray[3,2] = 5
The brackets in the int[,]
declaration indicate that the type is an array of integers, and the
comma indicates the array has two dimensions (two commas would
indicate three dimensions, and so on). The actual instantiation of
rectangularArray
with new int[rows, columns]
sets the size of each
dimension. Here, the declaration and instantiation have been
combined.
The program fills the rectangle with a pair of nested for
loops, iterating through each column in
each row. Thus, the first element filled is rectangularArray[0,0]
, followed by rectangularArray[0,1]
and rectangularArray[0,2]
. Once this is done,
the program moves on to the next rows: rectangularArray[1,0]
, rectangularArray[1,1]
, rectangularArray[1,2]
, and so forth, until
all the columns in all the rows are filled.
Just as you can initialize a one-dimensional array using
bracketed lists of values, you can initialize a two-dimensional array
using similar syntax. Example
10-5 declares a two-dimensional array (rectangularArray
), initializes its elements
using bracketed lists of values, and then prints out the
contents.
using System; namespace InitializingMultiDimensionalArray { public class Tester { static void Main( ) { const int rows = 4; const int columns = 3; // imply a 4x3 array int[,] rectangularArray = { {0,1,2}, {3,4,5}, {6,7,8}, {9,10,11} }; for ( int i = 0; i < rows; i++ ) { for ( int j = 0; j < columns; j++ ) { Console.WriteLine( "rectangularArray[{0},{1}] = {2}", i, j, rectangularArray[i, j] ); } } } } }
The output looks like this:
rectangularArrayrectangularArray[0,0] = 0 rectangularArrayrectangularArray[0,1] = 1 rectangularArrayrectangularArray[0,2] = 2 rectangularArrayrectangularArray[1,0] = 3 rectangularArrayrectangularArray[1,1] = 4 rectangularArrayrectangularArray[1,2] = 5 rectangularArrayrectangularArray[2,0] = 6 rectangularArrayrectangularArray[2,1] = 7 rectangularArrayrectangularArray[2,2] = 8 rectangularArrayrectangularArray[3,0] = 9 rectangularArrayrectangularArray[3,1] = 10 rectangularArrayrectangularArray[3,2] = 11
The preceding example is very similar to Example 10-4, but this time you imply the exact dimensions of the array by how you initialize it:
int[,] rectangularArrayrectangularArray = { {0,1,2}, {3,4,5}, {6,7,8}, {9,10,11} };
Assigning values in four bracketed lists, each consisting of three elements, implies a 4 (rows) by 3 (columns) array.
Had you written this as:
int[,] rectangularArrayrectangularArray = { {0,1,2,3}, {4,5,6,7}, {8,9,10,11} };
you would instead have implied a 3 by 4 array.
You can see that the C# compiler understands the implications of the way you grouped the input values, because it is able to access the objects with the appropriate offsets, as illustrated in the output.
C# arrays are “smart” and they keep track of their bounds. When you imply a 4 × 3 array, you must treat it as such, and not as a 3 × 4 array, or just an array of 12 integers (as you can with some other C-family languages).
A jagged array is an array of arrays. It is called “jagged” because each of the rows need not be the same size as all the others, and thus a graphical representation of the array would not be square.
When you create a jagged array, you declare the number of rows in your array. Each row will hold an array, which can be of any length. These arrays must each be declared. You can then fill in the values for the elements in these “inner” arrays.
In a jagged array, each dimension is a one-dimensional array. To declare a jagged array, use the following syntax, where the number of brackets indicates the number of dimensions of the array:
type
[] []...
For example, you would declare a two-dimensional jagged array of
integers named myJaggedArray
, as
follows:
int [] [] myJaggedArray;
Access the fifth element of the third array by writing myJaggedArray[2][4]
.
Remember that in all arrays, the first element is at offset 0 and the nth element is at offset n - 1; thus, the seventh element is at offset 6.
Example 10-6
creates a jagged array named myJaggedArray
, initializes its elements, and
then prints their content. To save space, the program takes advantage
of the fact that integer array elements are automatically initialized
to zero, and it initializes the values of only some of the
elements.
using System; namespace JaggedArray { public class Tester { static void Main( ) { const int rows = 4; // declare the jagged array as 4 rows high int[][] jaggedArray = new int[rows][]; // the first row has 5 elements jaggedArray[0] = new int[5]; // a row with 2 elements jaggedArray[1] = new int[2]; // a row with 3 elements jaggedArray[2] = new int[3]; // the last row has 5 elements jaggedArray[3] = new int[5]; // Fill some (but not all) elements of the rows jaggedArray[0][3] = 15; jaggedArray[1][1] = 12; jaggedArray[2][1] = 9; jaggedArray[2][2] = 99; jaggedArray[3][0] = 10; jaggedArray[3][1] = 11; jaggedArray[3][2] = 12; jaggedArray[3][3] = 13; jaggedArray[3][4] = 14; for ( int i = 0; i < 5; i++ ) { Console.WriteLine( "jaggedArray[0][{0}] = {1}", i, jaggedArray[0][i] ); } for ( int i = 0; i < 2; i++ ) { Console.WriteLine( "jaggedArray[1][{0}] = {1}", i, jaggedArray[1][i] ); } for ( int i = 0; i < 3; i++ ) { Console.WriteLine( "jaggedArray[2][{0}] = {1}", i, jaggedArray[2][i] ); } for ( int i = 0; i < 5; i++ ) { Console.WriteLine( "jaggedArray[3][{0}] = {1}", i, jaggedArray[3][i] ); } } } }
The output looks like this:
jaggedArray[0][0] = 0 jaggedArray[0][1] = 0 jaggedArray[0][2] = 0 jaggedArray[0][3] = 15 jaggedArray[0][4] = 0 jaggedArray[1][0] = 0 jaggedArray[1][1] = 12 jaggedArray[2][0] = 0 jaggedArray[2][1] = 9 jaggedArray[2][2] = 99 jaggedArray[3][0] = 10 jaggedArray[3][1] = 11 jaggedArray[3][2] = 12 jaggedArray[3][3] = 13 jaggedArray[3][4] = 14
In this example, a jagged array is created with four rows:
int[][] jaggedArray = new int[rows][];
Notice that the second dimension is not specified. This is set by creating a new array for each row. Each of these arrays can have a different size:
// the first row has 5 elements jaggedArray[0] = new int[5]; // a row with 2 elements jaggedArray[1] = new int[2]; // a row with 3 elements jaggedArray[2] = new int[3]; // the last row has 5 elements jaggedArray[3] = new int[5];
Once an array is specified for each row, you need only populate the various members of each array and then print out their contents to ensure that all went as expected.
Another way of outputting the values would be to use two nested
for
loops, and use the Length
property of the array to control the
loop:
for (int i = 0; i < jaggedArray.Length; i++ ) { for (int j = 0; j < jaggedArray[i].Length; j++) { Console.WriteLine("jaggedArray[{0}][{1}] = {2}", i, j, jaggedArray[i][j]); } }
In this case, the “outer” for
loop iterates over the rows in the array. The “inner” loop outputs
each column in the given row. Because you’re using Length
to control how many times the loop
runs, it doesn’t matter that each row is a different length.
Notice that when you access the members of the rectangular array, you put the indexes all within one set of square brackets:
rectangularArrayrectangularArray[i,j]
while with a jagged array, you need a pair of brackets:
jaggedArray[i][j]
You can keep this straight by thinking of the first as a single array of more than one dimension and the jagged array as an array of arrays .
Although you’ve been using arrays as built-in types
throughout this chapter, an array is actually an object of type System.Array
.[7] Arrays in C# thus provide you with the best of both
worlds: easy-to-use syntax underpinned with an actual class definition
so that instances of an array have access to the methods and properties of System.Array
. You’ve seen the Length
property of arrays used several times
already. Some of the other important methods and properties appear in
Table 10-1.
Method or property | Purpose |
Overloaded public static method that searches a one-dimensional sorted array | |
Public static method that sets a range of elements in the array either to zero or to a null reference | |
Overloaded public static method that copies a section of one array to another array | |
| Overloaded public static method that instantiates a new instance of an array |
Overloaded public static method that returns the index (offset) of the first instance of a value in a one-dimensional array | |
Overloaded public static method that returns the index of the last instance of a value in a one-dimensional array | |
Overloaded public static method that reverses the order of the elements in a one-dimensional array | |
Overloaded public static method that sorts the values in a one-dimensional array | |
| |
Public method that returns an |
Two useful static methods from Table 10-1 that deserve a
closer look are Sort( )
and Reverse( )
. These methods do what you think
they would: Reverse( )
reverses the
order of elements in the array, and Sort( )
sorts the elements in order. These are fully supported for
arrays of the built-in C# types, such as string
, so sorting an array of strings puts the elements in alphabetical
order, and sorting an array of ints puts them in numeric order. Making
the Sort( )
method work with your own
classes is a bit trickier, as you must implement the IComparable
interface (see Chapter 13 for more on interfaces).
Example 10-7
demonstrates the use of these two methods to manipulate String
objects.
using System; namespace ArraySortAndReverse { public class Tester { public static void PrintMyArray( string[] theArray ) { foreach ( string str in theArray ) { Console.WriteLine( "Value: {0}", str ); } Console.WriteLine( "\n" ); } static void Main( ) { String[] myArray = { "Proust", "Faulkner", "Mann", "Hugo" }; PrintMyArray( myArray ); Array.Reverse( myArray ); PrintMyArray( myArray ); String[] myOtherArray = { "We", "Hold", "These", "Truths", "To", "Be", "Self","Evident", }; PrintMyArray( myOtherArray ); Array.Sort( myOtherArray ); PrintMyArray( myOtherArray ); } } }
The output looks like this:
Value: Proust Value: Faulkner Value: Mann Value: Hugo Value: Hugo Value: Mann Value: Faulkner Value: Proust Value: We Value: Hold Value: These Value: Truths Value: To Value: Be Value: Self Value: Evident Value: Be Value: Evident Value: Hold Value: Self Value: These Value: To Value: Truths Value: We
The example begins by creating myArray
, an array of strings with the
words:
"Proust", "Faulkner", "Mann", "Hugo"
This array is printed, and then passed to the Array.Reverse( )
method, where it is printed
again to see that the array itself has been reversed:
Value: Hugo Value: Mann Value: Faulkner Value: Proust
Similarly, the example creates a second array, myOtherArray
, containing the words:
"We", "Hold", "These", "Truths", "To", "Be", "Self", "Evident",
This is passed to the Array.Sort( )
method. Then Array.Sort( )
happily sorts them alphabetically:
Value: Be Value: Evident Value: Hold Value: Self Value: These Value: To Value: Truths Value: We
Array.Sort( )
and Array.Reverse( )
are static
methods, meaning you call them on the
class, not the object, as discussed in Chapter 7. That means you don’t
call myArray.Reverse( )
to reverse
the elements; instead, you call the static method and pass in the
array as an argument, like this:
Array.Reverse(myArray);
An array is an indexed collection of objects, all of the same type.
You declare an array by giving the type of objects the array
contains, followed by the square bracket operator ([]
), followed by the name of the array.
You then instantiate the array with the new
keyword and the number of elements the
array will contain.
The index of the first element in the array is always zero,
and the index of the last element in the array is always Length-1
.
You can use a for
loop to
iterate through the array, by using the loop’s counter as the index
to the array.
The foreach
statement
allows you to iterate through the items in the array (or any other
collection) without the need for a counter.
The elements of an array can be initialized when the array is
created by providing the values of the members in curly braces
({}
).
The params
keyword lets you
pass an arbitrary number of parameters of the same type into a
method; the method will treat the parameters as a single
array.
Arrays can contain more than one dimension. A two-dimensional array has two indexes, which you can think of as rows and columns.
A rectangular array is a two-dimensional array in which all the rows have the same number of columns.
A jagged array is an array of arrays—the rows do not need to be all the same length.
The Length
property of an
array returns the total number of elements in the array.
The array class contains a number of methods for searching, sorting, and manipulating the elements.
What is the index of the seventh member of an array?
Are arrays type-safe?
Are arrays reference or value types? And where are they created?
Where are the elements of the array created?
What are the two ways to initialize an array of three values?
What does the params
keyword do?
What are the two types of multidimensional arrays and what is the difference between them?
Declare a Dog
class with
two private members: weight
(an
int
), and name
(a string
). Be sure to add properties to
access the members. Then create an array that holds three Dog
objects (Milo, 26 pounds; Frisky, 10 pounds; and Laika, 50
pounds). Output each dog’s name and weight.
Create an array of 10 integers. Populate the array by having
the user enter integers at the console (use Console.Readline
). Don’t worry about
error-checking for this exercise. Output the integers sorted from
greatest to least.
Extend Exercise 10-1 by creating a two-dimensional array that represents a collection of strings that indicate the awards each dog has won at dog shows. Each dog may have a different number of awards won. Output the contents of the array to check its validity.
Create a two-dimensional array that represents a chessboard (an 8 × 8 array). Each element in the array should contain either the string “black” or “white,” depending on where it is on the board. Create a method that initializes the array with the strings. Then create a method that asks the reader to enter two integers for the coordinates of a square, and returns whether that square is black or white.
[6] It is tempting to say 10 uprights for a 10-foot fence. But how many do you need for a one-foot fence? You need two, one on each end of the one-foot board. For a two-foot fence, you need three. You always need that extra upright, and so for a 10-foot fence, you need 11 uprights. Watch out for off-by-one errors; they come up a lot.
[7] Of course, when you create an array with int[] myArray = new int[5]
, what you
actually create in the IL code is an instance of System.int32[]
. Because this derives from
the Abstract Base Class System.Array
, however, it is fair to say
you’ve created an instance of a System.Array
.