繁体   English   中英

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

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

我有一个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();
}

但是,我不会这样做。 最好有一些方法可以将setter标记为不变的,以便UseShelter函数只能分配一个Animal,这样可以在编译时强制执行。 我之所以需要它,是因为我的代码中有一个地方知道它具有Shelter<Cat> ,并且需要将Contents属性重新分配给新的Cat。

到目前为止,我发现的解决方法是在显式set函数中添加一个麻烦的运行时类型检查。 杰克!

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

参数必须是object类型,以便可以在接口中指定此功能。 有什么办法在编译时强制执行此操作吗?

*要澄清一下,有一个函数:覆盖并返回IShelter<Cat> public virtual IShelter<Animal> GetAnimalShelter()

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

在这种情况下,只需从接口中删除setter并使用具体类型(或在本示例中使用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上的示例

System.Collections.Generic下的许多CLR类遵循相同的模式。 许多类都实现IEnumerable<T> ,它是协变的。 但是,如果要调用允许添加的方法,则必须将其作为诸如List<T>的具体类进行引用。 或者,如果您真的要通过接口添加,则可以使用IList<T> 但是,在任何情况下都没有一个协变量接口也允许您添加。

您试图实现的目标是不可能的,因为从技术上讲,您希望拥有既协变又互变的接口。 在此SO答案中很好地解释了每种方差的局限性

因此,如果您希望能够为Contents属性设置更多派生类型(例如TDerived : T ),则应使用相反接口:

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

另一方面,如果您希望能够将Contents传递给较少派生的类型(例如T : TBase ),则应坚持使用当前的实现:

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

任何其他组合都会导致可能的运行时错误,这就是为什么编译器不允许您同时使用协变和逆变的接口。

因此,可以使用两个不同的接口来实现所需的功能,或者围绕这些类型重新考虑/抛光您的体系结构。

到目前为止,我唯一想到的解决方案是针对您的每个顾虑使用两个单独的接口( 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()
    { }
}

用法:

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

这实际上取决于您的特定用例。 您应该提供更多示例和信息,以了解您确切想要做什么以及为什么要做。

暂无
暂无

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

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