Почему компилятор запрашивает явное приведение универсального типа (если неявное было определено) только тогда, когда универсальный экземпляр был создан с помощью интерфейса?
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
}
}
Это связано с ограничениями пользовательских конверсий:
Классу или структуре разрешено объявлять преобразование из источника. тип S к целевому типу T только в том случае, если выполняются все следующие условия:
- S₀ и T₀ — это разные типы.
- Либо S₀, либо T₀ — это тип класса или структуры, в котором происходит объявление оператора.
- Ни S₀, ни T₀ не являются типом интерфейса.
- За исключением пользовательских преобразований, преобразований из S в T или из T в S не существует.
в нашем случае общий параметр T
— это тип источника. И это работает до тех пор, пока это не тип интерфейса.
если бы вы определили неявный оператор следующим образом, вы бы получили ошибку времени компиляции
//CS0552 'G<T>.implicit operator G<T>(I)': user-defined conversions to or from an interface are not allowed
public static implicit operator G<T>(I unused) => new G<T>(unused);
Обновлено: Немного более глубокое погружение в наш общий случай раскрывает некоторые... забавные факты:
Компилятор не соблюдает свои собственные правила, когда дело касается дженериков для генерации методов. Чтобы продемонстрировать, что мы можем это сделать:
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 для последних двух утверждений:
IL_0018: newobj instance void C::.ctor() /* 06000008 */
IL_001d: call class G`1<!0> class G`1<class I>::op_Implicit(!0) /* 0A00000E */
Похоже, у нас есть этот метод в метаданных:
public static implicit operator G<I>(I unused) => new G<I>(unused);
и мы можем «вызвать его» из нашего кода, выполнив неявное преобразование.
Ограничение состоит в том, что мы не можем передавать ему прямые ссылки на I
, а только ссылки на классы, реализующие I
.
Обновлено еще раз:
Однако мы можем взломать его (почти?) полностью, если выполним неявное присваивание в обобщенном методе:
G<T> ReturnGI<T>(T instance) {
G<T> result = instance;
return result;
}
I i = new C();
var gi = ReturnGI(i); // will be G<I>
C# запрещает
implicit
преобразования с участиемinterface
типов.