From Chapter 1’s HelloWorld
program, you got a feel for the C# language, its structure, basic syntax characteristics, and how to write the simplest of programs. This chapter continues to discuss the C# basics by investigating the fundamental C# types.
Until now, you have worked with only a few built-in data types, with little explanation. In C# thousands of types exist, and you can combine types to create new types. A few types in C#, however, are relatively simple and are considered the building blocks of all other types. These types are the predefined types. The C# language’s predefined types include eight integer types, two binary floating-point types for scientific calculations and one decimal float for financial calculations, one Boolean type, and a character type. This chapter investigates these types, looks more closely at the string
type, and introduces arrays.
The basic numeric types in C# have keywords associated with them. These types include integer types, floating-point types, and a special floating-point type called decimal
to store large numbers with no representation error.
There are eight C# integer types. This variety allows you to select a data type large enough to hold its intended range of values without wasting resources. Table 2.1 lists each integer type.
Included in Table 2.1 (and in Tables 2.2 and 2.3) is a column for the full name of each type; we discuss the literal suffix later in the chapter. All the fundamental types in C# have both a short name and a full name. The full name corresponds to the type as it is named in the Base Class Library (BCL). This name, which is the same across all languages, uniquely identifies the type within an assembly. Because of the fundamental nature of these types, C# also supplies keywords as short names or abbreviations to the full names of fundamental types. From the compiler’s perspective, both names refer to the same type, producing exactly the same code. In fact, an examination of the resultant CIL code would provide no indication of which name was used.
Although C# supports using both the full BCL name and the keyword, as developers we are left with the choice of which to use when. Rather than switching back and forth, it is better to use one or the other consistently. For this reason, C# developers generally go with using the C# keyword form—choosing, for example, int
rather than System.Int32
and string
rather than System.String
(or a possible shortcut of String
).
Guidelines
DO use the C# keyword rather than the BCL name when specifying a data type (for example, string
rather than String
).
DO favor consistency rather than variety within your code.
The choice for consistency frequently may be at odds with other guidelines. For example, given the guideline to use the C# keyword in place of the BCL name, there may be occasions when you find yourself maintaining a file (or library of files) with the opposite style. In these cases it would better to stay consistent with the previous style than to inject a new style and inconsistencies in the conventions. Even so, if the “style” was actually a bad coding practice that was likely to introduce bugs and obstruct successful maintenance, by all means correct the issue throughout.
Language Contrast: C++—short Data Type
In C/C++, the short
data type is an abbreviation for short int
. In C#, short
on its own is the actual data type.
Floating-point numbers have varying degrees of precision, and binary floating-point types can represent numbers exactly only if they are a fraction with a power of 2 as the denominator. If you were to set the value of a floating-point variable to be 0.1, it could very easily be represented as 0.0999999999999999 or 0.10000000000000001 or some other number very close to 0.1. Similarly, setting a variable to a large number such as Avogadro’s number, 6.02 × 1023, could lead to a representation error of approximately 108, which after all is a tiny fraction of that number. The accuracy of a floating-point number is in proportion to the magnitude of the number it represents. A floating-point number is precise to a certain number of significant digits, not by a fixed value such as ±0.01.
C# supports the two binary floating-point number types listed in Table 2.2.
Binary numbers appear as base 10 (denary) numbers for human readability. The number of bits (binary digits) converts to 15 decimal digits, with a remainder that contributes to a sixteenth decimal digit as expressed in Table 2.2. Specifically, numbers between 1.7 × 10307 and less than 1 × 10308 have only 15 significant digits. However, numbers ranging from 1 × 10308 to 1.7 × 10308 will have 16 significant digits. A similar range of significant digits occurs with the decimal
type as well.
C# also provides a decimal floating-point type with 128-bit precision (see Table 2.3). This type is suitable for financial calculations.
Unlike binary floating-point numbers, the decimal
type maintains exact accuracy for all denary numbers within its range. With the decimal
type, therefore, a value of 0.1 is exactly 0.1. However, while the decimal
type has greater precision than the floating-point types, it has a smaller range. Thus, conversions from floating-point types to the decimal
type may result in overflow errors. Also, calculations with decimal
are slightly (generally imperceptibly) slower.
A literal value is a representation of a constant value within source code. For example, if you want to have System.Console.WriteLine()
print out the integer value 42
and the double
value 1.618034
, you could use the code shown in Listing 2.1.
System.Console.WriteLine(42);
System.Console.WriteLine(1.618034);
Output 2.1 shows the results of Listing 2.1.
42
1.618034
By default, when you specify a literal number with a decimal point, the compiler interprets it as a double
type. Conversely, a literal value with no decimal point generally defaults to an int
, assuming the value is not too large to be stored in an integer. If the value is too large, the compiler will interpret it as a long
. Furthermore, the C# compiler allows assignment to a numeric type other than an int
, assuming the literal value is appropriate for the target data type. short s = 42
and byte b = 77
are allowed, for example. However, this is appropriate only for constant values; b = s
is not allowed without additional syntax, as discussed in the section Conversions between Data Types later in this chapter.
As previously discussed in the section Fundamental Numeric Types, there are many different numeric types in C#. In Listing 2.2, a literal value is placed within C# code. Since numbers with a decimal point will default to the double
data type, the output, shown in Output 2.2, is 1.61803398874989
(the last digit, 5, is missing), corresponding to the expected accuracy of a double
.
System.Console.WriteLine(1.618033988749895);
1.61803398874989
To view the intended number with its full accuracy, you must declare explicitly the literal value as a decimal
type by appending an M
(or m
) (see Listing 2.3 and Output 2.3).
System.Console.WriteLine(1.618033988749895M);
1.618033988749895
Now the output of Listing 2.3 is as expected: 1.618033988749895
. Note that d
is for double
. To remember that m
should be used to identify a decimal
, remember that “m is for monetary calculations.”
You can also add a suffix to a value to explicitly declare a literal as float
or double
by using the F
and D
suffixes, respectively. For integer data types, the suffixes are U
, L
, LU
, and UL
. The type of an integer literal can be determined as follows:
• Numeric literals with no suffix resolve to the first data type that can store the value in this order: int
, uint
, long
, and ulong
.
• Numeric literals with the suffix U
resolve to the first data type that can store the value in the order uint
and then ulong
.
• Numeric literals with the suffix L
resolve to the first data type that can store the value in the order long
and then ulong
.
• If the numeric literal has the suffix UL
or LU
, it is of type ulong
.
Note that suffixes for literals are case insensitive. However, uppercase is generally preferred to avoid any ambiguity between the lowercase letter l and the digit 1.
In some situations, you may wish to use exponential notation instead of writing out several zeroes before or after the decimal point. To use exponential notation, supply the e
or E
infix, follow the infix character with a positive or negative integer number, and complete the literal with the appropriate data type suffix. For example, you could print out Avogadro’s number as a float
, as shown in Listing 2.4 and Output 2.4.
System.Console.WriteLine(6.023E23F);
6.023E+23
Guidelines
DO use uppercase literal suffixes (for example, 1.618033988749895M).
In all discussions of literal numeric values so far, we have covered only base 10 type values. C# also supports the ability to specify hexadecimal values. To specify a hexadecimal value, prefix the value with 0x
and then use any hexadecimal digit, as shown in Listing 2.5.
// Display the value 42 using a hexadecimal literal.
System.Console.WriteLine(0x002A);
Output 2.5 shows the results of Listing 2.5.
42
Note that this code still displays 42
, not 0x002A
.
The fundamental types discussed so far are numeric types. C# includes some additional types as well: bool
, char
, and string
.
Another C# primitive is a Boolean or conditional type, bool
, which represents true or false in conditional statements and expressions. Allowable values are the keywords true
and false
. The BCL name for bool
is System.Boolean
. For example, in order to compare two strings in a case-insensitive manner, you call the string.Compare()
method and pass a bool
literal true
(see Listing 2.8).
string option;
...
int comparison = string.Compare(option, "/Help", true);
In this case, you make a case-insensitive comparison of the contents of the variable option with the literal text /Help and assign the result to comparison.
Although theoretically a single bit could hold the value of a Boolean, the size of bool
is 1 byte.
A char
type represents 16-bit characters whose set of possible values are drawn from the Unicode character set’s UTF-16 encoding. A char
is the same size as a 16-bit unsigned integer (ushort
), which represents values between 0 and 65,535. However, char
is a unique type in C# and code should treat it as such.
The BCL name for char
is System.Char
.
To construct a literal char
, place the character within single quotes, as in 'A'
. Allowable characters comprise the full range of keyboard characters, including letters, numbers, and special symbols.
Some characters cannot be placed directly into the source code and instead require special handling. These characters are prefixed with a backslash () followed by a special character code. In combination, the backslash and special character code constitute an escape sequence. For example,
represents a newline, and
represents a tab. Since a backslash indicates the beginning of an escape sequence, it can no longer identify a simple backslash; instead, you need to use \
to represent a single backslash character.
Listing 2.9 writes out one single quote because the character represented by '
corresponds to a single quote.
class SingleQuote
{
static void Main()
{
System.Console.WriteLine(''');
}
}
In addition to showing the escape sequences, Table 2.4 includes the Unicode representation of characters.
You can represent any character using Unicode encoding. To do so, prefix the Unicode value with u
. You represent Unicode characters in hexadecimal notation. The letter A, for example, is the hexadecimal value 0x41
. Listing 2.10 uses Unicode characters to display a smiley face (:)
), and Output 2.8 shows the results.
System.Console.Write('u003A');
System.Console.WriteLine('u0029');
:)
A finite sequence of zero or more characters is called a string. The string type in C# is string
, whose BCL name is System.String
. The string type includes some special characteristics that may be unexpected to developers familiar with other programming languages. In addition to the string literal format discussed in Chapter 1, strings include a “verbatim string” prefix character of @
, string interpolation with the $ prefix character, and the fact that strings are immutable.
You can enter a literal string into code by placing the text in double quotes ("
), as you saw in the HelloWorld
program. Strings are composed of characters, and because of this, character escape sequences can be embedded within a string.
In Listing 2.11, for example, two lines of text are displayed. However, instead of using System.Console.WriteLine()
, the code listing shows System.Console.Write()
with the newline character,
. Output 2.9 shows the results.
class DuelOfWits
{
static void Main()
{
System.Console.Write(
""Truly, you have a dizzying intellect."");
System.Console.Write("
"Wait 'til I get going!"
");
}
}
"Truly, you have a dizzying intellect."
"Wait 'til I get going!"
The escape sequence for double quotes differentiates the printed double quotes from the double quotes that define the beginning and end of the string.
In C#, you can use the @ symbol in front of a string to signify that a backslash should not be interpreted as the beginning of an escape sequence. The resultant verbatim string literal does not reinterpret just the backslash character. Whitespace is also taken verbatim when using the @ string syntax. The triangle in Listing 2.12, for example, appears in the console exactly as typed, including the backslashes, newlines, and indentation. Output 2.10 shows the results.
class Triangle
{
static void Main()
{
System.Console.Write(@"begin
/
/
/
/
/________
end");
}
}
begin
/
/
/
/
/________
end
Without the @ character, this code would not even compile. In fact, even if you changed the shape to a square, eliminating the backslashes, the code still would not compile because a newline cannot be placed directly within a string that is not prefaced with the @ symbol.
The only escape sequence the verbatim string does support is ""
, which signifies double quotes and does not terminate the string.
Language Contrast: C++—String Concatenation at Compile Time
Unlike C++, C# does not automatically concatenate literal strings. You cannot, for example, specify a string literal as follows:
"Major Strasser has been shot."
"Round up the usual suspects."
Rather, concatenation requires the use of the addition operator. (If the compiler can calculate the result at compile time, however, the resultant CIL code will be a single string.)
If the same literal string appears within an assembly multiple times, the compiler will define the string only once within the assembly and all variables will refer to the same string. That way, if the same string literal containing thousands of characters was placed multiple times into the code, the resultant assembly would reflect the size of only one of them.
Begin 6.0
As discussed in Chapter 1, strings can support embedded expressions when using the string interpolation format. The string interpolation syntax prefixes a string literal with a dollar symbol and then embeds the expressions within curly brackets. The following is an example:
System.Console.WriteLine($"Your full name is {firstName} {lastName}.");
where firstName
and lastName
are simple expressions that refer to variables.
Note that string literals can be combined with string interpolation by specifying the “$
” prior to the “@
” symbol, as in this example:
System.Console.WriteLine($@"Your full name is:
{ firstName } { lastName }");
Since this is a string literal, the text output on two lines. You can, however, make a similar line break in the code without incurring a line break in the output by placing the line feeds inside the curly braces as follows:
System.Console.WriteLine($@"Your full name is: {
firstName } { lastName }");
End 6.0
The string type, like the System.Console
type, includes several methods. There are methods, for example, for formatting, concatenating, and comparing strings.
The Format()
method in Table 2.5 behaves exactly like the Console.Write()
and Console.WriteLine()
methods, except that instead of displaying the result in the console window, string.Format()
returns the result to the caller. Of course, with string interpolation the need for string.Format()
is significantly reduced (except for localization support). Under the covers, however, string interpolation compiles down to CIL that leverages string.Format()
.
All of the methods in Table 2.5 are static. This means that, to call the method, it is necessary to prefix the method name (for example, Concat
) with the type that contains the method (for example, string
). As illustrated below, however, some of the methods in the string class are instance methods. Instead of prefixing the method with the type, instance methods use the variable name (or some other reference to an instance). Table 2.6 shows a few of these methods, along with an example.
End 6.0
Whether you use string.Format()
or the C# 6.0 string interpolation feature to construct complex formatting strings, a rich and complex set of formatting patterns is available to display numbers, dates, times, timespans, and so on. For example, if price
is a variable of type decimal
, then string.Format("{0,20:C2}", price)
or the equivalent interpolation $"{price,20:C2}"
both convert the decimal value to a string using the default currency formatting rules, rounded to two figures after the decimal place, and right-justified in a 20-character-wide string. Space does not permit a detailed discussion of all the possible formatting strings; consult the MSDN documentation for string.Format()
for a complete listing of formatting strings.
If you want an actual left or right curly brace inside an interpolated string or formatted string, you can double the brace to indicate that it is not introducing a pattern. For example, the interpolated string $"{{ {price:C2} }}"
might produce the string "{ $1,234.56 }"
.
When writing out a new line, the exact characters for the new line will depend on the operating system on which you are executing. On Microsoft Windows platforms, the newline is the combination of both the carriage return (
)
and line feed (
)
characters, while a single line feed is used on UNIX. One way to overcome the discrepancy between platforms is simply to use System.Console.WriteLine()
to output a blank line. Another approach, which is almost essential for a new line on multiple platforms when you are not outputting to the console, is to use System.Environment.NewLine
. In other words, System.Console.WriteLine("Hello World")
and System.Console.Write($"Hello World{System.Environment.NewLine}")
are equivalent.
To determine the length of a string, you use a string member called Length
. This particular member is called a read-only property. As such, it cannot be set, nor does calling it require any parameters. Listing 2.14 demonstrates how to use the Length
property, and Output 2.11 shows the results.
class PalindromeLength
{
static void Main()
{
string palindrome;
System.Console.Write("Enter a palindrome: ");
palindrome = System.Console.ReadLine();
System.Console.WriteLine(
$"The palindrome "{palindrome}" is"
+ $" {palindrome.Length} characters.");
}
}
Enter a palindrome: Never odd or even
The palindrome "Never odd or even" is 17 characters.
The length for a string cannot be set directly; it is calculated from the number of characters in the string. Furthermore, the length of a string cannot change because a string is immutable.
A key characteristic of the string
type is that it is immutable. A string variable can be assigned an entirely new value but there is no facility for modifying the contents of a string
. It is not possible, therefore, to convert a string
to all uppercase letters. It is trivial to create a new string that is composed of an uppercase version of the old string, but the old string is not modified in the process. Consider Listing 2.15 as an example.
class Uppercase
{
static void Main()
{
string text;
System.Console.Write("Enter text: ");
text = System.Console.ReadLine();
// UNEXPECTED: Does not convert text to uppercase
text.ToUpper();
System.Console.WriteLine(text);
}
}
Output 2.12 shows the results of Listing 2.15.
Enter text: This is a test of the emergency broadcast system.
This is a test of the emergency broadcast system.
At a glance, it would appear that text.ToUpper()
should convert the characters within text
to uppercase. However, strings are immutable and, therefore, text.ToUpper()
will make no such modification. Instead, text.ToUpper()
returns a new string that needs to be saved into a variable or passed to System.Console.WriteLine()
directly. The corrected code is shown in Listing 2.16, and its output is shown in Output 2.13.
class Uppercase
{
static void Main()
{
string text, uppercase;
System.Console.Write("Enter text: ");
text = System.Console.ReadLine();
// Return a new string in uppercase
uppercase = text.ToUpper();
System.Console.WriteLine(uppercase);
}
}
Enter text: This is a test of the emergency broadcast system.
THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM.
If the immutability of a string is ignored, mistakes similar to those shown in Listing 2.15 can occur with other string methods as well.
To actually change the value of text
, assign the value from ToUpper()
back into text
, as in the following code:
text = text.ToUpper();
If considerable string modification is needed, such as when constructing a long string in multiple steps, you should use the data type System.Text.StringBuilder
rather than string
. The StringBuilder
type includes methods such as Append()
, AppendFormat()
, Insert()
, Remove()
, and Replace()
, some of which are also available with string
. The key difference, however, is that with StringBuilder
these methods will modify the data in the StringBuilder
itself, and will not simply return a new string.
Two additional keywords relating to types are null
and void
. The null
value identified with the null
keyword, indicates that the variable does not refer to any valid object. void
is used to indicate the absence of a type or the absence of any value altogether.
null
can also be used as a type of string “literal.” null
indicates that a variable is set to nothing. Reference types, pointer types, and nullable value types can be assigned the value null
. The only reference type covered so far in this book is string
; Chapter 5 covers the topic of creating classes (which are reference types) in detail. For now, suffice it to say that a variable of reference type contains a reference to a location in memory that is different from the value of the variable. Code that sets a variable to null
explicitly assigns the reference to refer to no valid value. In fact, it is even possible to check whether a reference refers to nothing. Listing 2.17 demonstrates assigning null
to a string
variable.
static void Main()
{
string faxNumber;
// ...
// Clear the value of faxNumber.
faxNumber = null;
// ...
}
Assigning the value null
to a reference type is not equivalent to not assigning it at all. In other words, a variable that has been assigned null
has still been set, whereas a variable with no assignment has not been set and, therefore, will often cause a compile error if used prior to assignment.
Assigning the value null
to a string
variable is distinctly different from assigning an empty string, ""
. Use of null
indicates that the variable has no value, whereas ""
indicates that there is a value—an empty string. This type of distinction can be quite useful. For example, the programming logic could interpret a faxNumber
of null
to mean that the fax number is unknown, while a faxNumber
value of ""
could indicate that there is no fax number.
Sometimes the C# syntax requires a data type to be specified but no data is actually passed. For example, if no return from a method is needed, C# allows you to specify void
as the data type instead. The declaration of Main
within the HelloWorld
program is an example. The use of void
as the return type indicates that the method is not returning any data and tells the compiler not to expect a value. void
is not a data type per se, but rather an indication that there is no data being returned.
Language Contrast: C++
In both C++ and C#, void
has two meanings: as a marker that a method does not return any data, and to represent a pointer to a storage location of unknown type. In C++ programs it is quite common to see pointer types like void**
. C# can also represent pointers to storage locations of unknown type using the same syntax, but this usage is comparatively rare in C# and typically encountered only when writing programs that interoperate with unmanaged code libraries.
Language Contrast: Visual Basic—Returning void Is Like Defining a Subroutine
The Visual Basic equivalent of returning a void
in C# is to define a subroutine (Sub
/End Sub
) rather than a function that returns a value.
End 3.0
All types fall into one of two categories: value types and reference types. The differences between the types in each category stem from how they are copied: Value type data is always copied by value, while reference type data is always copied by reference.
With the exception of string
, all the predefined types in the book so far have been value types. Variables of value types contain the value directly. In other words, the variable refers to the same location in memory where the value is stored. Because of this, when a different variable is assigned the same value, a copy of the original variable’s value is made to the location of the new variable. A second variable of the same value type cannot refer to the same location in memory as the first variable. Consequently, changing the value of the first variable will not affect the value in the second. Figure 2.1 demonstrates this. In the figure, number1
refers to a particular location in memory that contains the value 42
. After assigning number1
to number2
, both variables will contain the value 42
. However, modifying either variable’s value will not affect the other.
Similarly, passing a value type to a method such as Console.WriteLine()
will also result in a memory copy, and any changes to the parameter inside the method will not affect the original value within the calling function. Since value types require a memory copy, they generally should be defined to consume a small amount of memory; value types should almost always be less than 16 bytes in size.
By contrast, the value of a reference type is a reference to a storage location that contains data. Reference types store the reference where the data is located instead of storing the data directly, as value types do. Therefore, to access the data, the runtime will read the memory location out of the variable and then “jump” to the location in memory that contains the data. The memory area of the data a reference type points to is called the heap (see Figure 2.2).
A reference type does not require the same memory copy of the data that a value type does, which makes copying reference types far more efficient than copying large value types. When assigning the value of one reference type variable to another reference type variable, only the reference is copied, not the data referred to. In practice, a reference is always the same size as the “native size” of the processor: A 32-bit processor will copy a 32-bit reference and a 64-bit processor will copy a 64-bit reference, and so on. Obviously, copying the small reference to a large block of data is faster than copying the entire block, as a value type would.
Since reference types copy a reference to data, two different variables can refer to the same data. If two variables refer to the same object, changing a field of the object through one variable causes the effect to be seen when accessing the field via another variable. This happens both for assignment and for method calls. Therefore, a method can affect the data of a reference type, and that change can be observed when control returns to the caller. For this reason, a key factor when choosing between defining a reference type or a value type is whether the object is logically like an immutable value of fixed size (and therefore possibly a value type), or logically a mutable thing that can be referred to (and therefore likely to be a reference type).
Besides string
and any custom classes such as Program
, all types discussed so far are value types. However, most types are reference types. Although it is possible to define custom value types, it is relatively rare to do so in comparison to the number of custom reference types.
Begin 2.0
Value types cannot usually be assigned null
because, by definition, they cannot contain references, including references to nothing. However, this presents a problem because we frequently wish to represent values that are “missing.” When specifying a count, for example, what do you enter if the count is unknown? One possible solution is to designate a “magic” value, such as -1
or int.MaxValue
, but these are valid integers. Rather, it is desirable to assign null
to the value type because it is not a valid integer.
To declare variables of value type that can store null
, you use the nullable modifier, ?
. This feature, which was introduced with C# 2.0, appears in Listing 2.20.
static void Main()
{
int? count = null;
do
{
// ...
}
while(count == null);
}
Assigning null
to value types is especially attractive in database programming. Frequently, value type columns in database tables allow null
values. Retrieving such columns and assigning them to corresponding fields within C# code is problematic, unless the fields can contain null
as well. Fortunately, the nullable modifier is designed to handle such a scenario specifically.
End 2.0
Given the thousands of types predefined in the various CLI implementations and the unlimited number of types that code can define, it is important that types support conversion from one type to another where it makes sense. The most common operation that results in a conversion is casting.
Consider the conversion between two numerical types: converting from a variable of type long
to a variable of type int
. A long
type can contain values as large as 9,223,372,036,854,775,808; however, the maximum size of an int
is 2,147,483,647. As such, that conversion could result in a loss of data—for example, if the variable of type long
contains a value greater than the maximum size of an int
. Any conversion that could result in a loss of magnitude or an exception because the conversion failed, requires an explicit cast. Conversely, a conversion operation that will not lose magnitude and will not throw an exception regardless of the operand types is an implicit conversion.
In C#, you cast using the cast operator. By specifying the type you would like the variable converted to within parentheses, you acknowledge that if an explicit cast is occurring, there may be a loss of precision and data, or an exception may result. The code in Listing 2.21 converts a long
to an int
and explicitly tells the system to attempt the operation.
With the cast operator, the programmer essentially says to the compiler, “Trust me, I know what I am doing. I know that the value will fit into the target type.” Making such a choice will cause the compiler to allow the conversion. However, with an explicit conversion, there is still a chance that an error, in the form of an exception, might occur while executing if the data is not converted successfully. It is, therefore, the programmer’s responsibility to ensure the data is successfully converted, or else to provide the necessary error-handling code when the conversion fails.
You cannot convert any type to any other type simply because you designate the conversion explicitly using the cast operator. The compiler will still check that the operation is valid. For example, you cannot convert a long
to a bool
. No such conversion is defined, and therefore, the compiler does not allow such a cast.
Language Contrast: Converting Numbers to Booleans
It may be surprising to learn that there is no valid cast from a numeric type to a Boolean type, since this is common in many other languages. The reason no such conversion exists in C# is to avoid any ambiguity, such as whether –1 corresponds to true or false. More importantly, as you will see in the next chapter, this constraint reduces the chance of using the assignment operator in place of the equality operator (avoiding if(x=42){...}
when if(x==42){...}
was intended, for example).
In other instances, such as when going from an int
type to a long
type, there is no loss of precision and no fundamental change in the value of the type occurs. In these cases, the code needs to specify only the assignment operator; the conversion is implicit. In other words, the compiler is able to determine that such a conversion will work correctly. The code in Listing 2.25 converts from an int
to a long
by simply using the assignment operator.
int intNumber = 31416;
long longNumber = intNumber;
Even when no explicit cast operator is required (because an implicit conversion is allowed), it is still possible to include the cast operator (see Listing 2.26).
int intNumber = 31416;
long longNumber = (long) intNumber;
No conversion is defined from a string to a numeric type, so methods such as Parse()
are required. Each numeric data type includes a Parse()
function that enables conversion from a string to the corresponding numeric type. Listing 2.27 demonstrates this call.
string text = "9.11E-31";
float kgElectronMass = float.Parse(text);
Another special type is available for converting one type to the next. This type is System.Convert
, and an example of its use appears in Listing 2.28.
string middleCText = "261.626";
double middleC = System.Convert.ToDouble(middleCText);
bool boolean = System.Convert.ToBoolean(middleC);
System.Convert
supports only a small number of types and is not extensible. It allows conversion from any of the types bool
, char
, sbyte
, short
, int
, long
, ushort
, uint
, ulong
, float
, double
, decimal
, DateTime
, and string
to any other of those types.
Furthermore, all types support a ToString()
method that can be used to provide a string representation of a type. Listing 2.29 demonstrates how to use this method. The resultant output is shown in Output 2.18.
bool boolean = true;
string text = boolean.ToString();
// Display "True"
System.Console.WriteLine(text);
True
For the majority of types, the ToString()
method will return the name of the data type rather than a string representation of the data. The string representation is returned only if the type has an explicit implementation of ToString()
. One last point to make is that it is possible to code custom conversion methods, and many such methods are available for classes in the runtime.
Begin 2.0
End 2.0
One particular aspect of variable declaration that Chapter 1 didn’t cover is array declaration. With array declaration, you can store multiple items of the same type using a single variable and still access them individually using the index when required. In C#, the array index starts at zero. Therefore, arrays in C# are zero based.
Arrays are a fundamental part of nearly every programming language, so they are required learning for virtually all developers. Although arrays are frequently used in C# programming, and necessary for the beginner to understand, most programs now use generic collection types rather than arrays when storing collections of data. Therefore, readers should skim over the following section, “Declaring an Array,” simply to become familiar with their instantiation and assignment rather. Table 2.7 provides the highlights of what to note. Generic collections will be covered in detail in Chapter 14.
In addition, the final section of the chapter, “Common Array Errors,” provides a review of some of the array idiosyncrasies.
In C#, you declare arrays using square brackets. First, you specify the element type of the array, followed by open and closed square brackets; then you enter the name of the variable. Listing 2.31 declares a variable called languages
to be an array of strings.
string[] languages;
Obviously, the first part of the array identifies the data type of the elements within the array. The square brackets that are part of the declaration identify the rank, or the number of dimensions, for the array; in this case, it is an array of rank one. These two pieces form the data type for the variable languages
.
Language Contrast: C++ and Java—Array Declaration
The square brackets for an array in C# appear immediately following the data type instead of after the variable declaration. This keeps all the type information together instead of splitting it up both before and after the identifier, as occurs in C++ and Java.
Listing 2.31 defines an array with a rank of 1. Commas within the square brackets define additional dimensions. Listing 2.32, for example, defines a two-dimensional array of cells for a game of chess or tic-tac-toe.
// | |
// ---+---+---
// | |
// ---+---+---
// | |
int[,] cells;
In Listing 2.32, the array has a rank of 2. The first dimension could correspond to cells going across and the second dimension represents cells going down. Additional dimensions are added, with additional commas, and the total rank is one more than the number of commas. Note that the number of items that occur for a particular dimension is not part of the variable declaration. This is specified when creating (instantiating) the array and allocating space for each element.
Once an array is declared, you can immediately fill its values using a comma-delimited list of items enclosed within a pair of curly braces. Listing 2.33 declares an array of strings and then assigns the names of nine languages within curly braces.
string[] languages = { "C#", "COBOL", "Java",
"C++", "Visual Basic", "Pascal",
"Fortran", "Lisp", "J#"};
The first item in the comma-delimited list becomes the first item in the array; the second item in the list becomes the second item in the array, and so on. The curly brackets are the notation for defining an array literal.
The assignment syntax shown in Listing 2.33 is available only if you declare and assign the value within one statement. To assign the value after declaration requires the use of the keyword new
as shown in Listing 2.34.
string[] languages;
languages = new string[]{"C#", "COBOL", "Java",
"C++", "Visual Basic", "Pascal",
"Fortran", "Lisp", "J#" };
Starting in C# 3.0, specifying the data type of the array (string
) following new
is optional as long as the compiler is able to deduce the element type of the array from the types of the elements in the array initializer. The square brackets are still required.
C# also allows use of the new
keyword as part of the declaration statement, so it allows the assignment and the declaration shown in Listing 2.35.
string[] languages = new string[]{
"C#", "COBOL", "Java",
"C++", "Visual Basic", "Pascal",
"Fortran", "Lisp", "J#"};
The use of the new
keyword tells the runtime to allocate memory for the data type. It instructs the runtime to instantiate the data type—in this case, an array.
Whenever you use the new
keyword as part of an array assignment, you may also specify the size of the array within the square brackets. Listing 2.36 demonstrates this syntax.
string[] languages = new string[9]{
"C#", "COBOL", "Java",
"C++", "Visual Basic", "Pascal",
"Fortran", "Lisp", "J#"};
The array size in the initialization statement and the number of elements contained within the curly braces must match. Furthermore, it is possible to assign an array but not specify the initial values of the array, as demonstrated in Listing 2.37.
string[] languages = new string[9];
Assigning an array but not initializing the initial values will still initialize each element. The runtime initializes elements to their default values, as follows:
• Reference types (such as string
) are initialized to null
.
• Numeric types are initialized to zero.
• bool
is initialized to false
.
• char
is initialized to