【问题标题】:A generic method can use contravariant/covariant types?泛型方法可以使用逆变/协变类型吗?
【发布时间】:2011-12-27 08:12:36
【问题描述】:

我正在编写一个通用方法,以便在 T4 模板的特殊任务中使用它。该方法应该允许我使用通用接口中的特殊类型。我想到了以下签名:

interface IGreatInterface {
    Object aMethodAlpha<U>(U parameter) where U : IAnInterface;
    Object aMethodBeta(IAnInterface parameter)
}

public class AnInterestingClass : IAnInterface{}

当我尝试实现IGreatInterface 时,编译器会为aMethodBeta() 标记一个错误,因为我已经让我的T4 使用IAnInterface 的子类型编写该方法(即我想像这样实现该方法:@ 987654325@)。

方法aMethodAlpha&lt;U&gt;() 可以使用,但不像我想要的那样干净,因为我的T4 必须生成一些额外的代码。我(也许是错误的) 建议该方法的实现(必须由 T4 完成)可以是
Object aMethodAlpha&lt;AnInterestingClass&gt;(AnInterestingClass parameter)。

我认为泛型方法不支持逆变类型,但我不确定;我想这是编译器阻止编码器使用具有未在通用类型中定义的方法的特定类型的方式...

  1. 泛型方法在实现时是否必须使用确切的类型?
  2. 有什么技巧可以改变这种行为吗?

【问题讨论】:

  • 我不太明白你的问题。能贴出实现IGreatInterface的代码和具体的编译器错误吗?
  • @Juan:作为旁注,它有助于用 update 或其他东西标记您的更新,以便我们看到发生了什么变化。

标签: c# oop generics .net-4.0 t4


【解决方案1】:

如果您想从通用接口继承,请参阅 phoog 的回答。如果您正在谈论尝试以协变方式实现接口,这将导致我在下面的讨论。

假设:

internal interface IAnInterface { }

public class SomeSubClass : IAnInterface { }

public class AnotherSubClass : IAnInterface { }

public GreatClass : IGreatInterface { ... }

尝试使用更多派生(协变)参数实现接口的问题是,当通过接口调用此接口时,无法保证传入的IAnInterface 将是SomeSubClass 实例。这就是不允许直接允许的原因。

IGreatInterface x = new GreatClass();

x.aMethodBeta(new AnotherSubClass());

IF你可以做协方差,这会失败,因为你会期待SomeSubClass,但会得到AnotherSubClass。

你可以做的是做显式接口实现:

class GreatInterface : IGreatInterface
{
    // explicitly implement aMethodBeta() when called from interface reference
    object IGreatInterface.aMethodBeta(IAnInterface parameter)
    {
        // do whatever you'd do on IAnInterface itself...
        var newParam = parameter as SomeSubClass;

        if (newParam != null)
        {
            aMethodBeta(newParam);
        }

        // otherwise do some other action...
    }

    // This version is visible from the class reference itself and has the 
    // sub-class parameter
    public object aMethodBeta(SomeSubClass parameter)
    {
        // do whatever
    }
}

因此,如果你这样做,你的接口支持泛型,类有一个更具体的方法,但仍然支持接口。主要区别在于您需要处理传入 IAnInterface 的意外实现的情况。

更新:听起来你想要这样的东西:

public interface ISomeInterface
{
    void SomeMethod<A>(A someArgument);
}

public class SomeClass : ISomeInterface
{
    public void SomeMethod<TA>(TA someArgument) where TA : SomeClass
    {

    }
}

这是不允许的,当你从一个接口实现一个泛型方法时,约束必须匹配。

【讨论】:

  • hmmm...从您的回答中我可以推断出.net 不接受具有通用方法的子类型...我正确吗?
  • @Juan:我不确定您所说的“不接受”是什么意思。你的意思是作为论据吗?作为参数类型?
  • @Juan:如果你的意思是我可以实现一个接口但让类型比接口类型更派生吗?不,你不能。您可以协变、反变地传递/返回参数,并且可以协变、反变地分配接口和委托(如果标记得当),但不能是实现。
  • 我说的是方法的类型。假设我在一个界面中有aMethodAlpha&lt;AType&gt;(...)。实现可以是aMethodAlpha&lt;aDerivedType&gt;(...)aDerivedType : AType?我从你的回答中得出结论,这是不可能的,方法实现必须是aMethodAlpha&lt;aDerivedType&gt;(...)。如果我是正确的,您只能通过将其添加到您的原始答案来获得支票......谢谢!
  • @Juan:那么,您希望接口方法是泛型的,并且该接口方法的实现可以提供更派生的泛型类型占位符形式,对吗?
