【问题标题】:Covariance and Contravariance in C#C#中的协变和逆变
【发布时间】:2012-04-06 01:31:15
【问题描述】:

首先我要说我是一名学习 C# 编程的 Java 开发人员。因此,我将我所知道的与我所学的进行比较。

我已经用 C# 泛型玩了几个小时了,我已经能够在 C# 中重现我在 Java 中所知道的相同的东西,除了几个使用协变和逆变的例子。我正在读的这本书在这方面不是很好。我当然会在网上寻找更多信息,但在我这样做的同时,也许您可​​以帮我找到以下 Java 代码的 C# 实现。

一个例子值一千字,我希望通过查看一个好的代码示例,我能够更快地吸收这一点。

协方差

在 Java 中我可以这样做:

public static double sum(List<? extends Number> numbers) {
    double summation = 0.0;
    for(Number number : numbers){
        summation += number.doubleValue();
    }
    return summation;
}

我可以这样使用这段代码:

List<Integer> myInts = asList(1,2,3,4,5);
List<Double> myDoubles = asList(3.14, 5.5, 78.9);
List<Long> myLongs = asList(1L, 2L, 3L);

double result = 0.0;
result = sum(myInts);
result = sum(myDoubles)
result = sum(myLongs);

现在我确实发现 C# 仅在接口上支持协变/逆变,只要它们已明确声明(出/入)即可。我想我无法重现这种情况,因为我找不到所有数字的共同祖先,但我相信如果存在共同祖先,我可以使用 IEnumerable 来实现这样的事情。由于 IEnumerable 是协变类型。对吧?

对如何实现上面的列表有任何想法吗?只需指出我正确的方向。所有数值类型是否有共同的祖先?

逆变

我尝试的逆变示例如下。在 Java 中,我可以这样做将一个列表复制到另一个列表中。

public static void copy(List<? extends Number> source, List<? super Number> destiny){
    for(Number number : source) {
       destiny.add(number);
    }
}

然后我可以将它与逆变类型一起使用,如下所示:

List<Object> anything = new ArrayList<Object>();
List<Integer> myInts = asList(1,2,3,4,5);
copy(myInts, anything);

我的基本问题是,尝试在 C# 中实现这一点是我找不到一个同时是协变和逆变的接口,就像上面示例中的 List 一样。也许可以用 C# 中的两个不同接口来完成。

关于如何实现这个有什么想法吗?

非常感谢大家提供的任何答案。我很确定我会从您提供的任何示例中学到很多东西。

【问题讨论】:

  • 我很确定你不能,因为 .NET 团队以其无限的智慧没有提供一种方法来讨论基本类型(例如,数字、浮点)的类。因此,您不能在泛型类型的参数的泛型方法中使用 operator +,并且无法指定您应该能够使用。这是一个明显的洞,但它就在那里。
  • 我前段时间问了一个问题,有一些类似的概念:stackoverflow.com/questions/5516459/…

标签: c# java covariance contravariance


【解决方案1】:

.NET 中没有基础 Number 类。您可以获得的最接近的可能如下所示:

public static double sum(List<object> numbers) {
    double summation = 0.0;
    var parsedNumbers = numbers.Select(n => Convert.ToDouble(n));
    foreach (var parsedNumber in parsedNumbers) {
        summation += parsedNumber;
    }
    return summation;
}

您必须捕获Convert.ToDouble 期间发生的任何错误,以防列表中的任何对象不是数字且未实现IConvertible

更新

不过,在这种情况下,我个人会使用 IEnumerable 和泛型类型(感谢 Paul Tyng,您可以强制 T 实现 IConvertible):

public static double sum<T>(IEnumerable<T> numbers) where T : IConvertible {
    double summation = 0.0;
    var parsedNumbers = numbers.Select(n => Convert.ToDouble(n));
    foreach (var parsedNumber in parsedNumbers) {
        summation += parsedNumber;
    }
    return summation;
}

【讨论】:

  • 您可以使用sum&lt;T&gt;(List&lt;T&gt; numbers) where T : IConvertible添加通用约束
  • 你不能将List&lt;int&gt; 传递给这个方法(你会得到一个无效的转换错误)。如果您将参数类型更改为IEnumerable&lt;object&gt;,并传入例如myInts.Cast&lt;object&gt;() 它会起作用,但这需要在集合到达方法之前强制转换。
  • 我明白你的意思。因此,不能将其静态检查为数字,如果我尝试将不兼容的内容转换为 double,则会出现运行时异常。
  • @edalorzo -- 查看更新。现在将在编译时检查类型 T 以实现 IConvertible,这意味着它可以转换为数字。
  • +1 在这里使用 lambda 表达式来解决问题。我一直期待看到在 JDK 1.8 中实现的那些 :)
【解决方案2】:

可以使用IConvertible接口:

public static decimal sum<T>(IEnumerable<T> numbers) where T : IConvertible
{
    decimal summation = 0.0m;

    foreach(var number in numbers){
        summation += number.ToDecimal(System.Globalization.CultureInfo.InvariantCulture);
    }
    return summation;
}

注意泛型约束 (where T : IConvertible),类似于 Java 中的 extends

【讨论】:

  • 是否所有可转换类型都可以转换为十进制,或者如果不满足条件,我是否可以期望在运行时发生异常?
  • 据我所知,所有内置数字类型显然无法控制自定义实现,因此绝对可能出现异常。
  • 这是一个很好的答案,今天我在办公室的同事之间一直在讨论这个问题。我们是混合开发人员团队:Java 和 C#,您可以在这里想象我们每天都在维持的冬青战争 :)
【解决方案3】:

