Saturday, May 16, 2009

Co-variance / Contra-variance - Part II

This is a follow-up to my previous post: Co-variance / Contra-variance - Part I.

On my previous post I used the unfortunate expression:

All this suffering will be finally put to an end once C# 4.0 comes out with the lovely new feature of safe Co- and Contra-variance.

As it turns out, this isn't entirely true. Sure, this new feature will definitely make like a lot easier but it will not give us a straight forward solution for the problems I stated. The main reason is that safe co- and contra-variance will be only applicable to interfaces and delegates due to a restriction in the CLR. I don't know what this restriction is but I'll promptly post it once I find it out. Another restriction is that safe variance can only be applied when there is a reference conversion between the generic arguments. This means that no variance will be possible between int and object and I suspect that it will not be possible between two types where an implicit cast operator is declared.

Even though we cannot directly fix the methods on my previous post to work with safe variance, it can definitely be refactored into a safely variant solution. I will discuss how it would be achieved although for educational purposes I chose to implement something entirely new that is easier to understand and it's a better example of safe variance. Also I chose something different to what Mads Torgersen wrote for the “New Features in C# 4.0” document in C# Future, to both train myself and to put a brand new example out there.

So let's get our hands dirty:

public interface IConverter<TSource, TDestination>
{
    TDestination Convert(TSource source);
}

This interface can come in handy in lots of scenarios. Now we would like to implement a generic UI, so we may declare a form like this:

public partial class MyGenericForm<TBusinessObject> : Form
{

    private UserControl DisplayControl { get; set; }

    public MyGenericForm()
    {
        FormBorderStyle = FormBorderStyle.SizableToolWindow;

        InitializeComponent();
    }

    public MyGenericForm(IConverter<TBusinessObject, UserControl> converter, TBusinessObject sourceObject)
        :this()
    {
        AsignSourceObject(converter, sourceObject);
    }

    public void AsignSourceObject(IConverter<TBusinessObject, UserControl> converter, TBusinessObject sourceObject)
    {
        this.SuspendLayout();

        if (DisplayControl != null)
        {
            Controls.Remove(DisplayControl);
            DisplayControl.Dispose();
        }

        DisplayControl = converter.Convert(sourceObject);
        this.Text = DisplayControl.Text;
        this.ClientSize = DisplayControl.Size;
        this.Controls.Add(DisplayControl);

        this.ResumeLayout();
    }

    private void CloseButton_Click(object sender, EventArgs e)
    {
        this.Close();
    }

}

This form displays any kind of TBusinessObject given that you implement an IConverter that converts it to a UserControl. So we implement the class Person, UserControlPerson and a converter between them:

public class Person
{
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class PersonToUserControlConverter : IConverter<Person, UserControl>
{
    public UserControl Convert(Person source)
    {
        PersonUserControl ctrl = new PersonUserControl();
        ctrl.User = source;
        return ctrl;
    }
}

Now we use the form like this:

Person john = new Person() { UserName = "Johnny", FirstName = "John", LastName = "Doe" };
using (MyGenericForm<Person> frm = new MyGenericForm<Person>(new PersonToUserControlConverter(), john))
{
    frm.ShowDialog();
}

Cool, it works as expected.

Further on the development of my project, I found myself needing a new form with some other features and not as restrictive as this one. I make it.

public partial class AnotherGenericForm<TBusinessObject> : Form
{
    public AnotherGenericForm()
    {
        InitializeComponent();
    }

    // Form's code

    public void AsignSourceObject(IConverter<TBusinessObject, Control> converter, TBusinessObject sourceObject)
    {
        // Some code
    }

