#1,148 – When to Use a Generic Covariant Interface

You can use a generic covariant interface as a workaround for the lack of covariance in generic types.

Suppose that we have a generic Pile<T> type as follows:

    public class Pile<T> where T: class
    {
        List<T> pile = new List<T>();

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public T GetFirst()
        {
            return (pile.Count > 0) ? pile[0] : null;
        }
    }

Because this generic type doesn’t support covariance, we can run into the problem shown below.

        static void Main(string[] args)
        {
            Pile<Terrier> lilDogs = new Pile<Terrier>();
            lilDogs.Add(new Terrier("Jack"));
            lilDogs.Add(new Terrier("Eddie"));

            // Compile error: can't convert from Pile<Terrier> to
            // Pile<Dog>
            ShowFirstDog(lilDogs);
        }

        static void ShowFirstDog(Pile<Dog> dogs)
        {
            Console.WriteLine(dogs.GetFirst().Name);
        }

We could write a method that took a Pile<Terrier>, rather than Pile<Dog>, but the it still might be useful to have a Pile<Dog> version that could handle various subtypes of Dog.

The workaround is to add an interface to Pile<T> that contains only methods that have T as an output parameter.  The interface marks the T parameter with the out keyword.  The updated code is shown below.

    public interface IGetFirst<out T>
    {
        T GetFirst();
    }

    public class Pile<T> : IGetFirst<T>
        where T: class
    {
        List<T> pile = new List<T>();

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public T GetFirst()
        {
            return (pile.Count > 0) ? pile[0] : null;
        }
    }

We can now use IGetFirst<T> covariantly, rather than trying to use Pile<T> covariantly.

        static void Main(string[] args)
        {
            Pile<Terrier> lilDogs = new Pile<Terrier>();
            lilDogs.Add(new Terrier("Jack"));
            lilDogs.Add(new Terrier("Eddie"));

            // This works !
            ShowFirstDog(lilDogs);
        }

        static void ShowFirstDog(IGetFirst<Dog> dogs)
        {
            Console.WriteLine(dogs.GetFirst().Name);
        }

1184-001

Advertisement

#1,147 – Why Generics Don’t Support Covariance

In C#, arrays support covariance, but generics do not.  Generics aren’t covariant in order to avoid the problem that we have with covariant arrays.  With arrays, we get type mismatch exceptions when trying to put something into the array of the wrong type.  The core problem here is that the array looks syntactically like an array of the base type, but is actually an array of the more derived type.

Generics in C# aren’t covariant in order to avoid this same problem.  The language designers decided to support covariance for arrays but not for generics.  The reasons are likely more historical than technical.

            // Array covariance--OK
            Dog[] dogs = new Terrier[5];

            // The problem with array covariance.
            // Compiler allows, but throws ArrayTypeMismatchException
            // at run-time
            dogs[0] = new BorderCollie("Shep");

            // Generic covariance--not allowed (compiler error)
            List<Dog> moreDogs = new List<Terrier>();

#1,146 – Generics Don’t Support Covariance

In C#, arrays are covariant, so you can do the following:

            // Array covariance--OK
            Dog[] dogs = new Terrier[5];

Generics in C#, however, are not covariant (they are invariant). The following code will not compile.

            // Generic covariance--not OK (compiler error)
            List<Dog> moreDogs = new List<Terrier>();

#1,145 – Using Reflection on Generic Type Parameters

You can use the typeof operator to get information about one or more type parameters within a generic type.  In the example below, we get information about the type argument used when the generic type is constructed.

    public class Pile<T>
    {
        List<T> pile = new List<T>();

        public Pile()
        {
            Type t = typeof(T);
            Console.WriteLine("Constructing Pile<T> type, with T as [{0}]", t.Name);
        }

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }
    }

Here’s the output when we construct Pile<Dog> and Pile<int>:

            Pile<Dog> pack = new Pile<Dog>();
            Pile<int> justNumbers = new Pile<int>();

1145-001

#1,144 – Getting Type Information about a Generic Type

You can use the typeof operator to get information about a particular type.  The operator returns an instance of the Type class, which you can then query to get info about the type.

You can get type information about generic types in two different ways.  You can use the name of the type with empty angle brackets to get information about the generic type.  Or you can supply type arguments to get information about a particular constructed type.

        private static void DumpInfoForType(Type t)
        {
            Console.WriteLine("Type {0}:", t.Name);
            Console.WriteLine("  IsGenericType: {0}", t.IsGenericType);
            Console.WriteLine("  IsGenericTypeDefinition: {0}", t.IsGenericTypeDefinition);
            Console.WriteLine("  IsConstructedGenericType: {0}", t.IsConstructedGenericType);
            Console.WriteLine("  ContainsGenericParameters: {0}", t.ContainsGenericParameters);

            if (t.IsConstructedGenericType)
            {
                foreach (Type targ in t.GenericTypeArguments)
                    Console.WriteLine("Arg: {0}", targ.Name);
            }
        }

        static void Main(string[] args)
        {
            DumpInfoForType(typeof(Pile<>));
            DumpInfoForType(typeof(Pile<Dog>));
        }

The first type is a generic type definition with generic parameters. The second is a constructed generic type with a Dog argument.

1144-001

#1,143 – Implement IEquatable in a Generic Type

