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 10 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.
Type |
Size |
Range (Inclusive) |
BCL Name |
Signed |
Literal Suffix |
sbyte |
8 bits |
–128 to 127 |
System.SByte |
Yes |
|
byte |
8 bits |
0 to 255 |
System.Byte |
No |
|
short |
16 bits |
–32,768 to 32,767 |
System.Int16 |
Yes |
|
ushort |
16 bits |
0 to 65,535 |
System.UInt16 |
No |
|
int |
32 bits |
–2,147,483,648 to 2,147,483,647 |
System.Int32 |
Yes |
|
uint |
32 bits |
0 to 4,294,967,295 |
System.UInt32 |
No |
U or u |
Long |
64 bits |
–9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
System.Int64 |
Yes |
L or l |
ulong |
64 bits |
0 to 18,446,744,073,709,551,615 |
System.UInt64 |
No |
UL or ul |
nint |
Signed 32-bit or 64-bit integer is a [depends on platform] |
Depends on platform where the code is executing. Use sizeof(nint) to retrieve size. |
System.IntPtr |
Yes |
|
nuint |
Unsigned 32-bit or 64-bit integer is a [depends on platform] |
Depends on platform where the code is executing. Use sizeof(nuint) to retrieve size. |
System.UIntPtr |
No |
|
Included in Table 2.1 (and all the type tables within this section) 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 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.
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 digits of precision, not by a fixed value such as ±0.01. There are at most 17 significant digits for a double and 9 significant digits for a float1 (assuming the number wasn’t converted from a string as described in the “Advanced Topic: Floating-Point Types Dissected”). 2
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. While there are additional floating-point types beyond the scope of this book (see https://learn.microsoft.com/dotnet/standard/numerics), they are not built-in types with associated keywords.
Type |
Size |
Significant Digits |
BCL Name |
Significant Digits |
Literal Suffix |
float |
32 bits |
±1.5 × 10−45 to ±3.4 × 1038 |
System.Single |
7 |
F or f |
double |
64 bits |
±5.0 × 10−324 to ±1.7 × 10308 |
System.Double |
15–16 |
D or d |
Denary numbers within the range and precision limits of the decimal type are represented exactly. In contrast, the binary floating-point representation of many denary numbers introduces a rounding error. Just as ⅓ cannot be represented exactly in any finite number of decimal digits, so ¹¹⁄₁₀ cannot be represented exactly in any finite number of binary digits (the binary representation being 1.0001100110011001101…). In both cases, we end up with a rounding error of some kind.
A decimal is represented by ±N * 10k where the following is true:
In contrast, a binary float is any number ±N * 2k where the following is true:
C# also provides a decimal floating-point type with 128-bit precision (see Table 2.3). This type is suitable for financial calculations.
Type |
Size |
Range (Inclusive) |
BCL Name |
Significant Digits |
Literal Suffix |
decimal |
128 bits |
1.0 × 10−28 to approximately 7.9 × 1028 |
System.Decimal |
28–29 |
M or m |
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.
C# 9.0 added new contextual keywords to represent native-sized signed and unsigned integers, specifically nint and nuint respectively starting in C# 11. (See Table 2.4.) Unlike the other numeric types that are the same size regardless of the underlying operating system, the native sized integer types will vary depending on what platform the code is executing. A nint, for example, will be 32-bits on a 32-bit platform and 64-bit on a 64-bit platform. These types are designed to match the size of a pointer within the system on which they are executing. They are a more advanced type because they are generally only useful when working with pointers and memory in the underlying operating system rather than memory within the managed execution context of .NET.
Type |
Size |
Range (Inclusive) |
BCL Name |
Significant Digits |
Literal Suffix |
nint |
Matches Operating System (OS) |
Variable but available at runtime via nint.MinValue and nint.MaxValue |
System.IntPtr |
Matches OS |
none |
nuint |
Matches Operating System (OS) |
Variable but available at runtime via unint.MinValue and unint.MaxValue |
System.UIntPtr |
Matches OS |
none |
See Chapter 23 for more information on nint and nuint.
A literal value is a representation of a constant value within source code. For example, if you want to have Console.WriteLine() print out the integer value 42 and the double value 1.618034, you could use the code shown in Listing 2.1 with Output 2.1.
The practice of placing a value directly into source code is called hardcoding, because changing the values requires recompiling the code. Developers must carefully consider the choice between hardcoding values within their code and retrieving them from an external source, such as a configuration file, so that the values are modifiable without recompiling.
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 interprets 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 passed to the WriteLine method. Since numbers with a decimal point will default to the double data type, the output, shown in Output 2.2, is 1.618033988749895 (the last two digits, 48, are now 5), corresponding to the expected accuracy of a double.
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).
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 (or f) and D (or 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:
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.
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.
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.
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.
Usually, you work with numbers that are represented with a base of 10, meaning there are 10 symbols (0–9) for each place value in the number. If a number is displayed with hexadecimal notation, it is displayed with a base of 16 numbers, meaning 16 symbols are used: 0–9, A–F (or in lowercase). Therefore, 0x000A corresponds to the decimal value 10 and 0x002A corresponds to the decimal value 42, being 2 × 16 + 10. The actual number is the same. Switching from hexadecimal to decimal, or vice versa, does not change the number itself—just the representation of the number.
Each hex digit is four bits, so a byte can represent two hex digits.
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.
Output 2.5 shows the results of Listing 2.6. Note that this code still displays 42, not 0x002A.
Starting with C# 7.0, you can also represent numbers as binary values (see Listing 2.7).
The syntax is like the hexadecimal syntax except with 0b as the prefix (an uppercase B is also allowed). See “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.
To display a numeric value in its hexadecimal format, it is necessary to use the x or X numeric formatting specifier. The casing determines whether the hexadecimal letters appear in lowercase or uppercase. Listing 2.8 with Output 2.6 shows an example of how to do this.
Note that the numeric literal (42) can be in decimal or hexadecimal form. The result will be the same. Also, to achieve the hexadecimal formatting, we rely on the formatting specifier, separated from the string interpolation expression with a colon.
By default, Console.WriteLine(1.618033988749895); displays 1.61803398874989, with the last digit missing. To more accurately identify the string representation of the double value, it is possible to convert it using a format string and the round-trip format specifier, R (or r). For example, Console.WriteLine($"{1.618033988749895:R}") will display 1.6180339887498949.
The round-trip format specifier returns a string that, if converted back into a numeric value, will always result in the original value. Listing 2.9 with Output 2.7 shows the numbers are not equal without the use of the round-trip format.
When assigning text the first time, there is no round-trip format specifier; as a result, the value returned by double.Parse(text) is not the same as the original number value. In contrast, when the round-trip format specifier is used, double.Parse(text) returns the original value.
For those readers who are unfamiliar with the == syntax from C-based languages, result == number evaluates to true if result is equal to number, while result != number does the opposite. Both assignment and equality operators are discussed in the next chapter.
________________________________________