c#使用泛型的协变返回类型

时间:2010-12-03 18:44:14

标签: c# generics covariance

下面的代码是实现协变返回类型的唯一方法吗?

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

我相信这段代码运行正常,但是当我有几个需要相同行为的属性时,它会变得非常讨厌。

还有其他方法可以实现此行为吗?泛型或其他?

7 个答案:

答案 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

其次,您似乎尝试用泛型来解决多态性应该解决的问题。如果ExistingEmployeeNewEmployee都从基类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标记的少数优秀应用之一。它返回与基类相同的东西,只是为函数契约添加了额外的保证。