Подтвердить что ты не робот

Почему ограничение общего типа приводит к неявной ошибке преобразования ссылок?

Я создал пару интерфейсов и общие классы для работы с назначением на повестку дня:

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>'.

Может ли кто-нибудь объяснить, почему это не работает?

4b9b3361

Ответ 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>();