Ответ 1
Я считаю, что компилятор делает лучшее в VB.NET с предупреждением, но я все еще не думаю, что это происходит достаточно далеко. К сожалению, "правильная вещь", вероятно, либо требует отклонения того, что потенциально полезно (реализация одного и того же интерфейса с двумя ковариантными или контравариантными параметрами типового типа) или введение чего-то нового в язык.
В его нынешнем виде компилятор не может назначить ошибку прямо сейчас, кроме класса HungryWolf
. Это тот момент, когда класс претендует на то, чтобы знать, как сделать что-то, что потенциально может быть неоднозначным. Он указывает
Я знаю, как есть
ICloneable
, или что-либо, реализующее или наследующее от него, определенным образом.И я также знаю, как есть
IConvertible
, или что-либо, реализующее или наследующее от него, определенным образом.
Однако он никогда не заявляет, что он должен делать, если он получает на своей пластине что-то, что как ICloneable
, так и IConvertible
. Это не вызывает у компилятора горе, если ему задан экземпляр HungryWolf
, поскольку он может с уверенностью сказать: "Эй, я не знаю, что здесь делать!". Но это даст компилятор горе, когда ему будет предоставлен экземпляр ICanEat<string>
. Компилятор понятия не имеет, каков фактический тип объекта в переменной, только то, что он определенно реализует ICanEat<string>
.
К сожалению, когда a HungryWolf
хранится в этой переменной, он неоднозначно реализует этот точный интерфейс дважды. Поэтому, конечно, мы не можем выпустить ошибку, пытающуюся вызвать ICanEat<string>.Eat(string)
, поскольку этот метод существует и был бы вполне применим для многих других объектов, которые могли бы быть помещены в переменную ICanEat<string>
(batwad уже упоминал об этом в одном из своих ответов).
Кроме того, хотя компилятор может жаловаться на то, что назначение объекта HungryWolf
переменной ICanEat<string>
неоднозначно, это не может помешать ему в два этапа. A HungryWolf
может быть назначено переменной ICanEat<IConvertible>
, которая может быть передана другим методам и в конечном итоге назначена переменной ICanEat<string>
. Оба из них являются совершенно законными назначениями, и компилятор не сможет жаловаться ни на один.
Таким образом, вариант должен запретить классу HungryWolf
реализовать как ICanEat<IConvertible>
, так и ICanEat<ICloneable>
, когда параметр ICanEat
generic type является контравариантным, поскольку эти два интерфейса могут объединяться. Однако этот удаляет возможность кодировать что-то полезное без альтернативного метода обхода.
Вариант 2, к сожалению, будет требовать изменения компилятора, как IL, так и CLR. Это позволило бы классу HungryWolf
реализовать оба интерфейса, но для этого также потребовалась бы реализация интерфейса ICanEat<IConvertible & ICloneable>
, где параметр generic type реализует оба интерфейса. Вероятно, это не лучший синтаксис (как выглядит подпись этого метода Eat(T)
, Eat(IConvertible & ICloneable food)
?). Вероятно, лучшим решением будет создание автоматически генерируемого родового типа на классе реализации, чтобы определение класса было примерно таким:
class HungryWolf:
ICanEat<ICloneable>,
ICanEat<IConvertible>,
ICanEat<TGenerated_ICloneable_IConvertible>
where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable {
// implementation
}
Затем IL должен был измениться, чтобы позволить создавать типы реализации интерфейса, как и общие классы для инструкции callvirt
:
.class auto ansi nested private beforefieldinit HungryWolf
extends
[mscorlib]System.Object
implements
class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>,
class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>,
class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>
Затем CLR должен будет обработать инструкции callvirt
, построив реализацию интерфейса для HungryWolf
с string
как параметр типового типа для TGenerated_ICloneable_IConvertible
и проверив, будет ли он соответствовать лучше других реализаций интерфейса.
Для ковариации все это было бы проще, так как дополнительные интерфейсы, необходимые для реализации, не должны были быть типичными параметрами типа с ограничениями , а просто самым производным базовым типом между двумя другими типами, который известен во время компиляции.
Если один и тот же интерфейс реализован более двух раз, то количество дополнительных интерфейсов, требуемых для реализации, растет экспоненциально, но это будет стоить гибкости и безопасности типов при реализации нескольких контравариантных (или ковариантных) на одном класс.
Я сомневаюсь, что это переместит его в рамки, но это будет мое предпочтительное решение, тем более что новая сложность языка всегда будет самодостаточной для класса, который хочет делать то, что в настоящее время опасно.
изменить
Спасибо Jeppeнапомнив мне, что ковариация не проще, чем контравариантность, из-за того, что необходимо учитывать и общие интерфейсы. В случае string
и char[]
множество наибольших общности было бы {object
, ICloneable
, IEnumerable<char>
} (IEnumerable
покрыто IEnumerable<char>
).
Однако для этого потребуется новый синтаксис ограничения параметра универсального типа интерфейса, чтобы указать, что для параметра общего типа требуется
- наследует от указанного класса или класса, реализующего хотя бы один из указанных интерфейсов
- реализовать хотя бы один из указанных интерфейсов
Возможно, что-то вроде:
interface ICanReturn<out T> where T: class {
}
class ReturnStringsOrCharArrays:
ICanReturn<string>,
ICanReturn<char[]>,
ICanReturn<TGenerated_String_ArrayOfChar>
where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> {
}
Общий параметр типа TGenerated_String_ArrayOfChar
в этом случае (где один или несколько интерфейсов являются общими) всегда должен рассматриваться как object
, хотя общий базовый класс уже получен из object
; потому что общий тип может реализовать общий интерфейс, не наследуя от общего базового класса.