Given the thousands of types predefined in .NET 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 numeric 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 data (such as magnitude and/or precision) or an exception because the conversion failed requires an explicit cast. Conversely, a numeric conversion 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.26 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 either a data loss or an error, in the form of an exception, might occur during execution. 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.
C# provides special keywords for marking a code block to indicate what should happen if the target data type is too small to contain the assigned data. By default, if the target data type cannot contain the assigned data, the data will be truncated during assignment. For an example, see Listing 2.27 and Output 2.15.
Listing 2.27 writes the value -2147483648 to the console. However, placing the code within a checked block, or using the checked option when running the compiler, will cause the runtime to throw an exception of type System.OverflowException. The syntax for a checked block uses the checked keyword, as shown in Listing 2.28 with Output 2.16.
The result is that an exception is thrown if, within the checked block, an overflow assignment occurs at runtime.
To change the default checked behavior from unchecked to checked, add the <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> element to the .csproj file. C# also supports an unchecked block that overflows the data instead of throwing an exception for assignments within the block (see Listing 2.29 and Output 2.17).
Even if the checked option is on during compilation, the unchecked keyword in the preceding code will prevent the runtime from throwing an exception during execution.
Readers might wonder why, when adding 1 to int.MaxValue unchecked, the result yields −2147483648. The behavior is caused by wraparound semantics. The binary representation of int.MaxValue is 01111111111111111111111111111111, where the first digit (0) indicates a positive value. Incrementing this value yields the next value of 10000000000000000000000000000000, the smallest integer (int.MinValue), where the first digit (1) signifies the number is negative. Adding 1 to int.MinValue would result in 10000000000000000000000000000001 (−2147483647) and so on.
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.
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 important, as you will see in the next chapter, this constraint reduces the chance of using the assignment operator in place of the equality operator (e.g., avoiding if(x=42){…} when if(x==42){...} was intended).
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 can determine that such a conversion will work correctly. The code in Listing 2.30 converts from an int to a long by simply using the assignment operator.
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.31).
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.32 demonstrates this call.
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.33.
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.
All types support a ToString() method that can be used to provide a string representation of a type. Listing 2.34 demonstrates how to use this method. The resultant output is shown in Output 2.18.
For the majority of types, the ToString() method returns 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.
All the numeric primitive types include a static TryParse() method.7 This method is similar to the Parse() method, except that instead of throwing an exception if the conversion fails, the TryParse() method returns false, as demonstrated in Listing 2.35 and Output 2.19.
The resultant value that the code parses from the input string is returned via an out parameter (see “Output Parameters (out)” in Chapter 5)—in this case, number.
In addition to the various number types, the TryParse() method is available for enums.
Starting with C# 7.0, it is no longer necessary to declare a variable before using it as an out argument. Using this feature, the declaration for number is shown in Listing 2.36.
Notice that the data type of number is specified following the out modifier and before the variable that it declares. The result is that the number variable is available from both the true and false consequences of the if statement and even outside the if statement.
The key difference between Parse() and TryParse() is that TryParse() won’t throw an exception if it fails. Frequently, the conversion from a string to a numeric type depends on a user entering the text. It is expected, in such scenarios, that the user will enter invalid data that will not parse successfully. By using TryParse() rather than Parse(), you can avoid throwing exceptions in expected situations. (The expected situation in this case is that the user will enter invalid data, and we try to avoid throwing exceptions for expected scenarios.)
________________________________________