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 and looks more closely at the string
type.
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, as shown in Table 2.1. This variety allows you to select a data type large enough to hold its intended range of values without wasting resources.
Table 2.1: Integer Types
Type |
Size |
Range (Inclusive) |
BCL Name |
Signed |
Literal Suffix |
|
8 bits |
–128 to 127 |
|
Yes |
|
|
8 bits |
0 to 255 |
|
No |
|
|
16 bits |
–32,768 to 32,767 |
|
Yes |
|
|
16 bits |
0 to 65,535 |
|
No |
|
|
32 bits |
–2,147,483,648 to 2,147,483,647 |
|
Yes |
|
|
32 bits |
0 to 4,294,967,295 |
|
No |
|
|
64 bits |
–9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
|
Yes |
|
|
64 bits |
0 to 18,446,744,073,709,551,615 |
|
No |
|
|
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 for the full names of fundamental types. From the compiler’s perspective, both names refer to the same type, producing identical code. In fact, an examination of the resultant Common Intermediate Language (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 use the C# keyword form—choosing, for example, int
rather than System.Int32
and string
rather than System.String
(or a possible shortcut of String
).
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 be 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 a bad coding practice that was likely to introduce bugs and obstruct successful maintenance, by all means correct the issue throughout.
Begin 8.0
float
, double
)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. Starting with .NET Core 3.0, there are at most 17 significant digits for a double
and 9 significant digits for a float
(assuming the number wasn’t converted from a string as described in the Advanced Block: Floating-Point Types Dissected). 1
1. Prior to .NET Core 3.0, 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# supports the two binary floating-point number types listed in Table 2.2. Binary numbers appear as base 10 (denary) numbers for human readability.
Table 2.2: Floating-Point Types
Type |
Size |
Range (Inclusive) |
BCL Name |
Significant Digits |
Literal Suffix |
|
32 bits |
±1.5 × 10−45 to ±3.4 × 1038 |
|
7 |
|
|
64 bits |
±5.0 × 10−324 to ±1.7 × 10308 |
|
15–16 |
|
|
End 8.0
A decimal
is represented by ±N * 10
k where the following is true:
N
, the mantissa, is a positive 96-bit integer.
k
, the exponent, is given by -28 <= k <= 0
.
In contrast, a binary float is any number ±N * 2
k where the following is true:
N
is a positive 24-bit (for float
) or 53-bit (for double
) integer.
k
is an integer ranging from -149
to +104
for float
and from -1074
to +970
for double
.
C# also provides a decimal floating-point type with 128-bit precision (see Table 2.3). This type is suitable for financial calculations.
Table 2.3: Decimal Type
Type |
Size |
Range (Inclusive) |
BCL Name |
Significant Digits |
Literal Suffix |
|
128 bits |
1.0 × 10−28 to approximately 7.9 × 1028 |
|
28–29 |
|
|
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.
Output 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 a 32-bit 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 this section, 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);
Output 2.2
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);
Output 2.3
1.61803398874985
Now the output of Listing 2.3 is as expected: 1.618033988749895
. Note that d
is the abbreviation 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 a 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
.
Begin 7.0
On occasion, numbers can get quite large and difficult to read. To overcome the readability problem, C# 7.0 added support for a digit separator, an underscore (_
), when expressing a numeric literal, as shown in Listing 2.4.
System.Console.WriteLine(9_814_072_356);
In this case, we separate the digits into thousands (threes), but this is not required by C#. You can use the digit separator to create whatever grouping you like as long as the underscore occurs between the first and last digits. In fact, you can even have multiple underscores side by side—with no digit between them.
End 7.0
In addition, you may wish to use exponential notation instead of writing out several zeroes before or after the decimal point (whether using a digit separator or not). 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.5 and Output 2.4.
System.Console.WriteLine(6.023E23F);
Output 2.4
6.023E+23
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 series of digits, as shown in Listing 2.6.
// Display the value 42 using a hexadecimal literal
System.Console.WriteLine(0x002A);
Output 2.5 shows the results of Listing 2.6. Note that this code still displays 42
, not 0x002A
.
Output 2.5
42
Begin 7.0
Starting with C# 7.0, you can also represent numbers as binary values (see Listing 2.7).
// Display the value 42 using a binary literal
System.Console.WriteLine(0b101010);
The syntax is like the hexadecimal syntax except with 0b
as the prefix (an uppercase B
is also allowed). See the Beginner Topic: Bits and Bytes in Chapter 4 for an explanation of binary notation and the conversion between binary and decimal.
Note that starting with C# 7.2, you can place the digit separator after the x
for a hexadecimal literal or the b
for a binary literal.
End 7.0
The fundamental types discussed so far are numeric types. C# includes some additional types as well: bool
, char
, and string
.
bool
)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, to compare two strings in a case-insensitive manner, you call the string.Compare()
method and pass a bool
literal true
(see Listing 2.10).
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.
char
)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.11 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.
Table 2.4: Escape Characters
Escape Sequence |
Character Name |
Unicode Encoding |
|
Single quote |
|
|
Double quote |
|
|
Backslash |
|