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

#c# #generics #clr #runtime #compiler-bug

#c# #общие #clr #время выполнения #ошибка компилятора

Вопрос:

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

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

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

Но не является ли эта ошибка технически неправильной? Не отменяет ли использование Object for 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>() { }
}
  

But:

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

This compiles and runs perfectly for any type passed to Foo<T>() .

Why? Is there a bug in the runtime, or (more likely) is there a reason for this exception that I’m not seeing — in which case shouldn’t the compiler have stopped me?

Interestingly, if the scenario is reversed by moving the constraint from the class to the interface…

 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.Объект, и это не будет компилироваться. Я чувствую себя немного грязно, делая это, потому что я не уверен, намеренно ли это работает таким образом.

Комментарии:

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

2. … преобразуется в объекты кучи перед их передачей.

Ответ №1:

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

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

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

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

С уважением,

Эд Маурер, руководитель разработки компилятора C #

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

Комментарии:

1. Спасибо. Надеюсь, вы не возражаете, я отредактировал ваш ответ, включив в него то, что Microsoft сказала об ошибке, что довольно хорошо отвечает на мой вопрос. Я приму этот ответ, если только кто-нибудь вроде Эрика Липперта не расскажет в ближайшее время больше деталей.

2. @IgbyLargeman: Для меня это определенно выглядит как ошибка компилятора C #. Я не вижу причин, по которым метод Foo<T>() where T:U следует рассматривать как реализацию Foo<T>() . Я думаю, что трудность частично связана с тем, что C # не проверяет ограничения при сопоставлении сигнатур методов, хотя даже если C # сопоставляет сигнатуры, он должен заметить несовместимые ограничения.

Ответ №2:

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

Компилятор не выдает ошибку при использовании object … ну, это ошибка компилятора.

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

 interface I
{
    object M();
}

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

class D : C, I
{
}
  

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

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

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

Комментарии:

1. Я понимаю вашу точку зрения. Что, если среда выполнения не жаловалась? Сработает ли это? В моем втором примере, где ограничение на I.Foo , среда выполнения, кажется, довольна. Я все еще в замешательстве по этому поводу.

2. Привет! Если среда выполнения не жаловалась, ваш первый пример сработал бы. Но факт в том, что он жалуется. На ваш вопрос нет вразумительного ответа… Я думаю, что оба примера должны работать, но способ, которым среда выполнения проверяет ограничения, делает это неработоспособным. Это «правило» среды выполнения проверять ограничения таким образом, потому что оно было закодировано таким образом. Возможно, в будущей версии они сделают проверку, чтобы при использовании object она работала так, как будто вообще не было ограничений.

Ответ №3:

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

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

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

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

Комментарии:

1. Использование string в моих примерах было плохим выбором, потому что оно закрыто — спасибо, что указали на это (сейчас исправлено). Однако в итоге это не имеет никакого значения, поскольку я намеренно не соответствую ограничениям, поэтому ничего, кроме Object, компилироваться не будет. Хотя, спасибо, полезный ответ. Я посмотрел на ту часть спецификации, на которую вы ссылались — у меня немного болит голова.

Ответ №4:

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

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

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

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

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

Чтобы увидеть эту работу на практике, укажите реальный класс, который может быть унаследован от as 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 как string вместе со string в качестве ограничения типа в C.Foo().

 public sealed class MyClass
{
}
  

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

Комментарии:

1. String был первым типом, который пришел мне в голову — плохой пример. Спасибо, что указали на это, я отредактирую вопрос, чтобы избежать путаницы. Что касается вашего другого пункта — да, вы можете заставить его работать, добавив соответствующие ограничения, но это лишило бы смысла этот вопрос.

2. Почему указание ограничений сводит на нет суть вопроса. Оказывается, что компилятор устанавливает довольно прямое правило, согласно которому переопределенные методы или методы, реализующие интерфейс, должны переформулировать и удовлетворять ограничениям сигнатуры метода базового метода. Вы бы не столкнулись с той же проблемой, если бы хотели написать новый метод Foo2 или подобный, поскольку у него нет ограничений, с которыми вы сталкиваетесь, но дело в том, что метод должен подчиняться своей базе, иначе вы делаете невозможным применение правил, необходимых для полиморфизма.

3. Первое, что я делаю, это демонстрирую ожидаемую и понятную ошибку компилятора CS0425, чтобы задать фон для остальной части вопроса. Дело в том, что я могу обойти требование соответствия ограничений, когда я использую Object в качестве параметра типа, используемого в качестве ограничения, но это вызывает ошибку времени выполнения. Если бы я указал соответствующие ограничения, у меня не было бы вопросов. 🙂 (Мой вопрос, очевидно, слишком длинный и разговорный, вместо того, чтобы быть ясным и лаконичным, и я приношу извинения за это)

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