3

I have written the following piece of code in an attempt to provide a type-safe interface:

namespace MWE
{
    public abstract class C {}
    public class A : C {}
    public class B : C {}

    public class Container<T> where T : C
    {
        public readonly T Value;

        public static implicit operator T(Container<C> c)
        {
            return c.Value;
        }
    }

    public interface IWrapper<out TC> where TC : C {}

    public class Foo
    {
        public Foo(IWrapper<Container<C>> wrapper) {}
    }
}

Unfortunately this doesn't compile. The Container<C>-part of the wrapper parameter to the Foo constructor causes the compiler to produce the following error:

The type 'MFE.Container<MFE.C>' cannot be used as type parameter 'TC' in the generic type or method 'IWrapper<TC>'. There is no implicit reference conversion from 'MFE.Container<MFE.C>' to 'MFE.C'.
The type 'MFE.Container<MFE.C>' must be convertible to 'WeirdTestStuff.C' in order to use it as parameter 'TC' in the generic interface 'MFE.IWrapper<out TC>'.

I can't figure out where the problem is exactly, since the Covariance for the conversion seems to be there and there is even an implicit conversion from a Container<T> to T defined. Since T : C, I assumed it should just work like this.

I'd like to keep Foo's constructor as is if possible.

I hope someone can point me to a solution of this problem

2
  • Since C is already a base class is it acceptable that Container<T> : C where T : C? And also change implicit operator to T(Container<T> c)? Commented Mar 21, 2019 at 8:35
  • Unfortunately this is not acceptable; Container<T> can't be extend / implement C. I think you mean C(Container<T> c). Commented Mar 21, 2019 at 8:49

1 Answer 1

3

even an implicit conversion from a Container to T defined

That is true but that's not what the compiler requires. It requires:

implicit reference conversion

(My emphasis)

An implicit reference conversion is not one supplied by any user-defined operator and is allowed only when one type derives (directly or via intermediate types) from the other1.

Container has-a C and can be converted to a C via a user-defined operator but that's not enough to make it be-a C. Your question is too abstracted to say what the fix should be here - should Container be non-generic and derived from C? That's the obvious way to "shut up" the compiler but may not solve your actual problem.

You can't use generics to make a type's base-type settable at runtime.


1These are Eric Lippert's Representation-preserving conversions

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you for you answer. The point I was missing is the fact that reference conversions don't take my conversion into account. True, I kept the MFE relatively generic. In my case Container can't be derived from C. My implementation of the Container rather works like a Union<A, B, C> to some extend with A and B extending some abstract class C and Union<A, B, C> containing some object of A or B as some instance extending the base class C. Therefor the less beautiful way of fixing my specific problem is using something like Union<IWrap<A>, IWrap<B>, IWrap<C>>.
The key point here is that user-defined conversions are never classified as reference conversions, and that's what covariance requires. However, any reference conversion will do, even ones that do not directly involve inheritance relationships. For example, if we have IEnumerable<IEnumerable<string>> we can convert that to IEnumerable<IEnumerable<object>> even though there is no inheritance relationship between IE<string> and IE<object>. Of course there is an inheritance relationship between string and object.
The question of whether representation-preserving conversions that are not reference conversions should work with variance is a tricky one, and the CLR is inconsistent on this point. uint to int is representation-preserving, and an uint[] is convertible to both IEnumerable<int> and IEnumerable<uint> in the CLR, but (IEnumerable<int>)M(); where we have IEnumerable<uint> M() { yield break; } is not legal in the CLR.
@EricLippert - to avoid having to add too many caveats, do you think just changing "allowed only" to "typically"? If I made such a change, I'd welcome an additional footnote that digs into the details if you could suggest some text/create a new blog post :-)
Eh, I wouldn't worry about it too much. The vast majority of the time the conversions just work the way you'd expect them to, which was our goal in creating the feature in the first place. The small details of how exactly it works are of importance to the spec writers and the implementation team; I mention them just to be complete.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.