Почему этот сценарий дженериков вызывает исключение 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#, это нарушает правило, согласно которому "ограничения для 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, и это не скомпилируется. Я чувствую себя немного грязным, делая это, потому что я не уверен, что это намеренно работает таким образом.


person Igby Largeman    schedule 16.05.2011    source источник
comment
Не все типы действительно наследуются от Object. Все экземпляры кучи имеют типы, производные от Object, но место хранения типа значения просто содержит поля (общедоступные и частные) этого типа без какой-либо присоединенной информации о типе. Такой набор полей можно неявно преобразовать в Object, но таковым не является. Обратите внимание, что ограничение чего-либо параметром универсального типа типа Object эффективно добавляет ограничение class. Обратите внимание, что общий параметр как с ограничением интерфейса, так и с ограничением class будет принимать struct реализации этого интерфейса, если они...   -  person supercat    schedule 13.06.2012
comment
... преобразуются в объекты кучи перед их передачей.   -  person supercat    schedule 13.06.2012


Ответы (4)


Это ошибка — см. Реализация универсального метода из универсального интерфейса вызывает исключение TypeLoadException и method-with-type-parameter-constraint" rel="nofollow">Непроверяемый код с универсальным интерфейсом и универсальным методом с ограничением параметра типа. Однако мне не ясно, является ли это ошибкой С# или ошибкой CLR.

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

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

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

С уважением,

Эд Маурер Ведущий специалист по разработке компилятора C#

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

person kvb    schedule 20.05.2011
comment
Спасибо. Надеюсь, вы не возражаете, я отредактировал ваш ответ, включив в него то, что Microsoft сказала об ошибке, что довольно хорошо отвечает на мой вопрос. Я приму этот ответ, если только кто-то вроде Эрика Липперта не представит более подробную информацию в ближайшее время. - person Igby Largeman; 23.05.2011
comment
@IgbyLargeman: Мне кажется, это ошибка компилятора C#. Я не вижу причин, по которым метод Foo<T>() where T:U следует считать реализацией Foo<T>(). Я думаю, что сложность частично связана с тем, что C# не проверяет ограничения при сопоставлении сигнатур методов, хотя даже если C# соответствует сигнатурам, он должен заметить несовместимые ограничения. - person supercat; 13.06.2012

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

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

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

interface I
{
    object M();
}

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

class D : C, I
{
}

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

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

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

person Miguel Angelo    schedule 16.05.2011
comment
Я вижу вашу точку зрения. Что, если среда выполнения не жаловалась? Будет ли это работать? В моем втором примере, где ограничение на I.Foo, среда выполнения, похоже, довольна. Я все еще в замешательстве. - person Igby Largeman; 16.05.2011
comment
Привет! Если бы среда выполнения не жаловалась, ваш первый пример работал бы. Но дело в том, что он жалуется. На ваш вопрос нет вразумительного ответа... Я думаю, что оба примера должны работать, но то, как среда выполнения проверяет ограничения, не позволяет ему работать. Это правило среды выполнения проверять ограничения таким образом, потому что это было закодировано таким образом. Возможно, в будущей версии сделают проверку, чтобы при использовании object работало так, как будто ограничений вообще не было. - person Miguel Angelo; 21.05.2011

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

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

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

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

person Chris Hannon    schedule 18.05.2011
comment
Использование строки в моих примерах было плохим выбором, потому что она запечатана — спасибо, что указали на это (сейчас исправлено). Однако, в конце концов, это не имеет значения, поскольку я намеренно не соблюдаю ограничения, поэтому ничего, кроме Object, не скомпилируется. Хотя ответ полезный, спасибо. Я посмотрел ту часть спецификации, на которую вы ссылались, - немного болит голова. - person Igby Largeman; 20.05.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
{
}

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

person jpierson    schedule 19.05.2011
comment
Строка была первым типом, который мне пришел в голову — плохой пример. Спасибо, что указали на это, я отредактирую вопрос, чтобы избежать путаницы. Что касается вашего другого пункта - да, вы можете заставить его работать, добавив соответствующие ограничения, но это лишит смысл этого вопроса. - person Igby Largeman; 20.05.2011
comment
Почему указание ограничений может победить суть вопроса. Это довольно простое правило, установленное компилятором, согласно которому переопределенные методы или методы, реализующие интерфейс, должны переформулировать и удовлетворять ограничениям сигнатуры метода базового метода. Вы бы не столкнулись с той же проблемой, если бы захотели написать новый метод Foo2 или тому подобное, поскольку он не имеет ограничения, с которым вы работаете, но суть в том, что метод должен подчиняться своей базе, иначе вы создаете правила, необходимые для полиморфизм невозможно реализовать. - person jpierson; 20.05.2011
comment
Первое, что я делаю, это демонстрирую ожидаемую и понятую ошибку компилятора CS0425, чтобы установить фон для остальной части вопроса. Дело в том, что я могу обойти требование сопоставления ограничений, когда я использую Object в качестве параметра типа, используемого в качестве ограничения, но это вызывает ошибку времени выполнения. Если бы я указывал соответствующие ограничения, у меня не было бы вопросов. :) (Мой вопрос, очевидно, слишком длинный и разговорный, вместо того, чтобы быть ясным и кратким, и я извиняюсь за это) - person Igby Largeman; 20.05.2011
comment
Думаю, это хорошая тема, наверное, я просто не был уверен, что вы поняли, на что на самом деле жаловался компилятор. Теперь я понимаю, что проблема больше связана с несколькими особыми пограничными случаями и с тем, почему компилятор ждет до времени выполнения, чтобы применить то, что кажется большим ограничением времени компиляции. Удачи в поиске ответа. - person jpierson; 21.05.2011