Item 3: Prefer the is or as Operators to CastsC# is a strongly typed language. Good programming practice means that we all try to avoid coercing one type into another when we can avoid it. But sometimes, runtime type checking is simply unavoidable. Many times in C#, you write functions that take System.Object parameters because the framework defines the method signature for you. You likely need to attempt to downcast those objects to other types, either classes or interfaces. You've got two choices: Use the as operator or use that old C standby, the cast. You also have a defensive variant: You can test a conversion with is and then use as or casts to convert it. The correct choice is to use the as operator whenever you can because it is safer than blindly casting and is more efficient at runtime. The as and is operators do not perform any user-defined conversions. They succeed only if the runtime type matches the sought type; they never construct a new object to satisfy a request. Take a look at an example. You write a piece of code that needs to convert an arbitrary object into an instance of MyType. You could write it this way: object o = Factory.GetObject( ); // Version one: MyType t = o as MyType; if ( t != null ) { // work with t, it's a MyType. } else { // report the failure. } Or, you could write this: object o = Factory.GetObject( ); // Version two: try { MyType t; t = ( MyType ) o; if ( t != null ) { // work with T, it's a MyType. } else { // Report a null reference failure. } } catch { // report the conversion failure. } You'll agree that the first version is simpler and easier to read. It does not have the try/catch clause, so you avoid both the overhead and the code. Notice that the cast version must check null in addition to catching exceptions. null can be converted to any reference type using a cast, but the as operator returns null when used on a null reference. So, with casts, you need to check null and catch exceptions. Using as, you simply check the returned reference against null. The biggest difference between the as operator and the cast operator is how user-defined conversions are treated. The as and is operators examine the runtime type of the object being converted; they do not perform any other operations. If a particular object is not the requested type or is derived from the requested type, they fail. Casts, on the other hand, can use conversion operators to convert an object to the requested type. This includes any built-in numeric conversions. Casting a long to a short can lose information. The same problems are lurking when you cast user-defined types. Consider this type: public class SecondType
{
private MyType _value;
// other details elided
// Conversion operator. // This converts a SecondType to // a MyType, see item 29. public static implicit operator MyType( SecondType t ) { return t._value; }
}
Suppose an object of SecondType is returned by the Factory.GetObject() function in the first code snippet: object o = Factory.GetObject( ); // o is a SecondType: MyType t = o as MyType; // Fails. o is not MyType if ( t != null ) { // work with t, it's a MyType. } else { // report the failure. } // Version two: try { MyType t1; t = ( MyType ) o; // Fails. o is not MyType if ( t1 != null ) { // work with t1, it's a MyType. } else { // Report a null reference failure. } } catch { // report the conversion failure. } Both versions fail. But I told you that casts will perform user-defined conversions. You'd think the cast would succeed. You're rightit should succeed if you think that way. But it fails because your compiler is generating code based on the compile-time type of the object, o. The compiler knows nothing about the runtime type of o; it views o as an instance of System.Object. The compiler sees that there is no user-defined conversion from System.Object to MyType. It checks the definitions of System.Object and MyType. Lacking any user-defined conversion, the compiler generates the code to examine the runtime type of o and checks whether that type is a MyType. Because o is a SecondType object, that fails. The compiler does not check to see whether the actual runtime type of o can be converted to a MyType object. You could make the conversion from SecondType to MyType succeed if you wrote the code snippet like this: object o = Factory.GetObject( ); // Version three: SecondType st = o as SecondType; try { MyType t; t = ( MyType ) st; if ( t != null ) { // work with T, it's a MyType. } else { // Report a null reference failure. } } catch { // report the failure. } You should never write this ugly code, but it does illustrate a common problem. Although you would never write this, you can use a System.Object parameter to a function that expects the proper conversions: object o = Factory.GetObject( ); DoStuffWithObject( o ); private void DoStuffWithObject( object o2 ) { try { MyType t; t = ( MyType ) o2; // Fails. o is not MyType if ( t != null ) { // work with T, it's a MyType. } else { // Report a null reference failure. } } catch { // report the conversion failure. } } Remember that user-defined conversion operators operate only on the compile-time type of an object, not on the runtime type. It does not matter that a conversion between the runtime type of o2 and MyType exists. The compiler just doesn't know or care. This statement has different behavior, depending on the declared type of st: t = ( MyType ) st; This next statement returns the same result, no matter what the declared type of st is. So, you should prefer as to castsit's more consistent. In fact, if the types are not related by inheritance, but a user-defined conversion operator exists, the following statement will generate a compiler error: t = st as MyType; Now that you know to use as when possible, let's discuss when you can't use it. The as operator does not work on value types. This statement won't compile: object o = Factory.GetValue( ); int i = o as int; // Does not compile. That's because ints are value types and can never be null. What value of int should be stored in i if o is not an integer? Any value you pick might also be a valid integer. Therefore, as can't be used. You're stuck with a cast: object o = Factory.GetValue( ); int i = 0; try { i = ( int ) o; } catch { i = 0; } But you're not necessarily stuck with the behaviors of casts. You can use the is statement to remove the chance of exceptions or conversions: object o = Factory.GetValue( ); int i = 0; if ( o is int ) i = ( int ) o; If o is some other type that can be converted to an int, such as a double, the is operator returns false. The is operator always returns false for null arguments. The is operator should be used only when you cannot convert the type using as. Otherwise, it's simply redundant: // correct, but redundant: object o = Factory.GetObject( ); MyType t = null; if ( o is MyType ) t = o as MyType; The previous code is the same as if you had written the following: // correct, but redundant: object o = Factory.GetObject( ); MyType t = null; if ( ( o as MyType ) != null ) t = o as MyType; That's inefficient and redundant. If you're about to convert a type using as, the is check is simply not necessary. Check the return from as against null; it's simpler. Now that you know the difference among is, as, and casts, which operator do you suppose the foreach loop uses? public void UseCollection( IEnumerable theCollection ) { foreach ( MyType t in theCollection ) t.DoStuff( ); } foreach uses a cast operation to perform conversions from an object to the type used in the loop. The code generated by the foreach statement equates to this hand-coded version: public void UseCollection( IEnumerable theCollection ) { IEnumerator it = theCollection.GetEnumerator( ); while ( it.MoveNext( ) ) { MyType t = ( MyType ) it.Current; t.DoStuff( ); } } foreach needs to use casts to support both value types and reference types. By choosing the cast operator, the foreach statement exhibits the same behavior, no matter what the destination type is. However, because a cast is used, foreach loops can generateBadCastExceptions. Because IEnumerator.Current returns a System.Object, which has no conversion operators, none is eligible for this test. A collection of SecondType objects cannot be used in the previous UseCollection() function because the conversion fails, as you already saw. The foreach statement (which uses a cast) does not examine the casts that are available in the runtime type of the objects in the collection. It examines only the conversions available in the System.Object class (the type returned by IEnumerator.Current) and the declared type of the loop variable (in this case, MyType). Finally, sometimes you want to know the exact type of an object, not just whether the current type can be converted to a target type. The as operator returns TRue for any type derived from the target type. The GetType() method gets the runtime type of an object. It is a more strict test than the is or as statement provides. GetType() returns the type of the object and can be compared to a specific type. Consider this function again: public void UseCollection( IEnumerable theCollection ) { foreach ( MyType t in theCollection ) t.DoStuff( ); } If you made a create a NewType class derived from MyType, a collection of NewType objects would work just fine in the UseCollection function: public class NewType : MyType { // contents elided. } If you mean to write a function that works with all objects that are instances of MyType, that's fine. If you mean to write a function that works only with MyType objects exactly, you should use the exact type for comparison. Here, you would do that inside the foreach loop. The most common time when the exact runtime type is important is when doing equality tests (see Item 9). In most other comparisons, the .isinst comparisons provided by as and is are semantically correct. Good object-oriented practice says that you should avoid converting types, but sometimes there are no alternatives. When you can't avoid the conversions, use the language's as and is operators to express your intent more clearly. Different ways of coercing types have different rules. The is and as statements are almost always the correct semantics, and they succeed only when the object being tested is the correct type. Prefer those statements to cast operators, which can have unintended side effects and succeed or fail when you least expect it. |