Section 3.3. Interfaces and Generics


3.3. Interfaces and Generics

Like classes or structures, interfaces too can be defined in terms of generic type parameters.[*] Generic interfaces provide all the benefits of interface-based programming without compromising type safety, performance, or productivity. All of what you have seen so far with normal interfaces you can also do with generic interfaces. The main difference is that when deriving from a generic interface, you must provide a specific type parameter to use instead of the generic type parameter. For example, given this definition of the generic IList<T> interface:

[*] If you are not familiar with generics, Appendix D provides a concise overview.

     public interface IList<T>     {        void AddHead(T item);        void RemoveHead(T item);        void RemoveAll(  );     } 

you can implement the interface implicitly and substitute an integer for the generic type parameter:

     public class NumberList : IList<int>     {        public void AddHead(int item)        {...}        public void RemoveHead(int item)        {...}        public void RemoveAll(  )        {...}        //Rest of the implementation     } 

When the client uses IList<T>, it must choose an implementation of the interface with a specific type parameter:

     IList<int> list = new NumberList(  );     list.AddHead(3); 

Generic interfaces allow you to define an abstract service definition (the generic interface) once, yet use it on multiple components with multiple type parameters. For example, an integer-based list can implement the interface:

     public class NumberList : IList<int>     {...} 

And so can a string-based list:

     public class NameList : IList<string>     {...} 

Once a generic interface is bounded (i.e., once you've specified types for it) it is considered a distinct type. Consequently, two generic interface definitions with different generic type parameters are no longer polymorphic with each other. This means that a variable of the type IList<int> cannot be assigned to a variable or passed to a method that expects an IList<string>:

     void ProcessList(IList<strin; names)     {...}         IList<int> numbers = new NumberList(  );     ProcessList(numbers);//Does not compile 

You can maintain the polymorphism with generic interfaces if you keep the use of the interface in generic type parameter terms:

     public class ListClient<T>     {        public void ProcessList(IList<T> list)        {...}     }     IList<int>    numbers = new NumberList(  );     IList<string> names   w NameList(  );         ListClient<int>    numbersClient = new ListClient<int>(  );     ListClient<string> namesClient   = new ListClient<string>(  );         //Reuse of the code and algorithms of ProcessList(  ):     numbersClient.ProcessList(numbers);     namesClient.ProcessList(names); 

3.3.1. Deriving from a Generic Interface

When you derive an interface from a generic interface, you have a number of options as to how to define the sub-interface. Usually, you will prefer to have the sub-interface be a generic interface and provide the sub-interface's generic type parameters as type parameters to the base interface:

     public interface IBaseInterface<T>     {        void SomeMethod(T t);     }     public interface ISubInterface<T> : IBaseInterface<T>     {...} 

However, you can also specify a particular type to the base interface, thus making the sub-interface non-generic:

     public interface ISubInterface : IBaseInterface<string>     {...} 

Typically, when you derive from a generic interface you will do so in a generic subclass and let the client decide on the particular type parameters to use. This is an alternative to defining type-specific subclasses, and it does not limit the subclasses to the use of a particular type parameter:

     public class List<T> : IList<T>     {        public void AddHead(T item)        {...}        //Rest of the implementation     }     IList<int> numbers  = new List<int>(  );     IList<string> names = new List<string>(  ); 

When a generic class or a generic interface derives from a generic interface, it cannot specify multiple naked generic types to the interface:

     //Does not compile:     public class List<T,U> : IList<T>,IList<U>     {...} 

Doing so unifies the interfaces when the same type is specified, which violates the uniqueness of interfaces. If that were allowed, the compiler would not know how to resolve a definition such as this:

     List<int,int> list; 

3.3.2. Explicit Generic Interface Implementation

Just as with regular interfaces, you can implement a generic interface explicitly:

     public class NumberList : IList<int>     {        void IList<int>.AddHead(int item)        {...}        void IList<int>.RemoveHead(int item)        {...}        void IList<int>.RemoveAll(  )        {...}        //Rest of the implementation     } 

Note the specification of the type parameter in the explicit implementation:

     void IList<int>.AddHead(int item); 

This is required because the AddHead( ) method is not just a method of the generic interface IList<T>; rather, it is a method of the generic interface IList<T> with an integer as a type parameter. The same would be true if the implementing list were itself a generic class:

     public class List<T> : IList<T>     {        void IList<T>.AddHead(T item)        {...}        //Rest of the implementation     } 

Explicit generic interface implementation is especially handy when the same type implements multiple versions of the generic interface, each with a different concrete type parameter. Although you can use implicit interface implementation, like this:

     public class List : IList<int>,IList<string>     {        public void AddHead(int item)        {...}        public void AddHead(string item)        {...}        public void RemoveAll(  )        {...}        //Rest of the implementation     } 

it is preferable in such cases to use explicit interface implementation. The reason is that in methods such as RemoveAll( ), you channel the implementation of both IList<int> and IList<string> to the same method. The client can use either IList<int> or IList<string>:

     IList<int> list = new List(  );     list.RemoveAll(  ); 

Yet you have no way of knowing in the implementation of RemoveAll( ) which internal data structure you should clear when the client calls it. In addition, when you implement the same generic interface multiple times with different type parameters, you often have to use explicit implementation when the compiler cannot resolve the ambiguity. For example, if IList<T> has a method called GetHead( ), defined as:

     public interface IList<T>     {        void AddHead(T item);        void RemoveHead(T item);        void RemoveAll(  );        T GetHead(  );     } 

you must use explicit interface implementation, because in C# you cannot overload methods only by a different returned type:

     public class List : IList<int>,IList<string>     {        int IList<int>.GetHead(  )        {...}        string IList<string>.GetHead(  )        {...}        //Rest of the implementation     } 

3.3.3. Generic Interfaces as Operators

C# 2.0 has an additional interesting use for generic interfaces. In C# 2.0, it is impossible to use operators such as + or += on generic type parameters. For example, the following code does not compile because C# 2.0 does not have operator constraints:

     public class Calculator<T>     {        public T Add(T argument1,T argument2)        {           return argument1 + argument2;//Does not compile        }        //Rest of the methods     } 

The compiler does not know whether the type parameter the client will specify supports the + operator, so it will refuse to compile that code.

Nonetheless, you can compensate by using interfaces that define generic operations. Since an interface method cannot have any code in it, you can specify the generic operations at the interface level and provide a concrete type and implementation at the subclass level:

     public interface ICalculator<T>     {        T Add(T argument1,T argument2);        T Subtract(T argument1,T argument2);        T Divide(T argument1,T argument2);        T Multiply(T argument1,T argument2);     }     public class Calculator : ICalculator<int>     {        public int Add(int argument1, int argument2)        {           return argument1 + argument2;        }        //Rest oe methods     }     

3.3.4. Interface-Level Constraints

An interface can define constraints for the generic types it uses. For example:

     public interface IList<T> where T : IComparable<T>     {...} 

However, you should be very mindful about the implications of defining constraints at the scope of an interface. An interface should not have any shred of implementation details, to reinforce the notion of separation of interface from implementation. There are many ways to implement the generic interface, and the specific type parameters used are, after all, an implementation detail. Constraining them commonly couples the interface to specific implementation options.

For example, constraining a type parameter on an interface to derive from a particular class, like this, is a bad idea:

     public class Customer     {...}     public interface IList<T> where T : Customer     {...} 

This, in effect, makes the generic IList<T> useful only for managing lists of customers. If you want this level of strong typing with polymorphism, define a new interface dedicated to managing customers, instead of skewing the general-purpose definition of the generic IList<T>:

     public interface ICustomerList : IList<Customer>     {...} 

Constraining a default constructor, as follows, is also something to avoid:

     public interface IList<T> where T : new(  )     {...} 

Not all types have default public constructors, and the interface doesn't really care that they do not; the interface cannot contain any implementation code that uses such constructors anyway.

Even constraining an interface's generic type parameter to derive from another interface should be viewed with extreme caution. For example, you may think you should add a constraint for IComparable<T> to the list interface definition if you add a method that removes a specified item from the list:

     public interface IList<T> where T : IComparable<T>     {        void Remove(T item);        //Rest of the interface     } 

While implementing this method will often involve comparing the specified item to items in the list, which will in turn necessitate having the type parameter support IComparable<T>, this is still an implementation detail. Perhaps the implementing data structure has other ways of comparing the type parameters it uses, or perhaps it does not implement the method at all. All of that is irrelevant to the pure interface definition. Let the class implementing the generic interface add the constraint, and keep the interface itself constraint-free:

     public class List<T> : IList<T> where T : IComparable<T>     {        public void Remove(T item)        {...}        //Rest of the implementation      } 

3.3.5. Generic Derivation Constraints

When a generic class constrains one of its type parameters to derive from an interface, the client may provide as a specific type parameter a particular implementation of the interface:

     public class ListClient<L,T> where L : IList<T>     {        public void ProcessList(L list)        {...}     }     public class NumberList : IList<int>     {...}         ListClient<NumberList,int> client = new ListClient<NumberList,int>(  );     NumberList numbers = new NumberList(  );     client.ProcessList(numbers); 

However, you can also satisfy the constraint by specifying as a type parameter the very interface the type parameter is constrained against, not a particular implementation of it:

     public class ListClient<L,T> where L : IList<T>     {        public void ProcessList(L list)        {...}     }     public class List<T> : IListT>     {...}         ListClient<IList<int>,int> client = new ListClient<IList<int>,int>(  );     IList<int> numbers = new List<int>(  );     client.ProcessList(numbers); 

or even:

     public class AnotherClient<U>     {        ListClient<IList<U>,U> m_ListClient;     } 

This helps to separate the client code from particular interface implementations.

3.3.6. Generics, Interfaces, and Casting

The C# compiler will only let you implicitly cast generic type parameters to object, or to constraint-specified types, as shown in Example 3-11. Such implicit casting is of course type-safe, because any incompatibility is discovered at compile time.

Example 3-11. Implicit casting of generic type parameters
     public interface ISomeInterface     {...}     public class BaseClass     {...}     public class MyClass<T> where T : BaseClass,ISomeInterface     {        void SomeMethod(T t)        {           ISomeInterface obj1 = t;           BaseClass      obj2 = t;           object         obj3 = t;        }     } 

The compiler will let you explicitly cast generic type parameters to any other interface, but not to a class:

     public interface ISomeInterface     {...}     public class SomeClass     {...}     public class MyClass<T>     {        void SomeMethod(T t)        {           ISomeInterface obj1 = (ISomeInterface)t;//Compiles           SomeClass      obj2 = (SomeClass)t;     //Does not compile        }     } 

Such explicit casting is dangerous, because it may throw an exception at runtime if the specific type parameter used does not support the interface to which you explicitly cast. Instead of risking a casting exception, a better approach is to use the is and as operators, as shown in Example 3-12. You can use is and as both on naked generic type parameters and on generic classes with specific parameters.

Example 3-12. Using is and as operators on generic type parameters
     public interface IMyInterface     {...}     public interface ISomeInterface<T>     {...}     public class MyClass<T>     {        public void MyMethod(T t)        {           if(t is IMyInterface)           {...}               if(t is ISomeInterface<T>)           {...}               IMyInterface obj1 = t as IMyInterface;           if(obj1 != null)           {...}               ISomeInterface<T> obj2 = t as ISomeInterface<T>;           if(obj2 != null)           {...}        }     } 

3.3.7. Generic Interface Methods

In C# 2.0, an interface method can define generic type parameters, specific to its execution scope:

     public interface IMyInterface<T>     {        void MyMethod<X>(X x);     }     

This is an important capability, because it allows you to call the method with a different type every time, which is often very handy for utility classes.

You can define method-specific generic type parameters even if the containing interface does not use generics at all:

     public interface IMyInterface     {        void MyMethod<T>(T t);     } 

This ability is for methods only. Properties or indexers can use only generic type parameters defined at the scope of the interface.

When you call an interface method that defines generic type parameters, you can provide the type to use at the call site:

     public class MyClass : IMyInterface     {        public void MyMethod<T>(T t)        {...}     }     IMyInterface obj = new MyClass(  );     obj.MyMethod<int>(3);     

That said, when the method is invoked, the C# compiler is smart enough to infer the correct type based on the type of parameter passed in, and it allows you to omit the type specification altogether:

     IMyInterface obj = new MyClass(  );     obj.MyMethod(3); 

This ability is called generic type inference. Note that the compiler cannot infer the type based on the type of the returned value alone:

     public interface IMyInterface     {        T MyMethod<T>(  );     }     public class MyClass : IMyInterface     {        public T MyMethod<T>(  )        {...}     }     IMyInterface obj = new MyClass(  );     int number = obj.MyMethod(  );//Does not compile 

When an interface method defines its own generic type parameters, it can also define constraints for these types:

     public interface IMyInterface     {        void MyMethod<T>(T t) where T : IComparable<T>;     } 

However, my recommendation to avoid constraints at the interface level extends to method-level generic type parameters as well.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net