Safely Performing a Narrowing Numeric Cast

Safely Performing a Narrowing Numeric Cast

Problem

You need to cast a value from a larger value to a smaller one, while gracefully handling conditions that result in a loss of information. For example, casting a long to an int results in a loss of information only if the long data type is greater than int.MaxSize.

Solution

The simplest way to do this check is to use the checked keyword. The following method accepts two long data types and attempts to add them together. The result is stuffed into an int data type. If an overflow condition exists, the OverflowException is thrown:

using System;

public void UseChecked(long lhs, long rhs)
{
int result = 0;

try
{
result = checked((int)(lhs + rhs));
}
catch (OverflowException e)
{
// Handle overflow exception here.
}
}

This is the simplest method. However, if you do not want the overhead of throwing an exception and having to wrap a lot of code in try/catch blocks to handle the overflow condition, you can use the MaxValue and MinValue fields of each type. A check using these fields can be done prior to the conversion to insure that no loss of information occurs. If this does occur, the code can inform the application that this cast will cause a loss of information. You can use the following conditional statement to determine whether sourceValue can be cast to a short without losing any information:

// Our two variables are declared and initialized.
int sourceValue = 34000;
short destinationValue = 0;

// Determine if sourceValue will lose information in a cast to a short.
if (sourceValue <= short.MaxValue && sourceValue >= short.MinValue)
{
destinationValue = (short)sourceValue;
}
else
{
// Inform the application that a loss of information will occur.
}

Discussion

A narrowing conversion occurs when a larger type is cast down to a smaller type. For instance, consider casting a value of type Int32 to a value of type Int16. If the Int32 value is smaller than or equal to the Int16.MaxValue field and the Int32 value is higher than or equal to the Int16.MinValue field, the cast will occur without error or loss of information. Loss of information occurs when the Int32 value is larger than the Int16.MaxValue field or the Int32 value is lower than the Int16.MinValue field. In either of these cases, the most significant bits of the Int32 value are truncated and discarded, changing the value after the cast.

If a loss of information occurs in an unchecked context, it will occur silently without the application noticing. This problem can cause some very insidious bugs that are hard to trackdown. To prevent this, check the value to be converted to determine whether it is within the lower and upper bounds of the type that it will be cast to. If the value is outside these bounds, then code can be written to handle this situation. This code could force the cast not to occur and/or possibly inform the application of the casting problem. This solution can aid in the prevention of hard-to-find arithmetic bugs from appearing in your applications.

You should understand that both techniques shown in the Solution section are valid. However, the technique you use will depend on whether you expect to hit the overflow case on a regular basis or only occasionally. If you expect to hit the overflow case quite often, you might want to choose the second technique of manually testing the numeric value. Otherwise, it might be easier to use the checked keyword, as in the first technique.

 In C#, code can run in either a checked or unchecked context; by default, the code runs in an unchecked context. In a checked context, any arithmetic and conversions involving integral types are examined to determine whether an overflow condition exists. If so, an OverflowException is thrown. In an unchecked context, no OverflowException will be thrown when an overflow condition exists. A checked context can be set up by using the /checked{+} compiler switch, by setting the Check for Arithmetic Overflow/Underflow project property to true, or by using the checked keyword. An unchecked context can be set up using the /checked- compiler switch, by setting the Check for Arithmetic Overflow/Underflow project property to false, or by using the unchecked keyword.

Notice that floating-point and decimal types are not included in the code that handles the conversions to integral types in this recipe. The reason is that a conversion from any integral type to a float, double, or decimal will not lose any information; therefore, it is redundant to check these conversions.

In addition, you should be aware of the following when performing a conversion:

• Casting from a float, double, or decimal type to an integral type results in the truncation of the fractional portion of this number. Furthermore, if the integral portion of the number exceeds MaxValue for the target type, the result will be undefined unless the conversion is done in a checked context, in which case it will trigger an OverflowException.

• Casting from a float or double to a decimal results in the float or double being rounded to 28 decimal places.

• Casting from a double to a float results in the double being rounded to the nearest float value.

• Casting from a decimal to a float or double results in the decimal being rounded to the resulting type (float or double).

• Casting from an int, uint, or long to a float could result in the loss of precision, but never magnitude.

• Casting from a long to a double could result in the loss of precision, but never magnitude.