Eleno
Eleno

Reputation: 3016

"Cannot implicitly convert type" error only when generic is instantiated with an interface

Why does the compiler ask for an explicit cast for a generic - when an implicit one has been defined - only when the generic has been instantiated with an interface?

class G<T>
{
    public G(T unused) {}

    public static implicit operator G<T>(T unused) => new G<T>(unused);
}

interface I { }

class C : I { }

class Example
{
    void Main()
    {
        G<int> xint1 = 1; // OK
        G<int?> xint2 = 1; // OK
        G<object> xobject1 = new object(); // OK
        G<object?> xobject2 = new object(); // OK

        C c = new C();

        G<C> gc1 = c; // OK
        G<C?> gc2 = c; // OK
        
        I i = c;

        G<I> g1 = i;
        //        ^-- error CS0266: Cannot implicitly convert type 'I' to
        //        'G<I>'. An explicit conversion exists (are you missing a
        //        cast?)
                
        G<I?> g2 = i;
        //         ^-- error CS0266: Cannot implicitly convert type 'I' to
        //         'G<I?>'. An explicit conversion exists (are you missing a
        //         cast?)
        
        G<I?> g3 = (I?)i;
        //         ^-- error CS0266: Cannot implicitly convert type 'I' to
        //         'G<I?>'. An explicit conversion exists (are you missing a
        //         cast?)
        //         ^-- warning CS8600: Converting null literal or possible null
        //         value to non-nullable type.

        G<I?> g4 = (I)i;
        //         ^-- error CS0266: Cannot implicitly convert type 'I' to
        //         'G<I?>'. An explicit conversion exists (are you missing a
        //         cast?)

        G<I> g5 = (G<I>)i; // OK
        G<I?> g6 = (G<I?>)i; // OK
    }
}

Upvotes: 3

Views: 133

Answers (1)

Ivan Petrov
Ivan Petrov

Reputation: 4855

That's due to the limitations of user-defined conversions:

A class or struct is permitted to declare a conversion from a source type S to a target type T only if all of the following are true:

  • S₀ and T₀ are different types.
  • Either S₀ or T₀ is the class or struct type in which the operator declaration takes place.
  • Neither S₀ nor T₀ is an interface_type.
  • Excluding user-defined conversions, a conversion does not exist from S to T or from T to S.

So, when the compiler sees an I variable on the right side of an assignment to a G<I> variable, it's not looking for user-defined conversion methods and issues a standard error that we need an explicit cast as it would normally from interface value to someclass-value.

In non-generic cases (or closed-generic G<I> like the one below), we'd get a compile-time error for the method declaration

//CS0552 'G<T>.implicit operator G<I>(I)': user-defined conversions to or from an interface are not allowed
public static implicit operator G<I>(I unused) => new G<I>(unused);

But in our open-generic case where we have an unknown T, it was apparently decided to not even issue a warning that when T is I, the conversion won't be possible/considered by the compiler.

EDIT: A bit of a deeper dive into our generic case reveals some ... fun facts:

The compiler doesn't honor its own rule when it comes to generics for method generation. To demonstrate we can do this:

var implicitMethod = typeof(G<I>).GetMethod("op_Implicit");
implicitMethod.ReturnType.Dump(); // G`1[I]
implicitMethod.GetParameters()[0].ParameterType.Dump(); // I

C c = new C();
G<I> surprise = c; // OK?!

IL for the last two statements:

IL_0018: newobj instance void C::.ctor() /* 06000008 */
IL_001d: call class G`1<!0> class G`1<class I>::op_Implicit(!0) /* 0A00000E */

It looks like we do have this method in the metadata:

public static implicit operator G<I>(I unused) => new G<I>(unused);

and we can "call it" from our code by having the implicit conversion.

Restriction is that we can't pass it direct references of I, but references of classes that implement I.

EDIT 2:

We can, however, hack it (almost?) completely if we do our implicit assignment in a generic method:

G<T> ReturnGI<T>(T instance) {
    G<T> result = instance;
    return result;
}

I i = new C();
var gi = ReturnGI(i); // will be G<I>

Upvotes: 7

Related Questions