Starting a new series to parallel the Little Wonders series. In this series, I will examine some of the small pitfalls that can occasionally trip up developers. Introduction: Of Casts and Conversions What happens when we try to assign from an int and a double and vice-versa? 1: double pi = 3.14;
2: int theAnswer = 42;
3:
4: // implicit widening conversion, compiles!
5: double doubleAnswer = theAnswer;
6:
7: // implicit narrowing conversion, compiler error!
8: int intPi = pi;
As you can see from the comments above, a conversion from a value type where there is no potential data loss is can be done with an implicit conversion. However, when converting from one value type to another may result in a loss of data, you must make the conversion explicit so the compiler knows you accept this risk.
That is why the conversion from double to int will not compile with an implicit conversion, we can make the conversion explicit by adding a cast:
1: // explicit narrowing conversion using a cast, compiler
2: // succeeds, but results may have data loss:
3: int intPi = (int)pi;
So for value types, the conversions (implicit and explicit) both convert the original value to a new value of the given type. With widening and narrowing references, however, this is not the case.
Converting reference types is a bit different from converting value types. First of all when you perform a widening or narrowing you don’t really convert the instance of the object, you just convert the reference itself to the wider or narrower reference type, but both the original and new reference type both refer back to the same object.
Secondly, widening and narrowing for reference types refers the going down and up the class hierarchy instead of referring to precision as in value types. That is, a narrowing conversion for a reference type means you are going down the class hierarchy (for example from Shape to Square) whereas a widening conversion means you are going up the class hierarchy (from Square to Shape).
1: var square = new Square();
2:
3: // implicitly convers because all squares are shapes
4: // (that is, all subclasses can be referenced by a superclass reference)
5: Shape myShape = square;
6:
7: // implicit conversion not possible, not all shapes are squares!
8: // (that is, not all superclasses can be referenced by a subclass reference)
9: Square mySquare = (Square) myShape;
So we had to cast the Shape back to Square because at that point the compiler has no way of knowing until runtime whether the Shape in question is truly a Square. But, because the compiler knows that it’s possible for a Shape to be a Square, it will compile. However, if the object referenced by myShape is not truly a Square at runtime, you will get an invalid cast exception.
Of course, there are other forms of conversions as well such as user-specified conversions and helper class conversions which are beyond the scope of this post. The main thing we want to focus on is this seemingly innocuous casting method of widening and narrowing conversions that we come to depend on every day and, in some cases, can bite us if we don’t fully understand what is going on!
The Pitfall: Conversions on Boxed Value Types Can Fail
What if you saw the following code and – knowing nothing else – you were asked if it was legal or not, what would you think:
1: // assuming x is defined above this and this
2: // assignment is syntactically legal.
3: x = 3.14;
4:
5: // convert 3.14 to int.
6: int truncated = (int)x;
You may think that since x is obviously a double (can’t be a float) because 3.14 is a double literal, but this is inaccurate. Our x could also be dynamic and this would work as well, or there could be user-defined conversions in play. But there is another, even simpler option that can often bite us: what if x is object?
1: object x;
2:
3: x = 3.14;
4:
5: int truncated = (int) x;
On the surface, this seems fine. We have a double and we place it into an object which can be done implicitly through boxing (no cast) because all types inherit from object. Then we cast it to int. This theoretically should be possible because we know we can explicitly convert a double to an int through a conversion process which involves truncation.
But here’s the pitfall: when casting an object to another type, we are casting a reference type, not a value type! This means that it will attempt to see at runtime if the value boxed and referred to by x is of type int or derived from type int. Since it obviously isn’t (it’s a double after all) we get an invalid cast exception!
Now, you may say this looks awfully contrived, but in truth we can run into this a lot if we’re not careful. Consider using an IDataReader to read from a database, and then attempting to select a result row of a particular column type:
1: using (var connection = new SqlConnection("some connection string"))
2: using (var command = new SqlCommand("select * from employee", connection))
3: using (var reader = command.ExecuteReader())
4: {
5: while (reader.Read())
6: {
7: // if the salary is not an int32 in the SQL database, this is an error!
8: // doesn't matter if short, long, double, float, reader [] returns object!
9: total += (int) reader["annual_salary"];
10: }
11: }
Notice that since the reader indexer returns object, if we attempt to convert using a cast to a type, we have to make darn sure we use the true, actual type or this will fail! If the SQL database column is a double, float, short, etc this will fail at runtime with an invalid cast exception because it attempts to convert the object reference!
So, how do you get around this? There are two ways, you could first cast the object to its actual type (double), and then do a narrowing cast to on the value to int. Or you could use a helper class like Convert which analyzes the actual run-time type and will perform a conversion as long as the type implements IConvertible.
1: object x;
2:
3: x = 3.14;
4:
5: // if you want to cast, must cast out of object to double, then
6: // cast convert.
7: int truncated = (int)(double) x;
8:
9: // or you can call a helper class like Convert which examines runtime
10: // type of the value being converted
11: int anotherTruncated = Convert.ToInt32(x);
Summary
You should always be careful when performing a conversion cast from values boxed in object that you are actually casting to the true type (or a sub-type).
Since casting from object is a widening of the reference, be careful that you either know the exact, explicit type you expect to be held in the object, or instead avoid the cast and use a helper class to perform a safe conversion to the type you desire.
Technorati Tags: C#,.NET,Pitfalls,Little Pitfalls,BlackRabbitCoder