我有一个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类型,以便可以在接口中指定此功能。有没有办法在编译时强制执行此操作?
*为了明确起见,有一个函数: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
}
}
答案 0 :(得分:2)
在这种情况下,只需从接口中删除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
}
}
System.Collections.Generic
下的许多CLR类遵循相同的模式。许多类都实现IEnumerable<T>
,这是协变的;但是如果要调用允许您添加的方法,则必须将其作为具体的类进行引用,例如List<T>
。或者,如果您确实要通过界面添加,则可以使用IList<T>
。但是,在任何情况下都没有一个协变量接口也允许您添加。
答案 1 :(得分:0)
到目前为止,我唯一想到的解决方案是针对您所关注的每个问题(IShelter
和ISpecificShelter
)使用2个单独的接口:
// 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;
这实际上取决于您的特定用例。您应该提供更多示例和信息,以了解您到底想做什么以及为什么要做。
答案 2 :(得分:0)
您试图实现的目标是不可能的,因为从技术上来说,您希望拥有同时为covariant and contravariant的接口。 SO answer
在这里很好地解释了每种方差的局限性因此,如果您希望能够为TDerived : T
属性设置更多派生类型(例如Contents
),则应该使用变量接口:
public interface IShelter<in T>
{
T Contents { set; }
}
另一方面,如果您希望能够将Contents
传递给派生类型较少的类型(例如T : TBase
),则应坚持使用当前的实现:
public interface IShelter<out T>
{
T Contents { get; }
}
任何其他组合都可能导致运行时错误,这就是为什么编译器不允许您使用具有协变和互变的接口的原因。
因此,可以使用两个不同的接口来实现所需的功能,或者围绕这些类型重新考虑/抛光您的体系结构。