Почему сценарий 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
{
}
Это потому, что строка запечатана и не может стать основой ограничения.