下面的代码是实现协变返回类型的唯一方法吗?
public abstract class BaseApplication<T> {
public T Employee{ get; set; }
}
public class Application : BaseApplication<ExistingEmployee> {}
public class NewApplication : BaseApplication<NewEmployee> {}
我希望能够构造一个Application或NewApplication并让它从Employee属性中返回相应的Employee类型。
var app = new Application();
var employee = app.Employee; // this should be of type ExistingEmployee
我相信这段代码运行正常,但是当我有几个需要相同行为的属性时,它会变得非常讨厌。
还有其他方法可以实现此行为吗?泛型或其他?
答案 0 :(得分:67)
首先,你的问题的答案是否定的,C#不支持虚拟覆盖的任何形式的返回类型协方差。
一些回答者和评论者说“这个问题没有协方差”。这是不正确的;原始的海报完全正确地提出了问题。
回想一下a covariant mapping is a mapping which preserves the existence and direction of some other relation。例如,从类型T
到类型IEnumerable<T>
的映射是协变的,因为它保留了赋值兼容性关系。如果Tiger与Animal分配兼容,则地图下的转换也会保留:IEnumerable<Tiger>
与IEnumerable<Animal>
兼容。
这里的协变映射有点难以看到,但它仍然存在。问题基本上是这样的:这应该是合法的吗?
class B
{
public virtual Animal M() {...}
}
class D : B
{
public override Tiger M() {...}
}
Tiger与Animal分配兼容。现在进行从类型T到方法“public T M()”的映射。 该映射是否保持兼容性?也就是说,如果Tiger为了分配而与Animal兼容,那么为了虚拟覆盖,public Tiger M()
与public Animal M()
兼容吗?
C#的答案是“不”。 C#不支持这种协方差。
现在我们已经确定问题是使用正确的类型代数术语提出来的,还有一些关于实际问题的想法。显而易见的第一个问题是该属性甚至没有被声明为虚拟,因此虚拟兼容性的问题没有实际意义。显而易见的第二个问题是“得到;设定;”即使C#支持返回类型协方差,属性也不能协变,因为具有setter的属性的类型不仅仅是它的返回类型,它也是它的形式参数类型。您需要在形式参数类型上使用 contravariance 来实现类型安全。如果我们允许使用setter返回属性的返回类型协方差,那么你将拥有:
class B
{
public virtual Animal Animal{ get; set;}
}
class D : B
{
public override Tiger Animal { ... }
}
B b = new D();
b.Animal = new Giraffe();
嘿,我们刚刚把长颈鹿送到了一个期待老虎的二传手。如果我们支持此功能,我们必须将其限制为返回类型(就像我们在通用接口上的赋值兼容性协方差一样。)
第三个问题是CLR不支持这种差异;如果我们想用语言支持它(因为我相信托管的C ++确实如此),那么我们将不得不做一些合理的英雄措施来解决CLR中的签名匹配限制。
您可以通过仔细定义具有影响其基类类型的适当返回类型的“新”方法来自行执行这些英雄测量:
abstract class B
{
protected abstract Animal ProtectedM();
public Animal Animal { get { return this.ProtectedM(); } }
}
class D : B
{
protected override Animal ProtectedM() { return new Tiger(); }
public new Tiger Animal { get { return (Tiger)this.ProtectedM(); } }
}
现在,如果你有一个D实例,你会看到Tiger类型的属性。如果将其强制转换为B,则会看到Animal-typed属性。在任何一种情况下,您仍然可以通过受保护的成员获得虚拟行为。
简而言之,我们没有计划做过这个功能,抱歉。
答案 1 :(得分:3)
您尝试实现的目标可能存在多个问题。
首先,正如有人已经注意到的那样,你的例子中没有covarianace。您可以在此处找到协方差和泛型的简短描述new features in C# 2.0 - Variance, covariance on generics。
其次,您似乎尝试用泛型来解决多态性应该解决的问题。如果ExistingEmployee
和NewEmployee
都从基类Employee
继承,那么您的问题就会得到解决:
public class Application {
public ExistingEmployee Employee { get; }
}
public class NewApplication {
public NewEmployee Employee { get; }
}
...
Application app = new Application;
var emp = app.Employee; // this will be of type ExistingEmployee!
请注意以下情况也是如此:
Employee emp = app.Employee; // this will be of type ExistingEmployee even if
// declared as Employee because of polymorphism
多态性和泛型之间的一个不同之处是,如果将变量返回到特定类型,则需要在后一种情况下进行强制转换:
ExistingEmployee emp = (ExistingEmployee)app.Employee; // would have not been needed
// if working with generics
希望这有帮助。
答案 2 :(得分:0)
您可以针对员工界面进行编码,以获得您想要的内容。
public interface IEmployee
{}
public abstract class BaseApplication<T> where T:IEmployee{
public T IEmployee{ get; set; }
}
public class ExistingEmployee : IEmployee {}
public class NewEmployee : IEmployee {}
public class Application : BaseApplication<ExistingEmployee> {}
public class NewApplication : BaseApplication<NewEmployee> {}
答案 3 :(得分:0)
您发布的代码无法编译,但我基本了解您要执行的操作。简而言之,答案是肯定的,这是唯一的方法。如果您希望某个属性在扩展类中返回不同类型 并以不同方式键入 ,则必须按照现有方式使用泛型。
如果您可以将新的或现有的员工对象的公共合同封装到界面中,那么您根本不需要使用泛型。相反,你可以只返回接口并让多态接管。
public interface IEmployee
{ }
public class Employee1 : IEmployee
{ }
public class Employee2 : IEmployee
{ }
public abstract class ApplicationBase
{
public abstract IEmployee Employee { get; set; }
}
public class App1 : ApplicationBase
{
public override IEmployee Employee
{
get { return new Employee1(); }
set;
}
}
public class App2 : ApplicationBase
{
public override IEmployee Employee
{
get { return new Employee2(); }
set;
}
}
答案 4 :(得分:0)
使用泛型可以获得一个看起来有点整洁的版本。
协变返回类型由c#支持不。所以这不是一个解决方案,但是,我的感觉是从语法上讲这很好。它确实取得了类似的结果。
我发现在创建基类需要执行某些操作的fluent API's
时很有用,但我需要返回派生的实现。它真正实现的是隐藏演员。
public class Base
{
public virtual T Foo<T>() where T : Base
{
//... // do stuff
return (T)this;
}
}
public class A : Base
{
public A Bar() { "Bar".Dump(); return this; }
public A Baz() { "Baz".Dump(); return this; }
// optionally override the base...
public override T Foo<T>() { "Foo".Dump(); return base.Foo<T>(); }
}
var x = new A()
.Bar()
.Foo<A>() // cast back to A
.Baz();
意见会有所不同,而且不是100%漂亮。它可能不适合将要发布的API,但对于内部使用,例如在单元测试中,我发现它很有用。
答案 5 :(得分:-1)
YES!像这样。锅炉板比你希望的多,但确实有效。诀窍是通过扩展方法完成的。 它在内部施加一些讨厌的铸造,但呈现出协变的界面。
另请参阅:http://richarddelorenzi.wordpress.com/2011/03/25/return-type-co-variance-in-c/
using System;
namespace return_type_covariance
{
public interface A1{}
public class A2 : A1{}
public class A3 : A1{}
public interface B1
{
A1 theA();
}
public class B2 : B1
{
public A1 theA()
{
return new A2();
}
}
public static class B2_ReturnTypeCovariance
{
public static A2 theA_safe(this B2 b)
{
return b.theA() as A2;
}
}
public class B3 : B1
{
public A1 theA()
{
return new A3();
}
}
public static class B3_ReturnTypeCovariance
{
public static A3 theA_safe(this B3 b)
{
return b.theA() as A3;
}
}
public class C2
{
public void doSomething(A2 a){}
}
class MainClass
{
public static void Main (string[] args)
{
var c2 = new C2();
var b2 = new B2();
var a2=b2.theA_safe();
c2.doSomething(a2);
}
}
}
答案 6 :(得分:-2)
没有泛型的一个想法,但它有其他缺点:
public abstract class BaseApplication {
public Employee Employee{ get; protected set; }
}
public class Application : BaseApplication
{
public new ExistingEmployee Employee{ get{return (ExistingEmployee)base.Employee;} set{base.Employee=value; }}
}
public class NewApplication : BaseApplication
{
public new NewEmployee Employee{ get{return (NewEmployee)base.Employee;} set{base.Employee=value; }}
}
特别是使用此代码,您可以转换为基类并分配不合适类型的员工。所以你需要在基类的setter中添加检查。或者删除我通常喜欢的setter。一种方法是让二传手受到保护
另一个是添加一个虚函数EmployeeType()
,它在派生类中重写并返回派生类型。然后检查setter是否为EmployeeType().IsInstanceOf(value)
,否则抛出异常。
IMO模拟协变返回类型是new
标记的少数优秀应用之一。它返回与基类相同的东西,只是为函数契约添加了额外的保证。