对于您的问题的第二部分,您不需要逆变,您需要做的就是声明第一种类型可以转换为第二种。同样,使用where TSource: TDest 语法来执行此操作。这是一个完整的例子(展示了如何使用扩展方法):

static class ListCopy
{
    public static void ListCopyToEnd<TSource, TDest>(this IList<TSource> sourceList, IList<TDest> destList)
        where TSource : TDest // This lets us cast from TSource to TDest in the method.
    {
        foreach (TSource item in sourceList)
        {
            destList.Add(item);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<int> intList = new List<int> { 1, 2, 3 };
        List<object> objList = new List<object>(); ;

        ListCopy.ListCopyToEnd(intList, objList);
        // ListCopyToEnd is an extension method
        // This calls it for a second time on the same objList (copying values again).
        intList.ListCopyToEnd(objList);

        foreach (object obj in objList)
        {
            Console.WriteLine(obj);
        }
        Console.ReadLine();
    }

【讨论】:

  • 好吧,如果 TSource 是 TDest 的孩子,我认为您不需要演员表。你应该?这将是一个经典的向上转换。请问方法的参数中出现的“this”是什么?
  • 你说得对,我已经在答案中删除了它。 'this' 将静态方法(在静态类中)转换为扩展方法,这意味着它可以像IList&lt;TSource&gt; 的方法一样被调用。有关这些的更多信息,请参阅this link。请注意,编译器还会进行一些类型推断,因此它可以在您不明确告诉它的情况下计算出 TSourceTDest 是什么。
  • 非常感谢,您的回答很好。这是我一直在寻找的。实际上,它可以在 Java public static &lt;P, C extends P&gt; void copy(List&lt;C&gt; source, List&lt;P&gt; destiny) 中以相同的方式实现,正如您所指出的,它完全避免了逆变问题。
【解决方案4】:

我将回答一些略有不同的问题,而不是直接回答你的问题:

C# 是否有办法泛化支持算术运算符的类型?

不容易,不。如果能够创建一个Sum&lt;T&gt; 方法,该方法可以添加整数、双精度数、矩阵、复数、四元数……等等,那就太好了。尽管这是一个相当频繁地请求的特性,但它也是一个很大的特性,而且它在优先级列表中的位置从来没有足够高以证明它被包含在语言中是合理的。我个人会喜欢它,但你不应该期望它出现在 C# 5 中。也许会出现在该语言的假设未来版本中。

Java 的“调用站点”协变/逆变和 C# 的“声明站点”协变/逆变有什么区别?

在实现层面的根本区别当然是,实际上,Java 泛型是通过擦除实现的;尽管您获得了泛型类型令人愉悦的语法和编译时类型检查的好处,但您不一定会获得 C# 中的性能优势或运行时类型系统集成优势。

但这实际上更多的是实现细节。从我的角度来看,更有趣的区别是 Java 的变体规则是在本地执行的,而 C# 的变体规则是在全局执行的。

也就是说:某些变体转换是危险的,因为它们暗示某些非类型安全的操作不会被编译器捕获。经典的例子是:

  • 老虎是哺乳动物。
  • X 的列表在 X 中是协变的。(假设。)
  • 因此,老虎的列表就是哺乳动物的列表。
  • 哺乳动物列表中可以插入长颈鹿。
  • 因此,您可以将长颈鹿插入到老虎列表中。

这显然违反了类型安全以及长颈鹿的安全。

C# 和 Java 使用两种不同的技术来防止这种类型安全违规。 C# 说当I&lt;T&gt; 接口被声明时,如果它被声明为协变的,那么接口的方法不能接受T。如果没有将 T 插入列表的方法,那么您将永远不会将长颈鹿插入到老虎列表中,因为没有插入 anything 的方法。

相比之下,Java 说 在这个本地站点,我们可以协变地处理类型,并且我们承诺不会在这里调用任何可能违反类型安全的方法。

我对 Java 的特性没有足够的经验来说明在什么情况下哪个“更好”。 Java 技术当然很有趣。

【讨论】:

  • 我同意两种语言的处理方式不同。 C# 要求在接口上显式声明协变/逆变特性,以及仅通过此接口使用此特性的限制清楚地突出了与 Java 的显着差异。但是这两种语言都确保不会在编译时提交堆污染。老实说,在使用协变和逆变时,我喜欢 Java 中事物的可控性和简单性,因为您不需要显式地预定义它。
  • @edalorzo,运行时控制呢?
  • @Roman 如果您不允许在编译时提交堆污染,那么您的字节码或 IL 在运行时不会出错。据我所知,这就是泛型在两种语言中的实现方式的全部意义。唯一的区别是在 Java 中泛型类型是用类型擦除实现的,这意味着一些类型信息在编译时被丢弃。因此,有可能通过使用反射或使用遗留 API 来造成堆污染。但是,恕我直言,这是您必须真正打算做的事情,而不是您在 Java 中无意中做的事情。
  • @edalorzo,上面的 Eric 给出了他的观点:我们保证我们不会做坏事,而不是你不能做的时期(因为它不会编译)。似乎后者更安全,因为它不可能有意或无意地做。
  • @Roman 我完全同意。具有可具体化类型的泛型的 .Net 平台实现优于不可具体化的 Java 平台实现,因为它确保了编译时和运行时的类型安全。我无法反驳这一点。我的 cmets 显然是有偏见的,它们关注的是每种语言中表达式的简单性,而不是它们对解决方案的实现。显然,该语言的设计者有他们这样做的原因,但这是另一个讨论的主题,我无意进入那个洞。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-01-16
  • 1970-01-01
  • 2015-02-09
  • 1970-01-01
  • 2021-09-18
相关资源
最近更新 更多