简体   繁体   English

如何向协变接口添加类型不变的设置器?

[英]How can I add a type invariant setter to a covariant interface?

I have a type Shelter that needs to be covariant, so that an override in another class* can return a Shelter<Cat> where a Shelter<Animal> is expected. 我有一个Shelter类型,需要协变,以便另一个类中的重写*可以返回Shelter<Cat> ,其中应该有Shelter<Animal> Since classes cannot be co- or contravariant in C#, I added an interface: 由于类在C#中不能是协变的,因此我添加了一个接口:

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

However, there is a place where an IShelter (compile-time type) is assigned a new animal, where we know for sure that the animal being set is going to be a Cat. 但是,在某个地方会为IShelter(编译时类型)分配新的动物,我们可以肯定地确定所设置的动物将是猫。 At first, I thought I could just add a set to the Contents property and do: 起初,我以为可以将一个集添加到Contents属性中并执行以下操作:

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

But adding the setter is not possible; 但是不可能添加二传手。

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

This makes sense, because otherwise I could pass the catshelter to this function: 这很有道理,因为否则我可以将引导程序传递给此函数:

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

However, I'm not going to do that. 但是,我不会这样做。 It would be nice to have some way to mark the setter as invariant so that the UseShelter function would only be able to assign an Animal, and so that this would be enforced at compile-time. 最好有一些方法可以将setter标记为不变的,以便UseShelter函数只能分配一个Animal,这样可以在编译时强制执行。 The reason I need this is because there is a place in my code that knows it has a Shelter<Cat> , and needs to re-assign the Contents property to a new Cat. 我之所以需要它,是因为我的代码中有一个地方知道它具有Shelter<Cat> ,并且需要将Contents属性重新分配给新的Cat。

The workaround I found so far is to add a jucky runtime type check in an explicit set function; 到目前为止,我发现的解决方法是在显式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;
}

The parameter needs to be of type object, so that this function can be specified in the interface. 参数必须是object类型,以便可以在接口中指定此功能。 Is there any way to enforce this at compile-time? 有什么办法在编译时强制执行此操作吗?

* To clarify, there is a function: public virtual IShelter<Animal> GetAnimalShelter() that is overridden and returns an IShelter<Cat> : *要澄清一下,有一个函数:覆盖并返回IShelter<Cat> public virtual IShelter<Animal> GetAnimalShelter()

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

A minimal working example including most of the code above follows: 一个最小的工作示例,包括上面的大多数代码:

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
    }
}

In a case like this, just remove the setter from the interface and use the concrete type (or infer it using var as in this example). 在这种情况下,只需从接口中删除setter并使用具体类型(或在本示例中使用var推断)。 After all, if the code "knows" for sure it is adding a cat, it probably also knows the shelter's concrete type. 毕竟,如果代码“知道”肯定要添加一只猫,那么它可能也知道避难所的具体类型。

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
    }
}

Example on DotNetFiddle DotNetFiddle上的示例

The same pattern is followed by many CLR classes under System.Collections.Generic . System.Collections.Generic下的许多CLR类遵循相同的模式。 Lots of classes implement IEnumerable<T> , which is covariant; 许多类都实现IEnumerable<T> ,它是协变的。 but if you want to call the methods that allow you to add, you have to reference it as a concrete class such as List<T> . 但是,如果要调用允许添加的方法,则必须将其作为诸如List<T>的具体类进行引用。 Or if you really want to add via an interface you could use IList<T> . 或者,如果您真的要通过接口添加,则可以使用IList<T> But in no case is there a single covariant interface that also allows you to add. 但是,在任何情况下都没有一个协变量接口也允许您添加。

What you are trying to achieve is not possible, because technically you want to have interface which is both covariant and contravariant . 您试图实现的目标是不可能的,因为从技术上讲,您希望拥有既协变又互变的接口。 Limitations of each type of variance are well explained here in this SO answer 在此SO答案中很好地解释了每种方差的局限性

So, if you want to be able to set more derived type (eg TDerived : T ) to the Contents property, you should use contravariant interface: 因此,如果您希望能够为Contents属性设置更多派生类型(例如TDerived : T ),则应使用相反接口:

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

On the other hand, if you want to be able to pass Contents to less derived type (eg T : TBase ), you should stick with your current implementation: 另一方面,如果您希望能够将Contents传递给较少派生的类型(例如T : TBase ),则应坚持使用当前的实现:

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

Any other combination would result in possible runtime errors, that's why the compiler doesn't allow you to have an interface which is both co- and contravariant. 任何其他组合都会导致可能的运行时错误,这就是为什么编译器不允许您同时使用协变和逆变的接口。

So, either use two distinct interfaces to achieve what you want, or rethink/polish your architecture around these types. 因此,可以使用两个不同的接口来实现所需的功能,或者围绕这些类型重新考虑/抛光您的体系结构。

The only solution I could think of so far would be using 2 separate interfaces for each of your concerns ( IShelter and ISpecificShelter ): 到目前为止,我唯一想到的解决方案是针对您的每个顾虑使用两个单独的接口( IShelterISpecificShelter ):

// 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()
    { }
}

Usage: 用法:

// 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;

If you want to prevent instancing the class directly, but only work with the interfaces, you could implement a factory pattern: 如果要防止直接实例化该类,而使用接口,则可以实现工厂模式:

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;
        }
    }
}

Usage: 用法:

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;

It really depends on your specific use cases. 这实际上取决于您的特定用例。 You should provide more examples and information on what exactly you want to do and why. 您应该提供更多示例和信息,以了解您确切想要做什么以及为什么要做。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM