Как я могу добавить установщик инварианта типа к ковариантному интерфейсу? - PullRequest
0 голосов
/ 08 ноября 2018

У меня есть тип Shelter, который должен быть ковариантным, чтобы переопределение в другом классе * могло возвращать Shelter<Cat>, где ожидается Shelter<Animal>. Так как классы не могут быть совместными или контравариантными в C #, я добавил интерфейс:

public interface IShelter<out AnimalType>
{
    AnimalType Contents { get; }
}

Однако, есть место, где IShelter (тип времени компиляции) назначается новое животное, где мы точно знаем, что устанавливаемое животное будет Кошкой. Сначала я подумал, что могу просто добавить набор к свойству Contents и сделать:

IShelter<Cat> shelter = new Shelter(new Cat());
shelter.Contents = new Cat();

Но добавить сеттер невозможно;

Error   CS1961  Invalid variance: The type parameter 'AnimalType' must be invariantly valid on 'IShelter<AnimalType>.Contents'. 'AnimalType' is covariant.

Это имеет смысл, потому что в противном случае я мог бы передать укрытие этой функции:

private static void UseShelter(IShelter<Animal> s)
{
    s.Contents = new Lion();
}

Однако я не собираюсь этого делать. Было бы неплохо иметь какой-то способ пометить сеттер как инвариант, чтобы функция UseShelter могла назначать только Animal, и чтобы это выполнялось во время компиляции. Причина, по которой мне это нужно, заключается в том, что в моем коде есть место, которое знает, что оно имеет Shelter<Cat>, и ему необходимо переназначить свойство Contents для нового Cat.

Обходной путь, который я нашел до сих пор, состоит в том, чтобы добавить проверочную проверку типов во время выполнения в явной функции set; Juck!

public void SetContents(object newContents)
{
    if (newContents.GetType() != typeof(AnimalType))
    {
        throw new InvalidOperationException("SetContents must be given the correct AnimalType");
    }
    Contents = (AnimalType)newContents;
}

Параметр должен иметь тип объекта, чтобы эту функцию можно было указать в интерфейсе. Есть ли способ применить это во время компиляции?

* Для пояснения есть функция: public virtual IShelter<Animal> GetAnimalShelter(), которая переопределяется и возвращает IShelter<Cat>:

public override IShelter<Animal> GetAnimalShelter(){
    IShelter<Cat> = new Shelter<Cat>(new Cat());
}

Ниже приведен минимальный рабочий пример, включающий большую часть приведенного выше кода:

class Animal { }
class Cat : Animal { }
class Lion : Animal { }

public interface IShelter<out AnimalType>
{
    AnimalType Contents { get; }

    void SetContents(object newContents);
}

class Shelter<AnimalType> : IShelter<AnimalType>
{
    public Shelter(AnimalType animal)
    {
    }

    public void SetContents(object newContents)
    {
        if (newContents.GetType() != typeof(AnimalType))
        {
            throw new InvalidOperationException("SetContents must be given the correct AnimalType");
        }
        Contents = (AnimalType)newContents;
    }

    public AnimalType Contents { get; set; }
}

class Usage
{
    public static void Main()
    {
        IShelter<Cat> catshelter = new Shelter<Cat>(new Cat());
        catshelter.SetContents(new Cat());
        catshelter.SetContents(new Lion()); // should be disallowed by the compiler
    }
}

Ответы [ 3 ]

0 голосов
/ 08 ноября 2018

То, что вы пытаетесь достичь, невозможно, потому что технически вы хотите иметь интерфейс, который является ковариантным и контравариантным . Ограничения каждого типа дисперсии хорошо объяснены здесь в этом SO ответе

Итак, если вы хотите иметь возможность установить более производный тип (например, TDerived : T) для свойства Contents, вам следует использовать контравариантный интерфейс:

public interface IShelter<in T>
{
    T Contents { set; }
}

С другой стороны, если вы хотите иметь возможность передавать Contents в менее производный тип (например, T : TBase), вам следует придерживаться текущей реализации:

public interface IShelter<out T>
{
    T Contents { get; }
}

Любая другая комбинация может привести к возможным ошибкам во время выполнения, поэтому компилятор не позволяет иметь интерфейс, который является одновременно и контравариантным.

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

