Почему этот общий сценарий вызывает исключение TypeLoadException? - PullRequest
18 голосов
/ 16 мая 2011

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

Почему это вызывает исключение 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 # language , это нарушает правило, что «ограничения на C.Foo () должны соответствовать ограничениям на I.Foo ()», и я думал, что компилятор будет препятствием для правил , Но это компилируется. Кажется, компилятор видит, что я делаю, понимает, что это безопасно, и закрывает глаза.

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

Но разве эта ошибка не технически ошибочна? Разве использование Object для C<T1> не отменяет ограничение на C.Foo(), что делает его эквивалентным - НЕ сильнее, чем - 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, и это не скомпилируется. Я чувствую себя немного грязно, делая это, потому что я не уверен, намеренно ли это работает таким образом.

Ответы [ 4 ]

4 голосов
/ 21 мая 2011

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

[Добавлено ОП:]

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

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

С уважением,

Ed Maurer C # Разработка компилятора Lead

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

3 голосов
/ 19 мая 2011

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

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

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

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

3 голосов
/ 16 мая 2011

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

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

Другие «ограничения» имеют те же свойства общего ограничения:

interface I
{
    object M();
}

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

class D : C, I
{
}

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

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

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

0 голосов
/ 19 мая 2011

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

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

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

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

  1. вы не объявили необходимые ограничения типав C.Foo ().
  2. , если вы выбираете в качестве типа строку, в C.Foo () нет допустимого T, поскольку тип не может наследоваться от строки.

Для просмотрав этой работе на практике указан фактический класс, который может быть унаследован как 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 не обрабатывается специальным образом, просто добавьте sealed ключевое слово к объявлению MyClass выше, чтобы увидеть, что оно не будет таким же образом, если вы указали T1 как строку вместе со строкой в ​​качестве ограничения типа в C.Foo ().

public sealed class MyClass
{
}

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

...