In C# 4.0 why can't an out parameter in a method be covariant?
Asked Answered
S

2

17

Given this magical interface:

public interface IHat<out TRabbit>
{
    TRabbit Take();
}

And this class hierarchy:

public class Rabbit { }

public class WhiteRabbit : Rabbit { }

I can now compile this:

IHat<WhiteRabbit> hat1 = null;
IHat<Rabbit> hat2 = hat1;

Which is great. But what if I define the interface differently:

public interface IHat<out TRabbit>
{
    bool Take(out TRabbit r);
}

I'm indicating that the hat might be empty, using a separate boolean return value (the previous version would perhaps have returned a null rabbit from an empty hat). But I'm still only outputting a rabbit, so not doing anything logically different to the previous version.

The C# 4.0 compiler in the CTP gives an error in the interface definition - it requires 'out' method parameters to be of an invariant type. Is there a hard-and-fast reason why this isn't allowed, or is it something that might be addressed in a future version?

Sievert answered 9/2, 2009 at 11:14 Comment(0)
S
10

Interesting. However, at the CLI level there is no such thing as "out" - only "ref"; there is an attribute that helps compilers (for definite assignment) that says "you don't need to pass it in".

Maybe this restriction is because the CLI doesn't have "out", only "ref".

Spaceship answered 9/2, 2009 at 11:22 Comment(1)
For info, I've found multiple blogs etc saying the same, but none of the comments are from "official" MS sources. I'm pretty confident it is correct, though... the C# 4.0 variance is still based on the CLI rules.Spaceship
C
0

Although it's a bit of a hassle, you can use a covariance wrapper:

public class CovariantListWrapper<TOut, TIn> : IList<TOut> where TIn : TOut
{
    IList<TIn> list;

    public CovariantListWrapper(IList<TIn> list)
    {
        this.list = list;
    }

    public int IndexOf(TOut item)
    {
        // (not covariant but permitted)
        return item is TIn ? list.IndexOf((TIn)item) : -1;
    }

    public TOut this[int index]
    {
        get { return list[index]; }
        set { throw new InvalidOperationException(); }
    }

    public bool Contains(TOut item)
    {
        // (not covariant but permitted)
        return item is TIn && list.Contains((TIn)item);
    }

    public void CopyTo(TOut[] array, int arrayIndex)
    {
        foreach (TOut t in this)
            array[arrayIndex++] = t;
    }

    public int Count { get { return list.Count; } }

    public bool IsReadOnly { get { return true; } }

    public IEnumerator<TOut> GetEnumerator()
    {
        foreach (TIn t in list)
            yield return t;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Insert(int index, TOut item) { throw new InvalidOperationException(); }
    public void RemoveAt(int index) { throw new InvalidOperationException(); }
    public void Add(TOut item) { throw new InvalidOperationException(); }
    public void Clear() { throw new InvalidOperationException(); }
    public bool Remove(TOut item) { throw new InvalidOperationException(); }
}

This lets you keep the collection as it was originally typed and refer to it covariantly without creating a detached copy, so that updates to the original are seen in the covariant use. Example:

class CovarianceWrapperExample
{
    class Person { }
    class Employee : Person { }

    void ProcessPeople(IList<Person> people) { /* ... */ }

    void Foo()
    {
        List<Employee> employees = new List<Employee>();

        // cannot do:
        ProcessPeople(employees);

        // can do:
        ProcessPeople(new CovariantListWrapper<Person, Employee>(employees));
    }
}
Corporal answered 18/2, 2011 at 22:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.