    // More code

}

Unfortunately I can't use PersonToUserControl converter since it doesn't implement the IConverter<TBusinessObject, Control> interface. No problem, I'll just declare it and explicitly implement it:

public class PersonToUserControlConverter
    : IConverter<Person, UserControl>, IConverter<Person, Control>
{
    public UserControl Convert(Person source)
    {
        PersonUserControl ctrl = new PersonUserControl();
        ctrl.User = source;
        return ctrl;
    }

    Control IConverter<Person, Control>.Convert(Person source)
    {
        return this.Convert(source);
    }
}

OK, suddenly I realize the following: Eventually I may need to implement IConverter<TBusinessObject, ScrollableControl> and who knows what else. It seems that this will come back to haunt me. This isn't the best course of action.

The solution: Co-Variance.
With C# 4.0 I can make the IConverter interface co-variant. How? Using the out keyword.

public interface IConverter<TSource, out TDestination>
{
    TDestination Convert(TSource source);
}

Declaring the interface in this manner makes it safely co-variant. The out keyword indicates that TDestination can only be in an output position. Now IConverter<Person, UserControl> is also considered as IConverter<Person, ContainerControl>and all the way up the inheritance chain up to IConverter<Person, Object>. Notice that no breaking changes are introduced and we can now safely use our converter in the new form. Now we can safely declare:

IConverter<Person, Control> converter = new PersonToUserControlConverter();

In a similar fashion, we might end up having a form that has the following method:

public void AsignSourceObject(IConverter<ForumUser, Control> converter, TBusinessObject sourceObject)
{
    // Some code
}

ForumUser inherits from User. We can't use the same approach because in our interface TSource is an input. Also covariance goes up the inheritance chain, not downwards.

The solution: Contra-variance.
As you have already guessed, contra-variance and the in keyword will help us out here. Our (hopefully) final declaration of our IConverter is:

public interface IConverter<in TSource, out TDestination>
{
    TDestination Convert(TSource source);
}

The in keyword indicates that TSource can only be in an input position. Now IConverter<Person, UserControl> is also considered as IConverter<ForumUser, UserControl> and any other inheritance ramifications of Person.

Now, all of the following is completely legal:

PersonToUserControlConverter converter = new PersonToUserControlConverter();

IConverter<ForumUser, Control> forumUserToControl = converter;
IConverter<ForumUser, UserControl> forumUserToUserControl = converter;
IConverter<ForumAdministrator, ContainerControl> forumUserToControl = converter;
IConverter<Person, object> forumUserToControl = converter;
IConverter<Person, IComponent> forumUserToControl = converter;

Notice the last declaration. Even though IComponent isn't exactly in the inheritance chain, UserControl implements it, so it's legal too.

Why the in/out keywords?

Well, the obvious answer is out for output position and in for input position. Well… duh! But why is co-variance allowed only through arguments in output position? Well, if you think about it, it makes sense. Consider the following classes:

public class A { }
public class B : A { }
public class C : A { }
public class D : C { }
public class E : C { }

Now, if we have a method that returns a C we are certain that we can treat it as an A. We don't know if we can treat him as D since it might be a C, D or E. Hence, it is co-variant. Now if the object was to be used as a method's parameter, it means the method needs a C. So we can feed that method anything that can be considered a C, like D or E. We can't feed it with an A or a B since they cannot be used as a C. Hence, it is contra-variant. My head hurts.

Co- and Contra-variant Types in .NET Framework 4.0

public interface IEnumerable<out T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IEnumerator, IDisposable
{
    new T Current { get; }
}

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<in T1, in T2, in T3, in T4, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

A couple of educated guesses:

public interface IEqualityComparer<in T>
{
    // Members
}

public interface IGrouping<out TKey, out TElement>
{
    // Members
}

public interface IQueryable<out TElement>
{
    // Members
}

public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
public delegate bool Predicate<in T>(T obj);
public delegate int Comparison<in T>(T x, T y);

Solution to Previous Post

I'm not going to go into details about the solution. To the co-variance problem, well the solution is pretty straight forward. We change the SafeMethod to have an IEnumerable<MyBase> as parameter. Just like that we could now feed it with a List<MyFirstDerived> without any problems.

The contra-variant problem is not that simple. One possible solution would involve defining our own interface and class:

public interface ICollectable<T> 
{

    int Count { get; }
    void Add(T item);
    void Insert(int index, T item);
    bool Remove(T item);
    void RemoveAt(int index);
    void Clear();
    bool Contains(T item);

}

public partial class Pack<TAnimal>: IList<TAnimal>, ICollectable<TAnimal>, IList
    where TAnimal : Animal
{

    private List<TAnimal> innerList;

    // A whole lot of delegations!

}

Actually, I'd take it a little further and remove the Animal restriction to create yet another generic collection. The ICollectable gives you an interface of a collection where you can blindly modify it. Pretty weird, huh? Anyways, now our AddTigerToPack method would receive an ICollectable<Tiger> and our Pack<Animals> will be considered as such thanks to safe contra-variance.

Random thoughts: I wonder if they would include a better named ICollectable<in T> interface… Seriously, it keeps marveling me how they manage to keep introducing major changes without breaking any existing code. They manage to add a wonderful new functionality which opens a lot of possibilities without restricting what we already had. All these while still keeping things strongly typed and relatively simple. Kudos!

No comments:

Post a Comment