You can use the EqualityComparer<T> type to implement IEquatable<T> for a generic type.

In the example below, we implement IEquatable<TwoThings<T1,T2>> for our TwoThings<T1,T2> type.  The EqualityComparer<T> type is used to properly compare member data, regardless of the types.  Without this, the compiler wouldn’t know how to compare two T1 or two T2 objects.

    public class TwoThings<T1,T2> : IEquatable<TwoThings<T1,T2>>
    {
        T1 thing1;
        T2 thing2;

        EqualityComparer<T1> comparer1 = EqualityComparer<T1>.Default;
        EqualityComparer<T2> comparer2 = EqualityComparer<T2>.Default;

        public TwoThings(T1 thing1, T2 thing2)
        {
            this.thing1 = thing1;
            this.thing2 = thing2;
        }

        // IEquatable
        public bool Equals(TwoThings<T1, T2> other)
        {
            return (comparer1.Equals(thing1, other.thing1) &&
                    comparer2.Equals(thing2, other.thing2));
        }
    }

Now suppose that we construct this class using types that implement value equality.  The Equals method then behaves as we’d expect.

            Dog myDog = new Dog("Jack", 5);
            TwoThings<Dog, string> first = new TwoThings<Dog, string>(myDog, "Feisty");

            Dog otherDog = new Dog("Jack", 5);
            TwoThings<Dog, string> second = new TwoThings<Dog, string>(otherDog, "Feisty");

            // Returns true
            bool eq = first.Equals(second);

#1,142 – Using EqualityComparer to Compare Objects in Generic Types

By default, you can’t use equality operators on objects whose type is a type argument within a generic class.  If you constrain an argument to be a reference type, you can use equality operators to do reference equality checks.  But you can‘t make use of overloaded equality functionality.

You can do a true equality check between objects using the EqualityComparer<T> class.  Below is an example.  EqualityComparer<T> makes use of overloaded equality logic in the Dog class, even though we haven’t constrained the T argument.

    public class Pile<T>
    {
        List<T> pile = new List<T>();
        EqualityComparer<T> comparer = EqualityComparer<T>.Default;

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public bool IsFirst(T item)
        {
            // Reference equality only, if we do
            //   where T : class
            //bool equalStd = (pile[0] == item);

            // Makes use of overloaded Equality
            // functionality in T
            bool equalBetter = comparer.Equals(pile[0], item);

            return equalBetter;
        }
    }

1142-001

#1,141 – Reference Equality Used Even when Overload Available for Typed Parameter

If a type parameter in a generic class is constrained to be a reference type, using the class designator, then the == and != operators used for this type parameter will perform a check for reference type equality.  This is true even when the type provides a more intelligent equality check by overloading the == operator.

Assume that we overload == for the Dog type to do an intelligent equality check.

            Dog d = new Dog("Jack", 5);
            Dog d2 = new Dog("Jack", 5);

            // sameDog will be true
            bool sameDog = (d == d2);

Below, the == operator performs a reference type equality check on the type parameter.

    public class Pile<T> where T : class
    {
        List<T> pile = new List<T>();

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public bool IsFirst(T item)
        {
            // Reference equality, even if
            // == operator is overloaded in T
            return (pile[0] == item);
        }

Using the above class:

            Pile<Dog> pack = new Pile<Dog>();
            pack.Add(new Dog("Jack", 5));

            Dog d2 = new Dog("Jack", 5);

            // Returns false
            bool sameDog = pack.IsFirst(d2);

 

#1,140 – Comparing Reference-Typed Objects in Generic Types

You can’t normally use the == or != operators on objects whose type is one of the type parameters in a generic class.  The compiler doesn’t know enough about the type to be able to use the equality or inequality operators.

If you add a class constraint on a type parameter, indicating that it must be a reference type, then you can use the == and != operators to do basic reference type equality.

    public class Pile<T> where T : class
    {
        List<T> pile = new List<T>();

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public bool IsFirst(T item)
        {
            return (pile[0] == item);
        }
    }

Our IsFirst method returns true if the item parameter is the same object as the first item in the collection.

            Pile<Dog> pack = new Pile<Dog>();

            Dog d1 = new Dog("Bowser");
            Dog d1B = new Dog("Bowser");

            pack.Add(d1);

            Console.WriteLine(pack.IsFirst(d1));
            Console.WriteLine(pack.IsFirst(d1B));

1140-001

#1,139 – The Problem with Comparisons of Objects in Generic Types

If you don’t constrain a type parameter in a generic class, the compiler will not let you compare two instances of objects of that type using the == or != operators.

In the example below, we store a collection of objects whose type is the type parameter T.  

    public class Pile<T>
    {
        List<T> pile = new List<T>();

        public void Add(T item)
        {
            if (!pile.Contains(item))
                pile.Add(item);
        }

        public bool IsFirst(T item)
        {
            // Compare to null allowed
            bool isnull = (item == null);

            return (pile[0] == item) ;
        }

        public void Dump()
        {
            foreach (T item in pile)
                Console.WriteLine(item);
        }
    }

If we try compiling this code, we’ll get a compile-time error in the IsFirst method, indicating that we can’t apply the == operator.  The compiler doesn’t have enough information about the type T to know that we can use the == operator.
1139-002