At some point developers have to decide whether to use generic class or generic method (or in other words whether to put type parameter on type level or method level).
Type parameter at type level (generic type type parameter) allows to enforce constraints for multiple members that use this type parameter. For example, IEnumerable<T> fixes what kind of enumerators it can produce to IEnumerator<T>.
public interface IEnumerable<T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Generic type may be used when it:
- implements generic interface
- has internal state that depends on type parameter
- type parameter is implicitly used as an internal state
You can also look at the alternatives from granularity point of view. Type parameter of a generic type has wider influence than type parameter of generic method. If type parameter makes sense only for a single member it doesn’t makes sense to put type parameter at a higher level than member that uses it. Otherwise you will force consumers of members to depend on type parameter and its constrains they may not use.
static class Util<T> where T: IComparable<T> { public static T Min(T left, T right) { return left.CompareTo(right) < 0 ? left : right; } public static void Swap(ref T left, ref T right) { var tmp = left; left = right; right = tmp; } } ... int i1 = 10, i2 = 15; var min = Util<int>.Min(i1, i2); object o1 = i1; object o2 = i2; // Not allowed and results in compilation error Util<object>.Swap(ref o1, ref o2);
In the case above type parameter introduces constrain which makes sense only for Min method and thus limits capabilities of Swap method (Swap can operate on any argument and Min can only operate on comparable arguments). On the other hand generic type argument must be specified explicitly and it doesn’t use advantages of type inference which simplifies usability of the API.
static class Util { public static T Min<T>(T left, T right) where T: IComparable<T> { return left.CompareTo(right) < 0 ? left : right; } public static void Swap<T>(ref T left, ref T right) { var tmp = left; left = right; right = tmp; } } ... int i1 = 10, i2 = 15; var min = Util.Min(i1, i2); object o1 = i1; object o2 = i2; // Now it is perfectly legal Util.Swap(ref o1, ref o2);
Changing type parameter level with respect to members that depend on it within inheritance hierarchy may lead to Design by Contract and Liskov Substitution Principle violation.
I came across an API which do the trick recently. In the abstracted example below ISomething states that its DoSomethingWith method can do something with enumerable of any T.
interface ISomething { void DoSomethingWith<T>(IEnumerable<T> target); }
But the implementation provides specialization on a fixed type parameter. It changed type parameter level from method to type level – DoSomethingWith method implementation depends on generic type type parameter whereas interface states method’s dependency from method type parameter.
class FixedSomething<U> : ISomething { private Func<U, bool> predicate; private IEnumerable<U> slice = Enumerable.Empty<U>(); public void DoSomethingWith<T>(IEnumerable<T> target) { if (typeof (U) != typeof (T)) // Cannot do something on argument of type T. U type is expected. throw new ArgumentException(); slice = target.Cast<U>().Where(predicate).Concat(slice); } //... other members elided for clarity }
It violates ISomething contract as it strengthens input parameter type constrain and introduces new exception that may be thrown. On the other hand it performs runtime type check which violates LSP.
Summary:
- DO use generic type when it implements generic interface or has internal state that depends on type parameter.
- CONSIDER type parameter granularity.
- DO NOT change type parameter level within inheritance hierarchy.
No comments:
Post a Comment