You can find the wrox.com
code downloads for this chapter at www.wrox.com/go/beginningvisualc#2015programming
on the Download Code tab. The code is in the Chapter 5 download and individually named according to the names throughout the chapter.
Now that you've seen a bit more of the C# language, you can go back and tackle some of the more involved topics concerning variables.
The first subject you look at in this chapter is type conversion, whereby you convert values from one type into another. You've already seen a bit of this, but you look at it formally here. A grasp of this topic gives you a greater understanding of what happens when you mix types in expressions (intentionally or unintentionally), as well as tighter control over the way that data is manipulated. This helps you to streamline your code and avoid nasty surprises.
Then you'll look at a few more types of variables that you can use:
These are slightly more complex than the simple types you've been using up to now, but they can make your life much easier. Finally, you'll explore another useful subject concerning strings: basic string manipulation.
Earlier in this book, you saw that all data, regardless of type, is simply a sequence of bits — that is, a sequence of zeros and ones. The meaning of the variable is determined by the way in which this data is interpreted. The simplest example of this is the char
type. This type represents a character in the Unicode character set using a number. In fact, the number is stored in exactly the same way as a ushort
— both of them store a number between 0 and 65535.
However, in general, the different types of variables use varying schemes to represent data. This implies that even if it were possible to place the sequence of bits from one variable into a variable of a different type (perhaps they use the same amount of storage, or perhaps the target type has enough storage space to include all the source bits), the results might not be what you expect.
Instead of this one-to-one mapping of bits from one variable into another, you need to use type conversion on the data. Type conversion takes two forms:
Implicit conversion requires no work on your part and no additional code. Consider the code shown here:
var1 = var2;
This assignment may involve an implicit conversion if the type of var2
can be implicitly converted into the type of var1
; however, it could just as easily involve two variables with the same type, in which case no implicit conversion is necessary. For example, the values of ushort
and char
are effectively interchangeable, because both store a number between 0 and 65535. You can convert values between these types implicitly, as demonstrated by the following code:
ushort destinationVar;
char sourceVar = 'a';
destinationVar = sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
Here, the value stored in sourceVar
is placed in destinationVar
. When you output the variables with the two WriteLine()
commands, you get the following output:
sourceVar val: a
destinationVar val: 97
Even though the two variables store the same information, they are interpreted in different ways using their type.
There are many implicit conversions of simple types; bool
and string
have no implicit conversions, but the numeric types have a few. For reference, Table 5.1 shows the numeric conversions that the compiler can perform implicitly (remember that char
s are stored as numbers, so char
counts as a numeric type).
Table 5.1 Implicit Numeric Conversions
Type | Can Safely be Converted To |
byte | short, ushort, int, uint, long, ulong, float, double, decimal |
sbyte | short, int, long, float, double, decimal |
short | int, long, float, double, decimal |
ushort | int, uint, long, ulong, float, double, decimal |
int | long, float, double, decimal |
uint | long, ulong, float, double, decimal |
long | float, double, decimal |
ulong | float, double, decimal |
float | double |
char | ushort, int, uint, long, ulong, float, double, decimal |
Don't worry — you don't need to learn this table by heart, because it's actually quite easy to work out which conversions the compiler can do implicitly. Back in Chapter 3, Tables 3-1, 3-2 and 3-3 showed the range of possible values for every simple numeric type. The implicit conversion rule for these types is this: Any type A whose range of possible values completely fits inside the range of possible values of type B can be implicitly converted into that type.
The reasoning for this is simple. If you try to fit a value into a variable, but that value is outside the range of values the variable can take, then there will be a problem. For example, a short
type variable is capable of storing values up to 32767, and the maximum value allowed into a byte
is 255, so there could be problems if you try to convert a short
value into a byte
value. If the short
holds a value between 256 and 32767, then it simply won't fit into a byte
.
If you know that the value in your short
type variable is less than 255, then you should be able to convert the value, right? The simple answer is that, of course, you can. The slightly more complex answer is that, of course, you can, but you must use an explicit conversion. Performing an explicit conversion is a bit like saying “Okay, I know you've warned me about doing this, but I'll take responsibility for what happens.”
As the name suggests, an explicit conversion occurs when you explicitly ask the compiler to convert a value from one data type to another. These conversions require extra code, and the format of this code may vary, depending on the exact conversion method. Before you look at any of this explicit conversion code, look at what happens if you don't add any.
For example, the following modification to the code from the last section attempts to convert a short
value into a byte
:
byte destinationVar;
short sourceVar = 7;
destinationVar = sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
If you attempt to compile the preceding code, you will receive the following error:
Cannot implicitly convert type 'short' to 'byte'. An explicit conversion exists
(are you missing a cast?)
To get this code to compile, you need to add the code to perform an explicit conversion. The easiest way to do that in this context is to cast the short
variable into a byte
(as suggested by the preceding error string). Casting basically means forcing data from one type into another, and it uses the following simple syntax:
(<destinationType
>)<sourceVar
>
This will convert the value in <
sourceVar
>
into <
destinationType
>
.
You can, therefore, modify your example using this syntax to force the conversion from a short
to a byte
:
byte destinationVar;
short sourceVar = 7;
destinationVar = (byte)sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
This results in the following output:
sourceVar val: 7
destinationVar val: 7
What happens when you try to force a value into an incompatible variable type? For example, you can't fit a large integer into a numeric type that's too small. Modifying your code as follows illustrates this:
byte destinationVar;
short sourceVar = 281;
destinationVar = (byte)sourceVar;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
This results in the following:
sourceVar val: 281
destinationVar val: 25
What happened? Well, look at the binary representations of these two numbers, along with the maximum value that can be stored in a byte, which is 255:
281 = 100011001
25 = 000011001
255 = 011111111
You can see that the leftmost bit of the source data has been lost. This immediately raises a question: How can you tell when this happens? Obviously, there will be times when you will need to explicitly cast one type into another, and it would be nice to know if any data has been lost along the way. Not detecting this could cause serious errors — for example, in an accounting application or an application determining the trajectory of a rocket to the moon.
One way to do this is simply to check the value of the source variable and compare it with the known limits of the destination variable. Another technique is to force the system to pay special attention to the conversion at runtime. Attempting to fit a value into a variable when that value is too big for the type of that variable results in an overflow, and this is the situation you want to check for.
Two keywords exist for setting what is called the overflow checking context for an expression: checked
and unchecked
. You use these in the following way:
checked(<expression>
)
unchecked(<expression>
)
You can force overflow checking in the last example:
byte destinationVar;
short sourceVar = 281;
destinationVar = checked(
(byte)sourceVar)
;
WriteLine($"sourceVar val: {sourceVar}");
WriteLine($"destinationVar val: {destinationVar}");
When this code is executed, it will crash with the error message shown in Figure 5.1 (this was compiled in a project called OverflowCheck
).
However, if you replace checked
with unchecked
in this code, you get the result shown earlier, and no error occurs. That is identical to the default behavior, also shown earlier.
You also can configure your application to behave as if every expression of this type includes the checked
keyword, unless that expression explicitly uses the unchecked
keyword (in other words, you can change the default setting for overflow checking). To do this, you modify the properties for your project by right-clicking on it in the Solution Explorer window and selecting the Properties option. Click Build on the left side of the window to bring up the Build settings.
The property you want to change is one of the Advanced settings, so click the Advanced button. In the dialog box that appears, enable the Check for Arithmetic Overflow/Underflow box, as shown in Figure 5.2. By default, this setting is disabled; enabling it provides the checked
behavior detailed previously.
The type of explicit conversion you have been using in many of the Try It Out examples in this book is a bit different from those you have seen so far in this chapter. You have been converting string values into numbers using commands such as ToDouble()
, which is obviously something that won't work for every possible string.
If, for example, you try to convert a string like Number
into a double value using ToDouble()
, you will see the dialog box shown in Figure 5.3 when you execute the code.
As you can see, the operation fails. For this type of conversion to work, the string supplied must be a valid representation of a number, and that number must be one that won't cause an overflow. A valid representation of a number is one that contains an optional sign (that is, plus or minus), zero or more digits, an optional period followed by one or more digits, and an optional “e
” or “E
” followed by an optional sign, one or more digits, and nothing else except spaces (before or after this sequence). Using all of these optional extras, you can recognize strings as complex as -1.2451e-24
as being a number.
The important thing to note about these conversions is that they are always overflow-checked, and the checked
and unchecked
keywords and project property settings have no effect.
The next Try It Out is an example that covers many of the conversion types from this section. It declares and initializes a number of variables of different types and then converts between them implicitly and explicitly.
In addition to all the simple variable types, C# also offers three slightly more complex (but very useful) sorts of variables: enumerations (often referred to as enums), structs (occasionally referred to as structures), and arrays.
Each of the types you've seen so far (with the exception of string
) has a clearly defined set of allowed values. Admittedly, this set is so large in types such as double
that it can practically be considered a continuum, but it is a fixed set nevertheless. The simplest example of this is the bool
type, which can take only one of two values: true
or false
.
There are many other circumstances in which you might want to have a variable that can take one of a fixed set of results. For example, you might want to have an orientation
type that can store one of the values north
, south
, east
, or west
.
In situations like this, enumerations can be very useful. Enumerations do exactly what you want in this orientation
type: They allow the definition of a type that can take one of a finite set of values that you supply. What you need to do, then, is create your own enumeration type called orientation
that can take one of the four possible values.
Note that there is an additional step involved here — you don't just declare a variable of a given type; you declare and detail a user-defined type and then declare a variable of this new type.
You can use the enum
keyword to define enumerations as follows:
enum <typeName
>
{
<value1
>,
<value2
>,
<value3
>,
…
<valueN
>
}
Next, you can declare variables of this new type as follows:
<typeName
> <varName
>;
You can assign values using the following:
<varName
> = <typeName
>.<value
>;
Enumerations have an underlying type used for storage. Each of the values that an enumeration type can take is stored as a value of this underlying type, which by default is int
. You can specify a different underlying type by adding the type to the enumeration declaration:
enum <typeName
> : <underlyingType>
{
<value1
>,
<value2
>,
<value3
>,
…
<valueN
>
}
Enumerations can have underlying types of byte
, sbyte
, short
, ushort
, int
, uint
, long
, and ulong
.
By default, each value is assigned a corresponding underlying type value automatically according to the order in which it is defined, starting from zero. This means that <
value1
>
gets the value 0, <
value2
>
gets 1, <
value3
>
gets 2, and so on. You can override this assignment by using the =
operator and specifying actual values for each enumeration value:
enum <typeName
> : <underlyingType>
{
<value1
> = <actualVal1>,
<value2
> = <actualVal2>,
<value3
> = <actualVal3>,
…
<valueN
> = <actualValN>
}
In addition, you can specify identical values for multiple enumeration values by using one value as the underlying value of another:
enum <typeName
> : <underlyingType
>
{
<value1
> = <actualVal1
>,
<value2
> = <value1
>,
<value3
>,
…
<valueN
> = <actualValN
>
}
Any values left unassigned are given an underlying value automatically, whereby the values used are in a sequence starting from 1 greater than the last explicitly declared one. In the preceding code, for example, <
value3
>
will get the value <
value1
>
+ 1.
Note that this can cause problems, with values specified after a definition such as <
value2
>
=
<
value1
>
being identical to other values. For example, in the following code <
value4
>
will have the same value as <
value2
>
:
enum <typeName>
: <underlyingType>
{
<value1> = <actualVal1>,
<value2>,
<value3> = <value1>,
<value4>,
…
<valueN> = <actualValN>
}
Of course, if this is the behavior you want, then this code is fine. Note also that assigning values in a circular fashion will cause an error:
enum <typeName>
: <underlyingType>
{
<value1> = <value2>,
<value2> = <value1>
}
The following Try It Out shows an example of all of this. The code defines and then uses an enumeration called orientation
.
C:\BegVCSharp\Chapter05
.Program.cs
:namespace Ch05Ex02
{
enum orientation : byte
{
north = 1,
south = 2,
east = 3,
west = 4
}
class Program
{
static void Main(string[] args)
{
orientation myDirection = orientation.north;
WriteLine($"myDirection = {myDirection}");
ReadKey();
}
}
}
byte directionByte;
string directionString;
orientation myDirection = orientation.north;
WriteLine($"myDirection = {myDirection}");
directionByte = (byte)myDirection;
directionString = Convert.ToString(myDirection);
WriteLine($"byte equivalent = {directionByte}");
WriteLine($"string equivalent = {directionString}");
ReadKey();
This code defines and uses an enumeration type called orientation
. The first thing to notice is that the type definition code is placed in your namespace, Ch05Ex02
, but not in the same place as the rest of your code. That is because definitions are not executed; that is, at runtime you don't step through the code in a definition as you do the lines of code in your application. Application execution starts in the place you're used to and has access to your new type because it belongs to the same namespace.
The first iteration of the example demonstrates the basic method of creating a variable of your new type, assigning it a value, and outputting it to the screen. Next, you modify the code to show the conversion of enumeration values into other types. Note that you must use explicit conversions here. Even though the underlying type of orientation
is byte
, you still have to use the (byte)
cast to convert the value of myDirection
into a byte
type:
directionByte = (byte)myDirection;
The same explicit casting is necessary in the other direction, too, if you want to convert a byte
into an orientation
. For example, you could use the following code to convert a byte
variable called myByte
into an orientation
and assign this value to myDirection
:
myDirection = (orientation)myByte;
Of course, you must be careful here because not all permissible values of byte
type variables map to defined orientation
values. The orientation
type can store other byte
values, so you won't get an error straight away, but this may break logic later in the application.
To get the string value of an enumeration value you can use Convert.ToString()
:
directionString = Convert.ToString(myDirection);
Using a (string)
cast won't work because the processing required is more complicated than just placing the data stored in the enumeration variable into a string
variable. Alternatively, you can use the ToString()
command of the variable itself. The following code gives you the same result as using Convert.ToString()
:
directionString = myDirection.ToString();
Converting a string
to an enumeration value is also possible, except that here the syntax required is slightly more complex. A special command exists for this sort of conversion, Enum.Parse()
, which is used in the following way:
(enumerationType)Enum.Parse(typeof(enumerationType), enumerationValueString);
This uses another operator, typeof
, which obtains the type of its operand. You could use this for your orientation
type as follows:
string myString = "north";
orientation myDirection = (orientation)Enum.Parse(typeof(orientation),
myString);
Of course, not all string values will map to an orientation
value! If you pass in a value that doesn't map to one of your enumeration values, you will get an error. Like everything else in C#, these values are case sensitive, so you still get an error if your string agrees with a value in everything but case (for example, if myString
is set to North
rather than north
).
The struct (short for structure) is just that. That is, structs are data structures composed of several pieces of data, possibly of different types. They enable you to define your own types of variables based on this structure. For example, suppose that you want to store the route to a location from a starting point, where the route consists of a direction and a distance in miles. For simplicity, you can assume that the direction is one of the compass points (such that it can be represented using the orientation
enumeration from the last section), and that distance in miles can be represented as a double
type.
You could use two separate variables for this using code you've seen already:
orientation myDirection;
double myDistance;
There is nothing wrong with using two variables like this, but it is far simpler (especially where multiple routes are required) to store this information in one place.
Structs are defined using the struct
keyword as follows:
struct <typeName
>
{
<memberDeclarations
>
}
The <
memberDeclarations
>
section contains declarations of variables (called the data members of the struct) in almost the same format as usual. Each member declaration takes the following form:
<accessibility
> <type
> <name
>;
To allow the code that calls the struct to access the struct's data members, you use the keyword public
for <
accessibility
>
. For example:
struct route
{
public orientation direction;
public double distance;
}
Once you have a struct type defined, you use it by defining variables of the new type:
route myRoute;
In addition, you have access to the data members of this composite variable via the period character:
myRoute.direction = orientation.north;
myRoute.distance = 2.5;
This is demonstrated in the following Try It Out, where the orientation
enumeration from the last Try It Out is used with the route
struct shown earlier. This struct is then manipulated in code to give you a feel for how structs work.
C:\BegVCSharp\Chapter05
.Program.cs
:namespace Ch05Ex03
{
enum orientation: byte
{
north = 1,
south = 2,
east = 3,
west = 4
}
struct route
{
public orientation direction;
public double distance;
}
class Program
{
static void Main(string[] args)
{
route myRoute;
int myDirection = -1;
double myDistance;
WriteLine("1) North\n2) South\n3) East\n4) West");
do
{
WriteLine("Select a direction:");
myDirection = ToInt32(ReadLine());
}
while ((myDirection < 1) || (myDirection > 4));
WriteLine("Input a distance:");
myDistance = ToDouble(ReadLine());
myRoute.direction = (orientation)myDirection;
myRoute.distance = myDistance;
WriteLine($"myRoute specifies a direction of {myRoute.direction} " +
$"and a distance of {myRoute.distance}");
ReadKey();
}
}
}
Structs, like enumerations, are declared outside of the main body of the code. You declare your route
struct just inside the namespace declaration, along with the orientation
enumeration that it uses:
enum orientation: byte
{
north = 1,
south = 2,
east = 3,
west = 4
}
struct route
{
public orientation direction;
public double distance;
}
The main body of the code follows a structure similar to some of the example code you've already seen, requesting input from the user and displaying it. You perform some simple validation of user input by placing the direction selection in a do
loop, rejecting any input that isn't an integer between 1 and 4 (with values chosen such that they map onto the enumeration members for easy assignment).
Input that cannot be interpreted as an integer will result in an error. You'll see why this happens, and what to do about it, later in the book.
The interesting point to note is that when you refer to members of route
they are treated exactly the same way that variables of the same type as the members are. The assignment is as follows:
myRoute.direction = (orientation)myDirection;
myRoute.distance = myDistance;
You could simply take the input value directly into myRoute.distance
with no ill effects as follows:
myRoute.distance = ToDouble(ReadLine());
The extra step allows for more validation, although none is performed in this code. Any access to members of a structure is treated in the same way. Expressions of the form <structVar>.<memberVar>
can be said to evaluate to a variable of the type of <memberVar>.
All the types you've seen so far have one thing in common: Each of them stores a single value (or a single set of values in the case of structs). Sometimes, in situations where you want to store a lot of data, this isn't very convenient. You may want to store several values of the same type at the same time, without having to use a different variable for each value.
For example, suppose you want to perform some processing that involves the names of all your friends. You could use simple string variables as follows:
string friendName1 = "Todd Anthony";
string friendName2 = "Kevin Holton";
string friendName3 = "Shane Laigle";
But this looks like it will require a lot of effort, especially because you need to write different code to process each variable. You couldn't, for example, iterate through this list of strings in a loop.
The alternative is to use an array. Arrays are indexed lists of variables stored in a single array type variable. For example, you might have an array called friendNames
that stores the three names shown in the preceding string variables. You can access individual members of the array by specifying their index in square brackets, as shown here:
friendNames[<index>
]
The index is simply an integer, starting with 0 for the first entry, using 1 for the second, and so on. This means that you can go through the entries using a loop:
int i;
for (i = 0; i < 3; i++)
{
WriteLine($"Name with index of {i}: {friendNames[i]}");
}
Arrays have a single base type — that is, individual entries in an array are all of the same type. This friendNames
array has a base type of string
because it is intended for storing string
variables. Array entries are often referred to as elements.
Arrays are declared in the following way:
<baseType
>[] <name
>;
Here, <
baseType
>
may be any variable type, including the enumeration and struct types you've seen in this chapter. Arrays must be initialized before you have access to them. You can't just access or assign values to the array elements like this:
int[] myIntArray;
myIntArray[10] = 5;
Arrays can be initialized in two ways. You can either specify the complete contents of the array in a literal form or specify the size of the array and use the new
keyword to initialize all array elements.
Specifying an array using literal values simply involves providing a comma-separated list of element values enclosed in curly braces:
int[] myIntArray = { 5, 9, 10, 2, 99 };
Here, myIntArray
has five elements, each with an assigned integer value.
The other method requires the following syntax:
int[] myIntArray = new int[5];
Here, you use the new
keyword to explicitly initialize the array, and a constant value to define the size. This method results in all the array members being assigned a default value, which is 0 for numeric types. You can also use nonconstant variables for this initialization:
int[] myIntArray = new int[arraySize];
In addition, you can combine these two methods of initialization if you want:
int[] myIntArray = new int[5] { 5, 9, 10, 2, 99 };
With this method the sizes must match. You can't, for example, write the following:
int[] myIntArray = new int[10] { 5, 9, 10, 2, 99 };
Here, the array is defined as having 10 members, but only five are defined, so compilation will fail. A side effect of this is that if you define the size using a variable, then that variable must be a constant:
const int arraySize = 5;
int[] myIntArray = new int[arraySize] { 5, 9, 10, 2, 99 };
If you omit the const
keyword, this code will fail.
As with other variable types, there is no need to initialize an array on the same line that you declare it. The following is perfectly legal:
int[] myIntArray;
myIntArray = new int[5];
In the following Try It Out you create and use an array of strings, using the example from the introduction to this section.
C:\BegVCSharp\Chapter05
.Program.cs
:static void Main(string[] args)
{
string[] friendNames = { "Todd Anthony", "Kevin Holton",
"Shane Laigle" };
int i;
WriteLine($"Here are {friendNames.Length} of my friends:");
for (i = 0; i < friendNames.Length; i++)
{
WriteLine(friendNames[i]);
}
ReadKey();
}
This code sets up a string
array with three values and lists them in the console in a for
loop. Note that you have access to the number of elements in the array using friendNames.Length
:
WriteLine($"Here are {friendNames.Length} of my friends:");
This is a handy way to get the size of an array. Outputting values in a for
loop is easy to get wrong. For example, try changing <
to <=
as follows:
for (i = 0; i <= friendNames.Length; i++)
{
WriteLine(friendNames[i]);
}
Compiling and executing the preceding code results in the dialog box shown in Figure 5.9.
Here, the code attempted to access friendNames[3]
. Remember that array indices start from 0, so the last element is friendNames[2]
. If you attempt to access elements outside of the array size, the code will fail. It just so happens that there is a more resilient method of accessing all the members of an array: using foreach
loops.
A foreach
loop enables you to address each element in an array using this simple syntax:
foreach (<baseType
> <name
> in <array
>)
{
// can use <name
> for each element
}
This loop will cycle through each element, placing it in the variable <
name
>
in turn, without danger of accessing illegal elements. You don't have to worry about how many elements are in the array, and you can be sure that you'll get to use each one in the loop. Using this approach, you can modify the code in the last example as follows:
static void Main(string[] args)
{
string[] friendNames = { "Todd Anthony", "Kevin Holton",
"Shane Laigle" };
WriteLine($"Here are {friendNames.Length} of my friends:");
foreach (string friendName in friendNames)
{
WriteLine(friendName);
}
ReadKey();
}
The output of this code will be exactly the same as that of the previous Try It Out. The main difference between using this method and a standard for
loop is that foreach
gives you read-only access to the array contents, so you can't change the values of any of the elements. You couldn't, for example, do the following:
foreach (string friendName in friendNames)
{
friendName = "Rupert the bear";
}
If you try this, compilation will fail. If you use a simple for
loop, however, you can assign values to array elements.
A multidimensional array is simply one that uses multiple indices to access its elements. For example, suppose you want to plot the height of a hill against the position measured. You might specify a position using two coordinates, x
and y
. You want to use these two coordinates as indices, such that an array called hillHeight
would store the height at each pair of coordinates. This involves using multidimensional arrays.
A two-dimensional array such as this is declared as follows:
<baseType
>[,] <name
>;
Arrays of more dimensions simply require more commas:
<baseType
>[,,,] <name
>;
This would declare a four-dimensional array. Assigning values also uses a similar syntax, with commas separating sizes. Declaring and initializing the two-dimensional array hillHeight
, with a base type of double
, an x
size of 3, and a y
size of 4 requires the following:
double[,] hillHeight = new double[3,4];
Alternatively, you can use literal values for initial assignment. Here, you use nested blocks of curly braces, separated by commas:
double[,] hillHeight = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };
This array has the same dimensions as the previous one — that is, three rows and four columns. By providing literal values, these dimensions are defined implicitly.
To access individual elements of a multidimensional array, you simply specify the indices separated by commas:
hillHeight[2,1]
You can then manipulate this element just as you can other elements. This expression will access the second element of the third nested array as defined previously (the value will be 4). Remember that you start counting from 0 and that the first number is the nested array. In other words, the first number specifies the pair of curly braces, and the second number specifies the element within that pair of braces. You can represent this array visually, as shown in Figure 5.10.
The foreach
loop gives you access to all elements in a multidimensional way, just as with single-dimensional arrays:
double[,] hillHeight = { { 1, 2, 3, 4 }, { 2, 3, 4, 5 }, { 3, 4, 5, 6 } };
foreach (double height in hillHeight)
{
WriteLine("{0}", height);
}
The order in which the elements are output is the same as the order used to assign literal values. This sequence is as follows (the element identifiers are shown here rather than the actual values):
hillHeight[0,0]
hillHeight[0,1]
hillHeight[0,2]
hillHeight[0,3]
hillHeight[1,0]
hillHeight[1,1]
hillHeight[1,2]
…
Multidimensional arrays, as discussed in the last section, are said to be rectangular because each “row” is the same size. Using the last example, you can have a y
coordinate of 0 to 3 for any of the possible x
coordinates.
It is also possible to have jagged arrays, whereby “rows” may be different sizes. For this, you need an array in which each element is another array. You could also have arrays of arrays of arrays, or even more complex situations. However, all this is possible only if the arrays have the same base type.
The syntax for declaring arrays of arrays involves specifying multiple sets of square brackets in the declaration of the array, as shown here:
int[][] jaggedIntArray;
Unfortunately, initializing arrays such as this isn't as simple as initializing multidimensional arrays. You can't, for example, follow the preceding declaration with this:
jaggedIntArray = new int[3][4];
Even if you could do this, it wouldn't be that useful because you can achieve the same effect with simple multidimensional arrays with less effort. Nor can you use code such as this:
jaggedIntArray = { { 1, 2, 3 }, { 1 }, { 1, 2 } };
You have two options. You can initialize the array that contains other arrays (let's call these sub-arrays for clarity) and then initialize the sub-arrays in turn:
jaggedIntArray = new int[2][];
jaggedIntArray[0] = new int[3];
jaggedIntArray[1] = new int[4];
Alternatively, you can use a modified form of the preceding literal assignment:
jaggedIntArray = new int[3][] { new int[] { 1, 2, 3 }, new int[] { 1 },
new int[] { 1, 2 } };
This can be simplified if the array is initialized on the same line as it is declared, as follows:
int[][] jaggedIntArray = { new int[] { 1, 2, 3 }, new int[] { 1 },
new int[] { 1, 2 } };
You can use foreach
loops with jagged arrays, but you often need to nest these to get to the actual data. For example, suppose you have the following jagged array that contains 10 arrays, each of which contains an array of integers that are divisors of an integer between 1 and 10:
int[][] divisors1To10 = { new int[] { 1 },
new int[] { 1, 2 },
new int[] { 1, 3 },
new int[] { 1, 2, 4 },
new int[] { 1, 5 },
new int[] { 1, 2, 3, 6 },
new int[] { 1, 7 },
new int[] { 1, 2, 4, 8 },
new int[] { 1, 3, 9 },
new int[] { 1, 2, 5, 10 } };
The following code will fail:
foreach (int divisor in divisors1To10)
{
WriteLine(divisor);
}
The failure occurs because the array divisors1To10
contains int[]
elements, not int
elements. Instead, you have to loop through every sub-array, as well as through the array itself:
foreach (int[] divisorsOfInt in divisors1To10)
{
foreach(int divisor in divisorsOfInt)
{
WriteLine(divisor);
}
}
As you can see, the syntax for using jagged arrays can quickly become complex! In most cases, it is easier to use rectangular arrays or a simpler storage method. Nonetheless, there may well be situations in which you are forced to use this method, and a working knowledge can't hurt. An example of this happens when working with XML documents where some elements have sub-children and other do not.
Your use of strings so far has consisted of writing strings to the console, reading strings from the console, and concatenating strings using the + operator. In the course of programming more interesting applications, you will discover that manipulating strings is something that you end up doing a lot. Therefore, it is worth spending a few pages looking at some of the more common string- manipulation techniques available in C#.
To start with, a string
type variable can be treated as a read-only array of char
variables. This means that you can access individual characters using syntax like the following:
string myString = "A string";
char myChar = myString[1];
However, you can't assign individual characters this way. To get a char
array that you can write to, you can use the following code. This uses the ToCharArray()
command of the array variable:
string myString = "A string";
char[] myChars = myString.ToCharArray();
Then you can manipulate the char
array the standard way. You can also use strings in foreach
loops, as shown here:
foreach (char character in myString)
{
WriteLine($"{character}");
}
As with arrays, you can also get the number of elements using myString.Length
. This gives you the number of characters in the string:
string myString = ReadLine();
WriteLine($"You typed {myString.Length} characters.");
Other basic string manipulation techniques use commands with a format similar to this <
string
>.ToCharArray()
command. Two simple, but useful, ones are <
string
>.ToLower()
and <
string
>.ToUpper()
. These enable strings to be converted into lowercase and uppercase, respectively. To see why this is useful, consider the situation in which you want to check for a specific response from a user — for example, the string yes
. If you convert the string entered by the user into lowercase, then you can also check for the strings YES
, Yes
, yeS
, and so on — you saw an example of this in the previous chapter:
string userResponse = ReadLine();
if (userResponse.ToLower() == "yes")
{
// Act on response.
}
This command, like the others in this section, doesn't actually change the string to which it is applied. Instead, combining this command with a string results in the creation of a new string, which you can compare to another string (as shown here) or assign to another variable. The other variable may be the same one that is being operated on:
userResponse = userResponse.ToLower();
This is an important point to remember, because just writing
userResponse.ToLower();
doesn't actually achieve very much!
There are other things you can do to ease the interpretation of user input. What if the user accidentally put an extra space at the beginning or end of the input? In this case, the preceding code won't work. You need to trim the string entered, which you can do using the <
string
>.Trim()
command:
string userResponse = ReadLine();
userResponse = userResponse.Trim();
if (userResponse.ToLower() == "yes")
{
// Act on response.
}
The preceding code is also able detect strings like this:
" YES"
"Yes "
You can also use these commands to remove any other characters, by specifying them in a char
array, for example:
char[] trimChars = {' ', 'e', 's'};
string userResponse = ReadLine();
userResponse = userResponse.ToLower();
userResponse = userResponse.Trim(trimChars);
if (userResponse == "y")
{
// Act on response.
}
This eliminates any occurrences of spaces, as well as the letters "e"
and "s"
from the beginning or end of your string. Providing there aren't any other characters in the string, this will result in the detection of strings such as
"Yeeeees"
" y"
and so on.
You can also use the <
string
>.TrimStart()
and <
string
>.TrimEnd()
commands, which will trim spaces from the beginning and end of a string, respectively. These can also have char
arrays specified.
You can use two other string commands to manipulate the spacing of strings: <
string
>.PadLeft()
and <
string
>.PadRight()
. They enable you to add spaces to the left or right of a string to force it to the desired length. You use them as follows:
<string
>.PadX(<desiredLength
>);
Here is an example:
myString = "Aligned";
myString = myString.PadLeft(10);
This would result in three spaces being added to the left of the word Aligned
in myString
. These methods can be helpful when aligning strings in columns, which is particularly useful for positioning strings containing numbers.
As with the trimming commands, you can also use these commands in a second way, by supplying the character to pad the string with. This involves a single char
, not an array of char
s as with trimming:
myString = "Aligned";
myString = myString.PadLeft(10, '-');
This would add three dashes to the start of myString
.
There are many more of these string-manipulation commands, many of which are only useful in very specific situations. These are discussed as you use them in the forthcoming chapters. Before moving on, though, it is worth looking at one of the features contained in Visual Studio 2015 that you may have noticed over the course of the last few chapters, and especially this one. In the following Try It Out, you examine auto-completion, whereby the IDE tries to help you out by suggesting what code you might like to insert.
C:\BegVCSharp\Chapter05
.Program.cs
, exactly as written, noting windows that pop up as you do so:static void Main(string[] args)
{
string myString = "This is a test.";
char[] separator = {' '};
string[] myWords;
myWords = myString.
}
sp
. The pop-up window changes, and the Tooltip shown in Figure 5.12 appears.
(se.
Another pop-up window and Tooltip appears, as shown in Figure 5.13.
);.
The code should look as follows, and the pop-up windows should disappear:static void Main(string[] args)
{
string myString = "This is a test.";
char[] separator = {' '};
string[] myWords;
myWords = myString.Split(separator);
}
static void Main(string[] args)
{
string myString = "This is a test.";
char[] separator = {' '};
string[] myWords;
myWords = myString.Split(separator);
foreach (string word in myWords)
{
WriteLine($"{word}");
}
ReadKey();
}
Two main aspects of this code are the new string command used and the use of the auto-completion functionality. The command, <string>.Split()
, converts a string
into a string
array by splitting it at the points specified. These points take the form of a char
array, which in this case is simply populated by a single element, the space character:
char[] separator = {' '};
The following code obtains the substrings you get when the string is split at each space — that is, you get an array of individual words:
string[] myWords;
myWords = myString.Split(separator);
Next, you loop through the words in this array using foreach
and write each one to the console:
foreach (string word in myWords)
{
WriteLine($"{word}");
}
Each word obtained has no spaces, either embedded in the word or at either end. The separators are removed when you use Split()
.
5.1 Which of the following conversions can't be performed implicitly?
int
to short
(b)short
to int
(c)bool
to string
(d)byte
to float
5.2 Show the code for a color
enumeration based on the short
type containing the colors of the rainbow plus black and white. Can this enumeration be based on the byte
type?
5.3 Will the following code compile? Why or why not?
string[] blab = new string[5]
blab[5] = 5th string.
5.4 Write a console application that accepts a string from the user and outputs a string with the characters in reverse order.
5.5 Write a console application that accepts a string and replaces all occurrences of the string no
with yes
.
5.6 Write a console application that places double quotes around each word in a string.
5.6 Answers to the exercises can be found in Appendix A.
Topic | Key Concept |
Type conversion | You can convert values from one type into another, but there are rules that apply when you do so. Implicit conversion happens automatically, but only when all possible values of the source value type are available in the target value type. Explicit conversion is also possible, but you run the risk of values not being assigned as expected, or even causing errors. |
Enumerations | Enums, or enumerations, are types that have a discrete set of values, each of which has a name. Enums are defined with the enum keyword. This makes them easy to understand in code because they are very readable. Enums have an underlying numeric type (int by default), and you can use this property of enum values to convert between enum values and numeric values, or to identify enum values. |
Structs | Structs, or structures, are types that contain several different values at the same time. Structs are defined with the struct keyword. The values contained in a struct each have a name and a type; there is no requirement that every value stored in a struct is the same type. |
Arrays | An array is a collection of values of the same type. Arrays have a fixed size, or length, which determines how many values they can contain. You can define multidimensional or jagged arrays to hold different amounts and shapes of data. You can also iterate through the values in an array with a foreach loop. |