Почему ограничение общего типа приводит к неявной ошибке преобразования ссылок?
Я создал пару интерфейсов и общие классы для работы с назначением на повестку дня:
interface IAppointment<T> where T : IAppointmentProperties
{
T Properties { get; set; }
}
interface IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
DateTime Date { get; set; }
T Appointment { get; set; }
}
interface IAppointmentProperties
{
string Description { get; set; }
}
class Appointment<T> : IAppointment<T> where T : IAppointmentProperties
{
public T Properties { get; set; }
}
class AppointmentEntry<T> : IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
public DateTime Date { get; set; }
public T Appointment { get; set; }
}
class AppointmentProperties : IAppointmentProperties
{
public string Description { get; set; }
}
Я пытаюсь использовать некоторые ограничения для параметров типа, чтобы гарантировать, что могут быть указаны только допустимые типы. Однако при указании ограничения, определяющего, что T
должен реализовывать IAppointment<IAppointmentProperties>
, компилятор дает ошибку при использовании класса Appointment<AppointmentProperties>
:
class MyAppointment : Appointment<MyAppointmentProperties>
{
}
// This goes wrong:
class MyAppointmentEntry : AppointmentEntry<MyAppointment>
{
}
class MyAppointmentProperties : AppointmentProperties
{
public string ExtraInformation { get; set; }
}
Ошибка:
The type 'Example.MyAppointment' cannot be used as type parameter 'T' in the generic type or method 'Example.AppointmentEntry<T>'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment<Example.IAppointmentProperties>'.
Может ли кто-нибудь объяснить, почему это не работает?
Ответы
Ответ 1
Пусть упрощается:
interface IAnimal { ... }
interface ICage<T> where T : IAnimal { void Enclose(T animal); }
class Tiger : IAnimal { ... }
class Fish : IAnimal { ... }
class Cage<T> : ICage<T> where T : IAnimal { ... }
ICage<IAnimal> cage = new Cage<Tiger>();
Ваш вопрос: почему последняя строка является незаконной?
Теперь, когда я переписал код, чтобы упростить его, это должно быть ясно. An ICage<IAnimal>
- это клетка, в которую вы можете разместить любое животное, но Cage<Tiger>
может содержать только тигры, поэтому это должно быть незаконным.
Если это не было незаконным, вы могли бы сделать это:
cage.Enclose(new Fish());
И эй, ты просто положил рыбу в клетку тигра.
Система типов не допускает этого преобразования, поскольку это нарушает правило, что возможности типа источника не должны быть меньше возможностей целевого типа. (Это форма знаменитого "принципа замены Лискова".)
В частности, я бы сказал, что вы злоупотребляете дженериками. Тот факт, что вы создали отношения типа, которые слишком сложны для вас, чтобы анализировать себя, свидетельствует о том, что вы должны упростить все это; если вы не соблюдаете все отношения типа прямо, и вы написали это, то ваши пользователи, конечно же, не смогут его сохранить.
Ответ 2
Эрик уже дал очень хороший ответ. Просто хотел воспользоваться этой возможностью, чтобы поговорить об инвариантности, ковариантности и контравариантности здесь.
Определения см. на странице https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance.
Допустим, есть зоопарк.
abstract class Animal{}
abstract class Bird : Animal{}
abstract class Fish : Animal{}
class Dove : Bird{}
class Shark : Fish{}
Зоопарк переезжает, поэтому его животных нужно перевести из старого в новый.
неизменность
Прежде чем переместить их, нам нужно поместить животных в разные контейнеры. Все контейнеры выполняют одну и ту же операцию: кладут в нее животное или вынимают из него животное.
interface IContainer<T> where T : Animal
{
void Put(T t);
T Get(int id);
}
Очевидно, что для рыбы нам нужен аквариум:
class FishTank<T> : IContainer<T> where T : Fish
{
public void Put(T t){}
public T Get(int id){return default(T);}
}
Таким образом, рыбу можно положить и вытащить из аквариума (надеюсь, она еще жива):
IContainer<Fish> fishTank = new FishTank<Fish>(); //Invariance, the two types have to be the same
fishTank.Put(new Shark());
var fish = fishTank.Get(8);
Предположим, нам разрешено изменить его на IContainer<Animal>
, тогда вы можете случайно поместить голубя в аквариум, что, очевидно, приведет к трагедии.
IContainer<Animal> fishTank = new FishTank<Fish>(); //Wrong, some animal can be killed
fishTank.Put(new Shark());
fishTank.Put(new Dove()); //Dove will be killed
контрвариация
Чтобы повысить эффективность, команда управления зоопарком решает разделить процесс загрузки и выгрузки (руководство всегда делает это). Таким образом, у нас есть две отдельные операции, одна только для загрузки, другая выгрузка.
interface ILoad<in T> where T : Animal
{
void Put(T t);
}
Тогда у нас клетка для птиц:
class BirdCage<T> : ILoad<T> where T : Bird
{
public void Put(T t)
{
}
}
ILoad<Bird> normalCage = new BirdCage<Bird>();
normalCage.Put(new Dove()); //accepts any type of birds
ILoad<Dove> doveCage = new BirdCage<Bird>();//Contravariance, Bird is less specific then Dove
doveCage.Put(new Dove()); //only accepts doves
ковариации
В новом зоопарке у нас есть команда по разгрузке животных.
interface IUnload<out T> where T : Animal
{
IEnumerable<T> GetAll();
}
class UnloadTeam<T> : IUnload<T> where T : Animal
{
public IEnumerable<T> GetAll()
{
return Enumerable.Empty<T>();
}
}
IUnload<Animal> unloadTeam = new UnloadTeam<Bird>();//Covariance, since Bird is more specific then Animal
var animals = unloadTeam.GetAll();
С точки зрения команды, не имеет значения, что внутри, они просто выгружают животных из контейнеров.
Ответ 3
Потому что вы объявили свой класс MyAppointment
, используя конкретный тип, а не интерфейс. Вы должны заявить следующее:
class MyAppointment : Appointment<IAppointmentProperties> {
}
Теперь преобразование может происходить неявно.
Объявив AppointmentEntry<T>
с ограничением where T: IAppointment<IAppointmentProperties>
, вы создаете контракт, в соответствии с которым неуказанный тип для AppointmentEntry<T>
должен содержать любой тип, объявленный с помощью IAppointmentProperties
. Объявив тип с конкретным классом, вы нарушили этот контракт (он реализует тип IAppointmentProperties
, но не любой тип).
Ответ 4
Он будет работать, если вы переопределите интерфейс образца из:
interface ICage<T>
к
interface ICage<out T>
(обратите внимание на ключевое слово out
)
то верно следующее утверждение:
ICage<IAnimal> cage = new Cage<Tiger>();