【解决方案2】:

也许你正在寻找这个:

interface IGreatInterface<in U> where U : IAnInterface
{ 
    Object aMethodAlpha(U parameter);
} 

class SomeClass : IAnInterface { /*...*/ }

class GreatClass : IGreatInterface<SomeClass>
{
    public Object aMethodAlpha(SomeClass parameter) {}
}

编辑:

是的,你是对的:如果你在接口中定义了一个泛型方法,你就不能用兼容类型的具体方法来实现该方法。

如何使用委托(因为委托支持协变和逆变):

[删除示例,因为我得到了向后的方差——它不起作用。]

【讨论】:

    【解决方案3】:

    这个问题很混乱。让我看看我能不能澄清一下。

    当我尝试实现IGreatInterface 时,编译器将aMethodBeta() 标记为错误,因为我使用IAnInterface 的子类型创建了该方法,我想像这样实现该方法:Object aMethodBeta(AnInterestingClass parameter)。

    这是不合法的。稍微简化一下:

    class Food {}
    class Fruit : Food {}
    class Meat : Food {}
    interface IEater
    {
        void Eat(Food food);
    }
    class Vegetarian : IEater
    {
        public void Eat(Fruit fruit);
    }
    

    Vegetarian 类不履行IEater 的合同。您应该能够传递 any Food to Eat,但 Vegetarian 只接受 Fruit。 C# 不支持虚方法形参协方差,因为那不是类型安全的。

    现在,你可能会说,这个怎么样:

    interface IFruitEater
    {
        void Eat(Fruit fruit);
    }
    class Omnivore : IFruitEater
    {
        public void Eat(Food food);
    }
    

    现在我们有了类型安全; Omnivore 可以用作IFruitEater,因为Omnivore 可以吃水果以及任何其他食物。

    不幸的是,C# 不支持 虚方法形式参数类型逆变,尽管这样做在理论上是类型安全的。很少有语言支持这一点。

    同样,C# 也不支持虚方法返回类型变化。

    我不确定这是否真的回答了您的问题。你能澄清一下这个问题吗?

    更新:

    怎么样:

    interface IEater
    {
        void Eat<T>(T t) where T : Food;
    }
    class Vegetarian : IEater
    {
        // I only want to eat fruit!
        public void Eat<Fruit>(Fruit food) { }
    }
    

    不,这也不合法。 IEater 的约定是您将提供一个方法Eat&lt;T&gt;,它可以采用任何T,即Food。你不能部分地执行合同,就像你不能这样做一样:

    interface IAdder
    {
        int Add(int x, int y);
    }
    class Adder : IAdder
    {
        // I only know how to add two!
        public int Add(2, int y){ ... }
    }
    

    但是,您可以这样做:

    interface IEater<T> where T : Food
    {
        void Eat(T t);
    }
    class Vegetarian : IEater<Fruit>
    {
        public void Eat(Fruit fruit) { }
    }
    

    这是完全合法的。但是,您不能这样做:

    interface IEater<T> where T : Food
    {
        void Eat(T t);
    }
    class Omnivore : IEater<Fruit>
    {
        public void Eat(Food food) { }
    }
    

    再次因为,C# 不支持虚方法形式参数逆变或协方差。

    请注意,C#是否支持参数多态协方差,因为这样做是已知的类型安全的。例如,这是合法的:

    IEnumerable<Fruit> fruit = whatever;
    IEnumerable<Food> food = fruit;
    

    一个水果序列可以用作一个食物序列。或者,

    IComparable<Fruit> fruitComparer = whatever;
    IComparable<Apples> appleComparer = fruitComparer;
    

    如果你有可以比较任意两个水果的东西,那么它就可以比较任意两个苹果。

    然而,这种协变和逆变只有在满足以下所有条件时才是合法的:(1) 变体可证明是类型安全的,(2) 类型的作者添加了变体注释,表明所需的协变和反变差异,(3) 涉及的不同类型参数都是引用类型,(4) 泛型类型是委托或接口。

    【讨论】:

    • (很好的答案!此评论将用于稍后澄清问题......)。使用你的例子:我可以创建一个签名IEater.Eat&lt;T&gt;(T food) where T : Food,然后实现Omnivore.Eat&lt;Fruit&gt;(Fruit food)吗?没有编译器标记我没有实现IEater.Eat&lt;T&gt;(T food)?。从你的回答我认为这是不可能的......
    • @JuanPabloContreras:我添加了一些关于您的评论的附加文本。
    • @EricLippert 如果可能的话,请您对这个问题发表您的看法stackoverflow.com/questions/8109478/…
    猜你喜欢
    • 2013-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-02
    相关资源
    最近更新 更多