0 голосов
/ 08 ноября 2018

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

interface IShelter<out AnimalType>
{
    AnimalType Contents { get; }
}

class Shelter<AnimalType> : IShelter<AnimalType>
{
    public Shelter(AnimalType animal)
    {
    }

    public void SetContents(AnimalType newContents)
    {
        Contents = newContents;
    }

    public AnimalType Contents { get; set; }
}

public class Usage
{
    public static void Main()
    {
        var catshelter = new Shelter<Cat>(new Cat());
        catshelter.SetContents(new Cat());
        catshelter.SetContents(new Lion()); // Is disallowed by the compiler
    }
}

Пример для DotNetFiddle

За тем же шаблоном следуют многие классы CLR в System.Collections.Generic. Многие классы реализуют IEnumerable<T>, что является ковариантным; но если вы хотите вызвать методы, которые позволяют добавлять, вы должны ссылаться на него как на конкретный класс, такой как List<T>. Или, если вы действительно хотите добавить через интерфейс, вы можете использовать IList<T>. Но ни в коем случае не существует единого ковариантного интерфейса, который также позволяет добавлять.

0 голосов
/ 08 ноября 2018

Единственное решение, о котором я мог подумать, - это использование 2 отдельных интерфейсов для каждой из ваших задач (IShelter и ISpecificShelter):

// this replaces IShelter<Animal>
public interface IShelter
{
    Animal Animal { get; set; }
}

// this replaces IShelter<SpecificAnimal>
public interface ISpecificShelter<AnimalType> : IShelter where AnimalType : Animal
{
    new AnimalType Animal { get; set; }
}

// implementation
public class Shelter<AnimalType> : ISpecificShelter<AnimalType> where AnimalType : Animal
{
    private Animal _animal;

    // explicit implementation, so the correct implementation is called depending on what interface is used
    Animal IShelter.Animal { get { return _animal; } set { _animal = value; } }
    AnimalType ISpecificShelter<AnimalType>.Animal { get { return (AnimalType)_animal; } set { _animal = value; } }

    public Shelter()
    { }
}

Использование:

// specific animal
ISpecificShelter<Cat> catShelter = new Shelter<Cat>();
catShelter.Animal = new Cat();
catShelter.Animal = new Lion(); // => compiler error!
Cat cat = catShelter.Animal;

// cast to general interface
IShelter shelter = catShelter;
Animal animal = shelter.Animal;

Если вы хотите запретить создание экземпляров класса напрямую, но только работает с интерфейсами, вы можете реализовать фабричный шаблон:

public static class Shelter
{
    public static IShelter Create()
    {
        return new Implementation<Animal>();
    }

    public static IShelter Create(Animal animal)
    {
        return new Implementation<Animal>(animal);
    }

    public static ISpecificShelter<AnimalType> Create<AnimalType>() where AnimalType : Animal
    {
        return new Implementation<AnimalType>();
    }

    public static ISpecificShelter<AnimalType> Create<AnimalType>(AnimalType animal) where AnimalType : Animal
    {
        return new Implementation<AnimalType>(animal);
    }

    private class Implementation<AnimalType> : ISpecificShelter<AnimalType> where AnimalType : Animal
    {
        private Animal _animal;

        Animal IShelter.Animal { get { return _animal; } set { _animal = value; } }
        AnimalType ISpecificShelter<AnimalType>.Animal { get { return (AnimalType)_animal; } set { _animal = value; } }

        public Implementation()
        { }

        public Implementation(AnimalType animal)
        {
            _animal = animal;
        }
    }
}

Использование:

var shelter = Shelter.Create();
shelter.Animal = new Lion();
// OR:
// var shelter = Shelter.Create(new Lion());
var animal = shelter.Animal;

var catShelter = Shelter.Create<Cat>();
catShelter.Animal = new Cat();
// OR:
// var catShelter = Shelter.Create(new Cat());
var cat = catShelter.Animal;

// cast is also possible:
shelter = (IShelter)catShelter;

Это действительно зависит от ваших конкретных случаев использования. Вы должны предоставить больше примеров и информации о том, что именно вы хотите сделать и почему.

...