Conversions between Data Types

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.

Explicit Cast

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.

Listing 2.26: Explicit Cast Example
long longNumber = 50918309109;
int intNumber = (int)longNumber; // (int) is a cast operator.

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.

AdVanced Topic
Checked and Unchecked Conversions

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: Overflowing an Integer Value
// int.MaxValue equals 2147483647
int n = int.MaxValue;
n = n + 1;
Console.WriteLine(n);
Output 2.15
-2147483648

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.

Listing 2.28: A Checked Block Example
checked
{
    // int.MaxValue equals 2147483647
    int n = int.MaxValue;
    n = n + 1;
    Console.WriteLine(n);
}
Output 2.16
Unhandled Exception: System.OverflowException: Arithmetic operation resulted in an overflow at Program.Main() in ...Program.cs:line 12

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).

Listing 2.29: An Unchecked Block Example
unchecked
{
    // int.MaxValue equals 2147483647
    int n = int.MaxValue;
    n = n + 1;
    Console.WriteLine(n);
}
Output 2.17
-2147483648

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.

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 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).

Implicit Conversion

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.

Listing 2.30: Not Using the Cast Operator for an Implicit Conversion
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.31).

Listing 2.31: Using the Cast Operator for an Implicit Cast
int intNumber = 31416;
long longNumber = (long)intNumber;
Type Conversion without Casting

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.

Listing 2.32: Using float.Parse() to Convert a string to a Numeric Data Type
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.33.

Listing 2.33: Type Conversion Using System.Convert
string middleCText = $"{261.626}";
double middleC = Convert.ToDouble(middleCText);
bool boolean = 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.

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.

Listing 2.34: Using ToString() to Convert to a string
bool boolean = true;
string text = boolean.ToString();
// Display "True"
Console.WriteLine(text);
Output 2.18
True

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.

AdVanced Topic
TryParse()

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.

Listing 2.35: Using TryParse() in Place of an Invalid Cast Exception
double number;
string input;
 
Console.Write("Enter a number: ");
input = Console.ReadLine();
if (double.TryParse(input, out number))
{
    // Converted correctly, now use number
    // ...
}
else
{
    Console.WriteLine(
        "The text entered was not a valid number.");
}
Output 2.19
Enter a number: forty-two
The text entered was not a valid number.

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.

Listing 2.36: Using TryParse() with Inline out Declaration in C# 7.0
// double number;
string input;
 
Console.Write("Enter a number: ");
input = Console.ReadLine();
if (double.TryParse(input, out double number))
{
    Console.WriteLine(
        $"input was parsed successfully to {number}.");
}
else
{
    // Note: number scope is here too (although not assigned)
    Console.WriteLine(
        "The text entered was not a valid number.");
}
 
Console.WriteLine(
    $"'number' currently has the value: {number}");

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.)

________________________________________

7. Starting with C# 2.0.
{{ snackbarMessage }}