Почему сценарий generics вызывает исключение TypeLoadException?

Это получилось немного длинным, так что вот быстрая версия:

Почему это вызывает исключение TypeLoadException во время выполнения? (И должен ли компилятор помешать мне это делать?)

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<System.Object>, I { } 

Исключение возникает, если вы пытаетесь создать экземпляр D.


Более длинная, более исследовательская версия:

Рассмотрим:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class some_other_class { }

class D : C<some_other_class>, I { } // compiler error CS0425

Это незаконно, поскольку ограничения типа на C.Foo() не совпадают с ограничениями типа на I.Foo(). Он генерирует ошибку компилятора CS0425.

Но я думал, что могу нарушить правило:

class D : C<System.Object>, I { } // yep, it compiles

Используя Object как ограничение на T2, я отрицаю это ограничение. Я могу безопасно передать любой тип D.Foo<T>(), потому что все происходит от Object.

Тем не менее, я все еще ожидал получить ошибку компилятора. В смысле языка С# это нарушает правило, что "ограничения на C.Foo() должны соответствовать ограничениям на I.Foo()", и я думал, что компилятор был бы приверженцем правил. Но он компилируется. Кажется, компилятор видит, что я делаю, понимает, что он в безопасности, и закрывает глаза.

Я думал, что с этим справился, но время исполнения говорит не так быстро. Если я попытаюсь создать экземпляр D, я получаю исключение TypeLoadException: "Метод" C`1.Foo "в типе" D "попытался неявно реализовать метод интерфейса с более слабыми ограничениями параметров типа".

Но разве эта ошибка технически неверна? Не использует Object для C<T1> отрицать ограничение на C.Foo(), тем самым делая его эквивалентным - NOT сильнее, чем - I.Foo()? Компилятор, похоже, согласен, но среда выполнения не работает.

Чтобы доказать свою точку зрения, я упростил ее, выведя D из уравнения:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class some_other_class { }

class C : I<some_other_class> // compiler error CS0425
{
    public void Foo<T>() { }
}

Но:

class C : I<Object> // compiles
{
    public void Foo<T>() { }
}

Это компилируется и выполняется отлично для любого типа, переданного в Foo<T>().

Почему? Есть ли ошибка во время выполнения, или (что более вероятно), есть причина для этого исключения, которое я не вижу - в этом случае компилятор не остановил меня?

Интересно, что если сценарий отменяется путем перемещения ограничения от класса к интерфейсу...

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C
{
    public void Foo<T>() { }
}

class some_other_class { }

class D : C, I<some_other_class> { } // compiler error CS0425, as expected

И снова я отрицаю ограничение:

class D : C, I<System.Object> { } // compiles

На этот раз он отлично работает!

D d := new D();
d.Foo<Int32>();
d.Foo<String>();
d.Foo<Enum>();
d.Foo<IAppDomainSetup>();
d.Foo<InvalidCastException>();

Все идет, и это имеет для меня смысл. (То же с или без D в уравнении)

Итак, почему первый путь нарушается?

Приложение:

Я забыл добавить, что существует простое обходное решение для исключения TypeLoadException:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<Object>, I 
{
    void I.Foo<T>() 
    {
        Foo<T>();
    }
}

Явно реализую I.Foo() отлично. Только неявная реализация вызывает исключение TypeLoadException. Теперь я могу это сделать:

        I d = new D();
        d.Foo<any_type_i_like>();

Но это еще особый случай. Попробуйте использовать что-либо другое, кроме System.Object, и это не будет компилироваться. Я чувствую себя немного грязно, делая это, потому что я не уверен, намеренно ли он работает таким образом.

Ответ 1

Это ошибка - см. Реализация универсального метода из общего интерфейса вызывает исключение TypeLoadException и Неиспользуемый код с общим интерфейсом и общим методом с ограничениями параметров типов. Мне непонятно, является ли это ошибкой С# или ошибкой CLR.

[Добавлен OP:]

Вот что говорит Microsoft во втором потоке, с которым вы связались (мой акцент):

Существует несоответствие между алгоритмы, используемые временем выполнения и С#, чтобы определить, существует ли один набор ограничения столь же сильны, как и другие задавать. Это несоответствие приводит к тому, что С# компилятор принимает некоторые конструкции что время выполнения отклоняется и результатом является TypeLoadException вы видеть. Мы расследуем, чтобы определить если этот код является проявлением эта проблема. Независимо от того, , это конечно, не "по дизайну", что компилятор принимает такой код, который приводит к исключению во время выполнения.

Привет,

Разработка компилятора Ed Maurer С# Ведущий

Из части, которую я выделил, я думаю, что он говорит, что это ошибка компилятора. Это было еще в 2007 году. Думаю, это не достаточно серьезно, чтобы быть приоритетом для их исправления.

Ответ 2

Единственное объяснение заключается в том, что ограничение рассматривается как часть объявления метода. Вот почему в первом случае это ошибка компилятора.

Компилятор не получает ошибку при использовании object... ну, , что является ошибкой компилятора.

Другие "ограничения" имеют одинаковые свойства общего признака:

interface I
{
    object M();
}

class C
{
    public some_type M() { return null; }
}

class D : C, I
{
}

Я мог спросить: почему это не работает?

Вы видите? Это совсем тот же вопрос, что и ваш. Вполне возможно реализовать object с помощью some_type, но ни время выполнения, ни компилятор его не принимают.

Если вы попытаетесь сгенерировать код MSIL и принудительно выполните реализацию моего примера, время выполнения будет жаловаться.

Ответ 3

Неявная реализация интерфейса требует, чтобы общие ограничения на объявления метода были эквивалентными, но не обязательно одинаковыми в коде. Кроме того, параметры типового типа имеют неявное ограничение "где T: объект". Вот почему компиляция C<Object> компилирует, это приводит к тому, что ограничение становится эквивалентным неявному ограничению в интерфейсе. (Раздел 13.4.3 С# Language Spec).

Вы также правы, что использование явной реализации интерфейса, которая вызывает ваш ограниченный метод, будет работать. Он обеспечивает очень четкое сопоставление метода интерфейса с вашей реализацией в классе, где ограничения не могут отличаться, а затем переходит к вызову аналогично названного общего метода (который теперь не имеет никакого отношения к интерфейсу). В этот момент ограничения на вторичный метод могут быть разрешены так же, как любой вызов общего метода без каких-либо проблем с разрешением интерфейса.

Перемещение ограничений от класса к интерфейсу, во втором примере, лучше, потому что класс по умолчанию отключит свои ограничения от интерфейса. Это также означает, что вы должны указать ограничения в реализации класса, если это применимо (и в случае объекта это не применимо). Передача I<string> означает, что вы не можете напрямую указать это ограничение в коде (потому что строка запечатана), и поэтому она должна либо быть частью явной реализации интерфейса, либо общим типом, который будет равен ограничениям в обоих местах.

Насколько я знаю, среда выполнения и компилятор используют отдельные системы проверки для ограничений. Компилятор разрешает этот случай, но верификатор времени выполнения ему не нравится. Я хочу подчеркнуть, что я не знаю точно, почему у него есть проблемы с этим, но я бы предположил, что ему не нравится, что потенциал в определении этого класса не соответствует ограничениям интерфейса в зависимости от того, к. Если у кого-то есть окончательный ответ на это, это будет здорово.

Ответ 4

В ответ на ваш фрагмент, основанный на интерфейсе:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<string> // compiler error CS0425
{
    public void Foo<T>() { }
}

Я считаю, что проблема в том, что компилятор распознает, что:

  • вы не указали необходимые ограничения типа на C.Foo().
  • Если вы выберете строку как свой тип, то не существует допустимого T на C.Foo(), поскольку тип не может наследовать от строки.

Чтобы увидеть эту работу на практике, укажите фактический класс, который можно унаследовать от T1.

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<MyClass>
{
    public void Foo<T>() where T : MyClass { }
}

public class MyClass
{
}

Чтобы показать, что тип string не обрабатывается особым образом, просто добавьте ключевое слово запечатанное в объявление MyClass выше, чтобы увидеть, что он сбой аналогичным образом, если вы должны указать T1 как строку вместе со строкой как ограничение типа на C.Foo().

public sealed class MyClass
{
}

Это потому, что строка запечатана и не может стать основой ограничения.