This chapter returns to the subject of C#’s data types. It discusses arrays and the string type. The foreach loop is also examined.
An array is a collection of variables of the same type that are referred to by a common name. In C#, arrays can have one or more dimensions, although the one-dimensional array is the most common. Arrays are used for a variety of purposes because they offer a convenient means of grouping together related variables. For example, you might use an array to hold a record of the daily high temperature for a month, a list of stock prices, or your collection of programming books.
The principal advantage of an array is that it organizes data in such a way that it can be easily manipulated. For example, if you have an array containing the dividends for a selected group of stocks, it is easy to compute the average income by cycling through the array. Also, arrays organize data in such a way that it can be easily sorted.
Although arrays in C# can be used just like arrays in many other programming languages, they have one special attribute: They are implemented as objects. This fact is one reason that a discussion of arrays was deferred until objects had been introduced. By implementing arrays as objects, several important advantages are gained, not the least of which is that unused arrays can be garbage-collected.
A one-dimensional array is a list of related variables. Such lists are common in programming. For example, you might use a one-dimensional array to store the account numbers of the active users on a network. Another array might store the current batting averages for a baseball team.
Because arrays in C# are implemented as objects, two steps are needed to obtain an array for use in your program. First, you must declare a variable that can refer to an array. Second, you must create an instance of the array by use of new. Therefore, to declare a one-dimensional array, you will typically use this general form:
type[ ] array-name = new type[size];
Here, type declares the element type of the array. The element type determines the data type of each element that comprises the array. Notice the square brackets that follow type. They indicate that a one-dimensional array is being declared. The number of elements that the array will hold is determined by size.
NOTE If you come from a C or C++ background, pay special attention to the way arrays are declared. Specifically, the square brackets follow the type name, not the array name.
Here is an example. The following creates an int array of ten elements and links it to an array reference variable named sample.
int[] sample = new int[10];
The sample variable holds a reference to the memory allocated by new. This memory is large enough to hold ten elements of type int.
As is the case when creating an instance of a class, it is possible to break the preceding declaration in two. For example:
int[] sample;
sample = new int[10];
In this case, when sample is first created, it refers to no physical object. It is only after the second statement executes that sample refers to an array.
An individual element within an array is accessed by use of an index. An index describes the position of an element within an array. In C#, all arrays have 0 as the index of their first element. Because sample has 10 elements, it has index values of 0 through 9. To index an array, specify the number of the element you want, surrounded by square brackets. Thus, the first element in sample is sample[0], and the last element is sample[9]. For example, the following program loads sample with the numbers 0 through 9:
// Demonstrate a one-dimensional array.
using System;
class ArrayDemo {
static void Main() {
int[] sample = new int[10];
int i;
for(i = 0; i < 10; i = i+1)
sample[i] = i;
for(i = 0; i < 10; i = i+1)
Console.WriteLine("sample[" + i + "]: " + sample[i]);
}
}
The output from the program is shown here:
sample[0]: 0
sample[1]: 1
sample[2]: 2
sample[3]: 3
sample[4]: 4
sample[5]: 5
sample[6]: 6
sample[7]: 7
sample[8]: 8
sample[9]: 9
Conceptually, the sample array looks like this:
Arrays are common in programming because they let you deal easily with large numbers of related variables. For example, the following program finds the average of the set of values stored in the nums array by cycling through the array using a for loop:
// Compute the average of a set of values.
using System;
class Average {
static void Main() {
int[] nums = new int[10];
int avg = 0;
nums[0] = 99;
nums[1] = 10;
nums[2] = 100;
nums[3] = 18;
nums[4] = 78;
nums[5] = 23;
nums[6] = 63;
nums[7] = 9;
nums[8] = 87;
nums[9] = 49;
for(int i=0; i < 10; i++)
avg = avg + nums[i];
avg = avg / 10;
Console.WriteLine("Average: " + avg);
}
}
The output from the program is shown here:
Average: 53
In the preceding program, the nums array was given values by hand, using ten separate assignment statements. While that is perfectly correct, there is an easier way to accomplish this. Arrays can be initialized when they are created. The general form for initializing a one-dimensional array is shown here:
type[ ] array-name = { val1, val2, val3, ..., valN };
Here, the initial values are specified by val1 through valN. They are assigned in sequence, left to right, in index order. C# automatically allocates an array large enough to hold the initializers that you specify. There is no need to use the new operator explicitly. For example, here is a better way to write the Average program:
// Compute the average of a set of values.
using System;
class Average {
static void Main() {
int[] nums = { 99, 10, 100, 18, 78, 23,
63, 9, 87, 49 };
int avg = 0;
for(int i=0; i < 10; i++)
avg = avg + nums[i];
avg = avg / 10;
Console.WriteLine("Average: " + avg);
}
}
As a point of interest, although not needed, you can use new when initializing an array. For example, this is a proper, but redundant, way to initialize nums in the foregoing program:
int[] nums = new int[] { 99, 10, 100, 18, 78, 23,
63, 9, 87, 49 };
Although redundant here, the new form of array initialization is useful when you are assigning a new array to an already-existent array reference variable. For example:
int[] nums;
nums = new int[] { 99, 10, 100, 18, 78, 23,
63, 9, 87, 49 };
In this case, nums is declared in the first statement and initialized by the second.
One last point: It is permissible to specify the array size explicitly when initializing an array, but the size must agree with the number of initializers. For example, here is another way to initialize nums:
int[] nums = new int[10] { 99, 10, 100, 18, 78, 23,
63, 9, 87, 49 };
In this declaration, the size of nums is explicitly stated as 10.
Array boundaries are strictly enforced in C#; it is a runtime error to overrun or underrun the ends of an array. If you want to confirm this for yourself, try the following program that purposely overruns an array:
// Demonstrate an array overrun.
using System;
class ArrayErr {
static void Main() {
int[] sample = new int[10];
int i;
// Generate an array overrun.
for(i = 0; i < 100; i = i+1)
sample[i] = i;
}
}
As soon as i reaches 10, an IndexOutOfRangeException is generated and the program is terminated. (See Chapter 13 for a discussion of exceptions and exception handling.)
Although the one-dimensional array is the most commonly used array in programming, multidimensional arrays are certainly not rare. A multidimensional array is an array that has two or more dimensions, and an individual element is accessed through the combination of two or more indices.
The simplest form of the multidimensional array is the two-dimensional array. In a two-dimensional array, the location of any specific element is specified by two indices. If you think of a two-dimensional array as a table of information, one index indicates the row, the other indicates the column.
To declare a two-dimensional integer array table of size 10, 20, you would write
int[,] table = new int[10, 20];
Pay careful attention to the declaration. Notice that the two dimensions are separated from each other by a comma. In the first part of the declaration, the syntax
[,]
indicates that a two-dimensional array reference variable is being created. When memory is actually allocated for the array using new, this syntax is used:
int[10, 20]
This creates a 10×20 array, and again, the comma separates the dimensions.
To access an element in a two-dimensional array, you must specify both indices, separating the two with a comma. For example, to assign the value 10 to location 3, 5 of array table, you would use
table[3, 5] = 10;
Here is a complete example. It loads a two-dimensional array with the numbers 1 through 12 and then displays the contents of the array.
// Demonstrate a two-dimensional array.
using System;
class TwoD {
static void Main() {
int t, i;
int[,] table = new int[3, 4];
for(t=0; t < 3; ++t) {
for(i=0; i < 4; ++i) {
table[t, i] = (t*4)+i+1;
Console.Write(table[t,i] + " ");
}
Console.WriteLine();
}
}
}
In this example, table[0, 0] will have the value 1, table[0, 1] the value 2, table[0, 2] the value 3, and so on. The value of table[2, 3] will be 12. Conceptually, the array will look like the one shown in Figure 7-1.
NOTE If you have previously programmed in C, C++, or Java, be careful when declaring or accessing multidimensional arrays in C#. In these other languages, array dimensions and indices are specified within their own set of brackets. C# separates dimensions using commas.
C# allows arrays with more than two dimensions. Here is the general form of a multidimensional array declaration:
type[, ...,] name = new type[size1, size2, ..., sizeN];
FIGURE 7-1 A conceptual view of the table array created by the TwoD program
For example, the following declaration creates a 4×10×3 three-dimensional integer array:
int[,,] multidim = new int[4, 10, 3];
To assign element 2, 4, 1 of multidim the value 100, use this statement:
multidim[2, 4, 1] = 100;
Here is a program that uses a three-dimensional array that holds a 3×3×3 matrix of values. It then sums the value on one of the diagonals through the cube.
// Sum the values on a diagonal of a 3x3x3 matrix.
using System;
class ThreeDMatrix {
static void Main() {
int[,,] m = new int[3, 3, 3];
int sum = 0;
int n = 1;
for(int x=0; x < 3; x++)
for(int y=0; y < 3; y++)
for(int z=0; z < 3; z++)
m[x, y, z] = n++;
sum = m[0, 0, 0] + m[1, 1, 1] + m[2, 2, 2];
Console.WriteLine("Sum of first diagonal: " + sum);
}
}
The output is shown here:
Sum of first diagonal: 42
A multidimensional array can be initialized by enclosing each dimension’s initializer list within its own set of curly braces. For example, the general form of array initialization for a two-dimensional array is shown here:
type[,] array_name = {
{ val, val, val, ..., val },
{ val, val, val, ..., val },
.
.
.
{ val, val, val, ..., val }
};
Here, val indicates an initialization value. Each inner block designates a row. Within each row, the first value will be stored in the first position, the second value in the second position, and so on. Notice that commas separate the initializer blocks and that a semicolon follows the closing }.
For example, the following program initializes an array called sqrs with the numbers 1 through 10 and their squares.
// Initialize a two-dimensional array.
using System;
class Squares {
static void Main() {
int[,] sqrs = {
{ 1, 1 },
{ 2, 4 },
{ 3, 9 },
{ 4, 16 },
{ 5, 25 },
{ 6, 36 },
{ 7, 49 },
{ 8, 64 },
{ 9, 81 },
{ 10, 100 }
};
int i, j;
for(i=0; i < 10; i++) {
for(j=0; j < 2; j++)
Console.Write(sqrs[i,j] + " ");
Console.WriteLine();
}
}
}
Here is the output from the program:
1 1
2 4
3 9
4 16
5 25
6 36
7 49
8 64
9 81
10 100
In the preceding examples, when you created a two-dimensional array, you were creating what is called a rectangular array. Thinking of two-dimensional arrays as tables, a rectangular array is a two-dimensional array in which the length of each row is the same for the entire array. However, C# also allows you to create a special type of two-dimensional array called a jagged array. A jagged array is an array of arrays in which the length of each array can differ. Thus, a jagged array can be used to create a table in which the lengths of the rows are not the same.
Jagged arrays are declared by using sets of square brackets to indicate each dimension. For example, to declare a two-dimensional jagged array, you will use this general form:
type[ ] [ ] array-name = new type[size][ ];
Here, size indicates the number of rows in the array. The rows, themselves, have not been allocated. Instead, the rows are allocated individually. This allows for the length of each row to vary. For example, the following code allocates memory for the first dimension of jagged when it is declared. It then allocates the second dimensions manually.
int[][] jagged = new int[3][];
jagged[0] = new int[4];
jagged[1] = new int[3];
jagged[2] = new int[5];
After this sequence executes, jagged looks like this:
It is easy to see how jagged arrays got their name!
Once a jagged array has been created, an element is accessed by specifying each index within its own set of brackets. For example, to assign the value 10 to element 2, 1 of jagged, you would use this statement:
jagged[2][1] = 10;
Note that this differs from the syntax that is used to access an element of a rectangular array.
The following program demonstrates the creation of a jagged two-dimensional array:
// Demonstrate jagged arrays.
using System;
class Jagged {
static void Main() {
int[][] jagged = new int[3][];
jagged[0] = new int[4];
jagged[1] = new int[3];
jagged[2] = new int[5];
int i;
// Store values in first array.
for(i=0; i < 4; i++)
jagged[0][i] = i;
// Store values in second array.
for(i=0; i < 3; i++)
jagged[1][i] = i;
// Store values in third array.
for(i=0; i < 5; i++)
jagged[2][i] = i;
// Display values in first array.
for(i=0; i < 4; i++)
Console.Write(jagged[0][i] + " ");
Console.WriteLine();
// Display values in second array.
for(i=0; i < 3; i++)
Console.Write(jagged[1][i] + " ");
Console.WriteLine();
// Display values in third array.
for(i=0; i < 5; i++)
Console.Write(jagged[2][i] + " ");
Console.WriteLine();
}
}
The output is shown here:
0 1 2 3
0 1 2
0 1 2 3 4
Jagged arrays are not used by all applications, but they can be effective in some situations. For example, if you need a very large two-dimensional array that is sparsely populated (that is, one in which not all of the elements will be used), then a jagged array might be a perfect solution.
One last point: Because jagged arrays are arrays of arrays, there is no restriction that requires that the arrays be one-dimensional. For example, the following creates an array of two-dimensional arrays:
int[][,] jagged = new int[3][,];
The next statement assigns jagged[0] a reference to a 4×2 array:
jagged[0] = new int[4, 2];
The following statement assigns a value to jagged[0][1,0]:
jagged[0][1,0] = i;
As with other objects, when you assign one array reference variable to another, you are simply making both variables refer to the same array. You are neither causing a copy of the array to be created, nor are you causing the contents of one array to be copied to the other. For example, consider this program:
// Assigning array reference variables.
using System;
class AssignARef {
static void Main() {
int i;
int[] nums1 = new int[10];
int[] nums2 = new int[10];
for(i=0; i < 10; i++) nums1[i] = i;
for(i=0; i < 10; i++) nums2[i] = -i;
Console.Write("Here is nums1: ");
for(i=0; i < 10; i++)
Console.Write(nums1[i] + " ");
Console.WriteLine();
Console.Write("Here is nums2: ");
for(i=0; i < 10; i++)
Console.Write(nums2[i] + " ");
Console.WriteLine();
nums2 = nums1; // now nums2 refers to nums1
Console.Write("Here is nums2 after assignment: ");
for(i=0; i < 10; i++)
Console.Write(nums2[i] + " ");
Console.WriteLine();
// Next, operate on nums1 array through nums2.
nums2[3] = 99;
Console.Write("Here is nums1 after change through nums2: ");
for(i=0; i < 10; i++)
Console.Write(nums1[i] + " ");
Console.WriteLine();
}
}
The output from the program is shown here:
Here is nums1: 0 1 2 3 4 5 6 7 8 9
Here is nums2: 0 -1 -2 -3 -4 -5 -6 -7 -8 -9
Here is nums2 after assignment: 0 1 2 3 4 5 6 7 8 9
Here is nums1 after change through nums2: 0 1 2 99 4 5 6 7 8 9
As the output shows, after the assignment of nums1 to nums2, both array reference variables refer to the same object.
A number of benefits result because C# implements arrays as objects. One comes from the fact that each array has associated with it a Length property that contains the number of elements that an array can hold. Thus, each array provides a means by which its length can be determined. Here is a program that demonstrates this property:
// Use the Length array property.
using System;
class LengthDemo {
static void Main() {
int[] nums = new int[10];
Console.WriteLine("Length of nums is " + nums.Length);
// Use Length to initialize nums.
for(int i=0; i < nums.Length; i++)
nums[i] = i * i;
// Now use Length to display nums.
Console.Write("Here is nums: ");
for(int i=0; i < nums.Length; i++)
Console.Write(nums[i] + " ");
Console.WriteLine();
}
}
This program displays the following output:
Length of nums is 10
Here is nums: 0 1 4 9 16 25 36 49 64 81
In LengthDemo notice the way that nums.Length is used by the for loops to govern the number of iterations that take place. Since each array carries with it its own length, you can use this information rather than manually keeping track of an array’s size. Keep in mind that the value of Length has nothing to do with the number of elements that are actually in use. Length contains the number of elements that the array is capable of holding.
When the length of a multidimensional array is obtained, the total number of elements that can be held by the array is returned. For example:
// Use the Length array property on a 3D array.
using System;
class LengthDemo3D {
static void Main() {
int[,,] nums = new int[10, 5, 6];
Console.WriteLine("Length of nums is " + nums.Length);
}
}
Length of nums is 300
As the output verifies, Length obtains the number of elements that nums can hold, which is 300 (10×5×6) in this case. It is not possible to use Length to obtain the length of a specific dimension.
The inclusion of the Length property simplifies many algorithms by making certain types of array operations easier—and safer—to perform. For example, the following program uses Length to reverse the contents of an array by copying it back-to-front into another array:
// Reverse an array.
using System;
class RevCopy {
static void Main() {
int i,j;
int[] nums1 = new int[10];
int[] nums2 = new int[10];
for(i=0; i < nums1.Length; i++) nums1[i] = i;
Console.Write("Original contents: ");
for(i=0; i < nums2.Length; i++)
Console.Write(nums1[i] + " ");
Console.WriteLine();
// Reverse copy nums1 to nums2.
if(nums2.Length >= nums1.Length) // make sure nums2 is long enough
for(i=0, j=nums1.Length-1; i < nums1.Length; i++, j--)
nums2[j] = nums1[i];
Console.Write("Reversed contents: ");
for(i=0; i < nums2.Length; i++)
Console.Write(nums2[i] + " ");
Console.WriteLine();
}
}
Here is the output:
Original contents: 0 1 2 3 4 5 6 7 8 9
Reversed contents: 9 8 7 6 5 4 3 2 1 0
Here, Length helps perform two important functions. First, it is used to confirm that the target array is large enough to hold the contents of the source array. Second, it provides the termination condition of the for loop that performs the reverse copy. Of course, in this simple example, the size of the arrays is easily known, but this same approach can be applied to a wide range of more challenging situations.
A special case occurs when Length is used with jagged arrays. In this situation, it is possible to obtain the length of each individual array. For example, consider the following program, which simulates the CPU activity on a network with four nodes:
// Demonstrate Length with jagged arrays.
using System;
class Jagged {
static void Main() {
int[][] network_nodes = new int[4][];
network_nodes[0] = new int[3];
network_nodes[1] = new int[7];
network_nodes[2] = new int[2];
network_nodes[3] = new int[5];
int i, j;
// Fabricate some fake CPU usage data.
for(i=0; i < network_nodes.Length; i++)
for(j=0; j < network_nodes[i].Length; j++)
network_nodes[i][j] = i * j + 70;
Console.WriteLine("Total number of network nodes: " +
network_nodes.Length + "\n");
for(i=0; i < network_nodes.Length; i++) {
for(j=0; j < network_nodes[i].Length; j++) {
Console.Write("CPU usage at node " + i +
" CPU " + j + ": ");
Console.Write(network_nodes[i][j] + "% ");
Console.WriteLine();
}
Console.WriteLine();
}
}
}
The output is shown here:
Total number of network nodes: 4
CPU usage at node 0 CPU 0: 70%
CPU usage at node 0 CPU 1: 70%
CPU usage at node 0 CPU 2: 70%
CPU usage at node 1 CPU 0: 70%
CPU usage at node 1 CPU 1: 71%
CPU usage at node 1 CPU 2: 72%
CPU usage at node 1 CPU 3: 73%
CPU usage at node 1 CPU 4: 74%
CPU usage at node 1 CPU 5: 75%
CPU usage at node 1 CPU 6: 76%
CPU usage at node 2 CPU 0: 70%
CPU usage at node 2 CPU 1: 72%
CPU usage at node 3 CPU 0: 70%
CPU usage at node 3 CPU 1: 73%
CPU usage at node 3 CPU 2: 76%
CPU usage at node 3 CPU 3: 79%
CPU usage at node 3 CPU 4: 82%
Pay special attention to the way Length is used on the jagged array network_nodes. Recall, a two-dimensional jagged array is an array of arrays. Thus, when the expression
network_nodes.Length
is used, it obtains the number of arrays stored in network_nodes, which is four in this case. To obtain the length of any individual array in the jagged array, you will use an expression such as this:
network_nodes[0].Length
which, in this case, obtains the length of the first array.
As explained in Chapter 3, C# 3.0 added the ability to declare implicitly typed variables by using the var keyword. These are variables whose type is determined by the compiler, based on the type of the initializing expression. Thus, all implicitly typed variables must be initialized. Using the same mechanism, it is also possible to create an implicitly typed array. As a general rule, implicitly typed arrays are for use in certain types of queries involving LINQ, which is described in Chapter 19. In most other cases, you will use the “normal” array declaration approach. Implicitly typed arrays are introduced here for completeness.
An implicitly typed array is declared using the keyword var, but you do not follow var with [ ]. Furthermore, the array must be initialized because it is the type of the initializers that determine the element type of the array. All of the initializers must be of the same or compatible type. Here is an example of an implicitly typed array:
var vals = new[] { 1, 2, 3, 4, 5 };
This creates an array of int that is five elements long. A reference to that array is assigned to vals. Thus, the type of vals is “array of int” and it has five elements. Again, notice that var is not followed by [ ]. Also, even though the array is being initialized, you must include new[ ]. It’s not optional in this context.
Here is another example. It creates a two-dimensional array of double:
var vals = new[,] { {1.1, 2.2}, {3.3, 4.4},{ 5.5, 6.6} };
In this case, vals has the dimensions 2×3.
You can also declare implicitly typed jagged arrays. For example, consider the following program:
// Demonstrate an implicitly typed jagged array.
using System;
class Jagged {
static void Main() {
var jagged = new[] {
new[] { 1, 2, 3, 4 },
new[] { 9, 8, 7 },
new[] { 11, 12, 13, 14, 15 }
};
for(int j = 0; j < jagged.Length; j++) {
for(int i=0; i < jagged[j].Length; i++)
Console.Write(jagged[j][i] + " ");
Console.WriteLine();
}
}
}
The program produces the following output:
1 2 3 4
9 8 7
11 12 13 14 15
Pay special attention to the declaration of jagged:
var jagged = new[] {
new[] { 1, 2, 3, 4 },
new[] { 9, 8, 7 },
new[] { 11, 12, 13, 14, 15 }
};
Notice how new[ ] is used in two ways. First, it creates the array of arrays. Second, it creates each individual array, based on the number and type of initializers. As you would expect, all of the initializers in the individual arrays must be of the same type. The same general approach used to declare jagged can be used to declare any implicitly typed jagged array.
As mentioned, implicitly typed arrays are most applicable to LINQ-based queries. They are not meant for general use. In most cases, you should use explicitly typed arrays.
In Chapter 5, it was mentioned that C# defines a loop called foreach, but a discussion of that statement was deferred until later. The time for that discussion has now come.
The foreach loop is used to cycle through the elements of a collection. A collection is a group of objects. C# defines several types of collections, of which one is an array. The general form of foreach is shown here:
foreach(type loopvar in collection) statement;
Here, type loopvar specifies the type and name of an iteration variable. The iteration variable receives the value of the next element in the collection each time the foreach loop iterates. The collection being cycled through is specified by collection, which, for the rest of this discussion, is an array. Thus, type must be the same as (or compatible with) the element type of the array. The type can also be var, in which case the compiler determines the type based on the element type of the array. This can be useful when working with certain queries, as described later in this book. Normally, you will explicitly specify the type.
Here is how foreach works. When the loop begins, the first element in the array is obtained and assigned to loopvar. Each subsequent iteration obtains the next element from the array and stores it in loopvar. The loop ends when there are no more elements to obtain. Thus, the foreach cycles through the array one element at a time, from start to finish.
One important point to remember about foreach is that the iteration variable loopvar is read-only. This means you can’t change the contents of an array by assigning the iteration variable a new value.
Here is a simple example that uses foreach. It creates an array of integers and gives it some initial values. It then displays those values, computing the summation in the process.
// Use the foreach loop.
using System;
class ForeachDemo {
static void Main() {
int sum = 0;
int[] nums = new int[10];
// Give nums some values.
for(int i = 0; i < 10; i++)
nums[i] = i;
// Use foreach to display and sum the values.
foreach(int x in nums) {
Console.WriteLine("Value is: " + x);
sum += x;
}
Console.WriteLine("Summation: " + sum);
}
}
The output from the program is shown here:
Value is: 0
Value is: 1
Value is: 2
Value is: 3
Value is: 4
Value is: 5
Value is: 6
Value is: 7
Value is: 8
Value is: 9
Summation: 45
As this output shows, foreach cycles through an array in sequence from the lowest index to the highest.
Although the foreach loop iterates until all elements in an array have been examined, it is possible to terminate a foreach loop early by using a break statement. For example, this program sums only the first five elements of nums:
// Use break with a foreach.
using System;
class ForeachDemo {
static void Main() {
int sum = 0;
int[] nums = new int[10];
// Give nums some values.
for(int i = 0; i < 10; i++)
nums[i] = i;
// Use foreach to display and sum the values.
foreach(int x in nums) {
Console.WriteLine("Value is: " + x);
sum += x;
if(x == 4) break; // stop the loop when 4 is obtained
}
Console.WriteLine("Summation of first 5 elements: " + sum);
}
}
This is the output produced:
Value is: 0
Value is: 1
Value is: 2
Value is: 3
Value is: 4
Summation of first 5 elements: 10
As is evident, the foreach loop stops after the fifth element has been obtained.
The foreach loop also works on multidimensional arrays. It returns those elements in row order, from first to last.
// Use foreach on a two-dimensional array.
using System;
class ForeachDemo2 {
static void Main() {
int sum = 0;
int[,] nums = new int[3,5];
// Give nums some values.
for(int i = 0; i < 3; i++)
for(int j=0; j < 5; j++)
nums[i,j] = (i+1)*(j+1);
// Use foreach to display and sum the values.
foreach(int x in nums) {
Console.WriteLine("Value is: " + x);
sum += x;
}
Console.WriteLine("Summation: " + sum);
}
}
The output from this program is shown here:
Value is: 1
Value is: 2
Value is: 3
Value is: 4
Value is: 5
Value is: 2
Value is: 4
Value is: 6
Value is: 8
Value is: 10
Value is: 3
Value is: 6
Value is: 9
Value is: 12
Value is: 15
Summation: 90
Since the foreach loop can only cycle through an array sequentially, from start to finish, you might think that its use is limited. However, this is not true. A large number of algorithms require exactly this mechanism, of which one of the most common is searching. For example, the following program uses a foreach loop to search an array for a value. It stops if the value is found.
// Search an array using foreach.
using System;
class Search {
static void Main() {
int[] nums = new int[10];
int val;
bool found = false;
// Give nums some values.
for(int i = 0; i < 10; i++)
nums[i] = i;
val = 5;
// Use foreach to search nums for key.
foreach(int x in nums) {
if(x == val) {
found = true;
break;
}
}
if(found)
Console.WriteLine("Value found!");
}
}
The output is shown here:
Value found!
The foreach loop is an excellent choice in this application because searching an array involves examining each element. Other types of foreach applications include such things as computing an average, finding the minimum or maximum of a set, looking for duplicates, and so on. As you will see later in this book, foreach is especially useful when operating on other types of collections.
From a day-to-day programming standpoint, string is one of C#’s most important data types because it defines and supports character strings. In many other programming languages, a string is an array of characters. This is not the case with C#. In C#, strings are objects. Thus, string is a reference type. Although string is a built-in data type in C#, a discussion of string needed to wait until classes and objects had been introduced.
Actually, you have been using the string class since Chapter 2, but you did not know it. When you create a string literal, you are actually creating a string object. For example, in the statement
Console.WriteLine("In C#, strings are objects.");
the string “In C#, strings are objects.” is automatically made into a string object by C#. Thus, the use of the string class has been “below the surface” in the preceding programs. In this section, you will learn to handle them explicitly.
The easiest way to construct a string is to use a string literal. For example, here str is a string reference variable that is assigned a reference to a string literal:
string str = "C# strings are powerful.";
In this case, str is initialized to the character sequence “C# strings are powerful.”
You can also create a string from a char array. For example:
char[] charray = {'t', 'e', 's', 't'};
string str = new string(charray);
Once you have created a string object, you can use it nearly anywhere that a quoted string is allowed. For example, you can use a string object as an argument to WriteLine( ), as shown in this example:
// Introduce string.
using System;
class StringDemo {
static void Main() {
char[] charray = {'A', ' ', 's', 't', 'r', 'i', 'n', 'g', '.' };
string str1 = new string(charray);
string str2 = "Another string.";
Console.WriteLine(str1);
Console.WriteLine(str2);
}
}
The output from the program is shown here:
A string.
Another string.
The string class contains several methods that operate on strings. Table 7-1 shows a few. Notice that several of the methods take a parameter of type StringComparison. This is an enumeration type that defines various values that determine how a comparison involving strings will be conducted. (Enumerations are described in Chapter 12, and the use of StringComparison here does not require an understanding of enumerations.) As you might guess, there are various ways in which two strings can be compared. For example, they can be compared based on the binary values of the characters that comprise them. This is called an ordinal comparison. Strings can also be compared in a way that takes into account various cultural metrics, such as dictionary order. This type of comparison is called culture-sensitive. (Culture sensitivity is especially important in applications that are being internationalized.) Furthermore, comparisons can either be case-sensitive or ignore case differences. Although overloads of methods such as Compare( ), Equals( ), IndexOf( ), and LastIndexOf( ) do exist that provide a default string comparison approach, it is now considered best to explicitly specify what type of comparison you want. Doing so helps avoid ambiguity in this regard. (It also helps with internationalizing an application.) This is why these forms are shown here.
In general, and with some exceptions, if you want to compare two strings for sorting relative to the cultural norms, you will use StringComparison.CurrentCulture as the comparison approach. If you want to compare two strings based solely on the value of their characters, it is usually best to use StringComparison.Ordinal. To ignore case differences, specify either StringComparison.CurrentCultureIgnoreCase or StringComparison.OrdinalIgnoreCase. You can also specify an invariant culture. (See Chapter 22.)
Notice that in Table 7-1, Compare( ) is declared as static. The static modifier is described in Chapter 8, but briefly, as it is used here, it means that Compare( ) is called on its class name, not an instance of its class. Thus, to call Compare( ), you will use this general form:
result = string.Compare(str1, str2, how);
where how specifies the string comparison approach.
NOTE Additional information about approaches to string comparisons and searches, including the importance of choosing the right technique, is found in Chapter 22, where string handling is discussed in detail.
Also notice the two methods ToUpper( ) and ToLower( ), which uppercase or lowercase a string, respectively. The forms shown here both have a CultureInfo parameter. This is a class that describes the cultural attributes to use for the conversion. The examples in this book use the current culture settings. These settings are specified by passing CultureInfo.CurrentCulture. The CultureInfo class is in System.Globalization. As a point of interest, there are versions of these methods that use the current culture by default, but to avoid ambiguity on this point, this book will explicitly specify this argument.
The string type also includes the Length property, which contains the length of the string.
TABLE 7-1 A Sampling of Common String Handling Methods
To obtain the value of an individual character of a string, you simply use an index. For example:
string str = "test";
Console.WriteLine(str[0]);
This displays “t”, the first character of “test”. Like arrays, string indexes begin at zero. One important point, however, is that you cannot assign a new value to a character within a string using an index. An index can only be used to obtain a character.
You can use the = = operator to test two strings for equality. Normally, when the = = operator is applied to object references, it determines if both references refer to the same object. This differs for objects of type string. When the = = is applied to two string references, the contents of the strings themselves are compared for equality. The same is true for the != operator: when comparing string objects, the contents of the strings are compared. In both cases, an ordinal comparison is performed. To test two strings for equality using cultural information, use Equals( ) and specify the comparison approach, such as StringComparison.CurrentCulture. One other point: the Compare( ) method is intended to compare strings to determine an ordering relationship, such as for sorting. To test for equality, use Equals( ) or the string operators.
Here is a program that demonstrates several string operations:
// Some string operations.
using System;
using System.Globalization;
class StrOps {
static void Main() {
string str1 = "When it comes to .NET programming, C# is #1.";
string str2 = "When it comes to .NET programming, C# is #1.";
string str3 = "C# strings are powerful.";
string strUp, strLow;
int result, idx;
Console.WriteLine("str1: " + str1);
Console.WriteLine("Length of str1: " + str1.Length);
// Create upper- and lowercase versions of str1.
strLow = str1.ToLower(CultureInfo.CurrentCulture);
strUp = str1.ToUpper(CultureInfo.CurrentCulture);
Console.WriteLine("Lowercase version of str1:\n " +
strLow);
Console.WriteLine("Uppercase version of str1:\n " +
strUp);
Console.WriteLine();
// Display str1, one char at a time.
Console.WriteLine("Display str1, one char at a time.");
for(int i=0; i < str1.Length; i++)
Console.Write(str1[i]);
Console.WriteLine("\n");
// Compare strings using == and !=. These comparisons are ordinal.
if(str1 == str2)
Console.WriteLine("str1 == str2");
else
Console.WriteLine("str1 != str2");
if(str1 == str3)
Console.WriteLine("str1 == str3");
else
Console.WriteLine("str1 != str3");
// This comparison is culture-sensitive.
result = string.Compare(str1, str3, StringComparison.CurrentCulture);
if(result == 0)
Console.WriteLine("str1 and str3 are equal");
else if(result < 0)
Console.WriteLine("str1 is less than str3");
else
Console.WriteLine("str1 is greater than str3");
Console.WriteLine();
// Assign a new string to str2.
str2 = "One Two Three One";
// Search a string.
idx = str2.IndexOf("One", StringComparison.Ordinal);
Console.WriteLine("Index of first occurrence of One: " + idx);
idx = str2.LastIndexOf("One", StringComparison.Ordinal);
Console.WriteLine("Index of last occurrence of One: " + idx);
}
}
This program generates the following output:
str1: When it comes to .NET programming, C# is #1.
Length of str1: 44
Lowercase version of str1:
when it comes to .net programming, c# is #1.
Uppercase version of str1:
WHEN IT COMES TO .NET PROGRAMMING, C# IS #1.
Display str1, one char at a time.
When it comes to .NET programming, C# is #1.
str1 == str2
str1 != str3
str1 is greater than str3
Index of first occurrence of One: 0
Index of last occurrence of One: 14
Before moving on, in the program notice that Compare( ) is called as shown here:
result = string.Compare(str1, str3, StringComparison.CurrentCulture);
As explained, because Compare( ) is declared as static, it is called on its class name, not an instance of its class.
You can concatenate (join together) two strings using the + operator. For example, this statement:
string str1 = "One";
string str2 = "Two";
string str3 = "Three";
string str4 = str1 + str2 + str3;
initializes str4 with the string “OneTwoThree”.
One other point: The string keyword is an alias for (that is, maps directly to) the System.String class defined by the .NET Framework class library. Thus, the fields and methods defined by string are those of the System.String class, which includes more than the sampling described here. System.String is examined in detail in Part II.
Like any other data type, strings can be assembled into arrays. For example:
// Demonstrate string arrays.
using System;
class StringArrays {
static void Main() {
string[] str = { "This", "is", "a", "test." };
Console.WriteLine("Original array: ");
for(int i=0; i < str.Length; i++)
Console.Write(str[i] + " ");
Console.WriteLine("\n");
// Change a string.
str[1] = "was";
str[3] = "test, too!";
Console.WriteLine("Modified array: ");
for(int i=0; i < str.Length; i++)
Console.Write(str[i] + " ");
}
}
Here is the output from this program:
Original array:
This is a test.
Modified array:
This was a test, too!
Here is a more interesting example. The following program displays an integer value using words. For example, the value 19 will display as “one nine”.
// Display the digits of an integer using words.
using System;
class ConvertDigitsToWords {
static void Main() {
int num;
int nextdigit;
int numdigits;
int[] n = new int[20];
string[] digits = { "zero", "one", "two",
"three", "four", "five",
"six", "seven", "eight",
"nine" };
num = 1908;
Console.WriteLine("Number: " + num);
Console.Write("Number in words: ");
nextdigit = 0;
numdigits = 0;
// Get individual digits and store in n.
// These digits are stored in reverse order.
do {
nextdigit = num % 10;
n[numdigits] = nextdigit;
numdigits++;
num = num / 10;
} while(num > 0);
numdigits--;
// Display the words.
for( ; numdigits >= 0; numdigits--)
Console.Write(digits[n[numdigits]] + " ");
Console.WriteLine();
}
}
The output is shown here:
Number: 1908
Number in words: one nine zero eight
In the program, the string array digits holds in order the word equivalents of the digits from zero to nine. The program converts an integer into words by first obtaining each digit of the value and storing those digits, in reverse order, in the int array called n. Then, this array is cycled through from back to front. In the process, each integer value in n is used as an index into digits, with the corresponding string being displayed.
Here is something that might surprise you: The contents of a string object are immutable. That is, once created, the character sequence comprising that string cannot be altered. This restriction allows strings to be implemented more efficiently. Even though this probably sounds like a serious drawback, it isn’t. When you need a string that is a variation on one that already exists, simply create a new string that contains the desired changes. Because unused string objects are automatically garbage-collected, you don’t even need to worry about what happens to the discarded strings.
It must be made clear, however, that string reference variables may, of course, change which object they refer to. It is just that the contents of a specific string object cannot be changed after it is created.
To fully understand why immutable strings are not a hindrance, we will use another of string’s methods: Substring( ). The Substring( ) method returns a new string that contains a specified portion of the invoking string. Because a new string object is manufactured that contains the substring, the original string is unaltered, and the rule of immutability is still intact. The form of Substring( ) that we will be using is shown here:
string Substring(int startIndex, int length)
Here, startIndex specifies the beginning index, and length specifies the length of the substring.
Here is a program that demonstrates Substring( ) and the principle of immutable strings:
// Use Substring().
using System;
class SubStr {
static void Main() {
string orgstr = "C# makes strings easy.";
// Construct a substring.
string substr = orgstr.Substring(5, 12);
Console.WriteLine("orgstr: " + orgstr);
Console.WriteLine("substr: " + substr);
}
}
Here is the output from the program:
orgstr: C# makes strings easy.
substr: kes strings
As you can see, the original string orgstr is unchanged, and substr contains the substring.
One more point: Although the immutability of string objects is not usually a restriction or hindrance, there may be times when it would be beneficial to be able to modify a string. To allow this, C# offers a class called StringBuilder, which is in the System.Text namespace. It creates string objects that can be changed. For most purposes, however, you will want to use string, not StringBuilder.
A string can be used to control a switch statement. It is the only non-integer type that can be used in the switch. The fact that strings can be used in switch statements makes it possible to handle some otherwise difficult situations quite easily. For example, the following program displays the digit equivalent of the words “one,” “two,” and “three”:
// A string can control a switch statement.
using System;
class StringSwitch {
static void Main() {
string[] strs = { "one", "two", "three", "two", "one" };
foreach(strings in strs) {
switch(s) {
case "one":
Console.Write(1);
break;
case "two":
Console.Write(2);
break;
case "three":
Console.Write(3);
break;
}
}
Console.WriteLine();
}
}
The output is shown here:
12321