C#中的协变和逆变

13

我是一名Java开发人员,正在学习C#编程。因此,我会将自己所知道的知识与正在学习的内容进行比较。

我已经花了几个小时来尝试使用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#仅在接口上支持协变/逆变,并且只有在显式声明为此(out/in)时才支持。我认为我无法重现这种情况,因为我找不到所有数字的共同祖先,但如果存在共同祖先,我相信我可以使用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一样。也许可以用两个不同的接口来实现。

你有什么想法如何实现这个问题吗?

非常感谢大家为我提供任何答案。我相信我会从你们提供的任何示例中学到很多东西。


1
我非常确定你做不到,因为.NET团队在他们的无限智慧中没有提供一种谈论原始类型类(例如数字、浮点数)的方法。因此,在泛型方法中不能使用操作符+来处理泛型类型的参数,并且没有办法指定应该能够这样做。这是一个明显的漏洞,但就是这样。 - siride
1
我之前问过一个类似概念的问题:https://dev59.com/Tm035IYBdhLWcg3wSN4G - ken
4个回答

24
不直接回答您的问题,我将回答一些略有不同的问题:
C#是否有一种可以以支持算术运算符的类型进行泛型化的方法?
不容易。它能够实现一个Sum方法,该方法可以添加整数、双精度浮点数、矩阵、复数、四元数等等,这是一个相当频繁被请求的功能,但由于它是一个大的特性,并且它从未在优先级列表中得到足够高的地位来证明其包含在语言中的正当性。我个人很喜欢它,但您不应该期望在C# 5中看到它。也许在假设的未来版本中。
Java的“调用站点”协变/逆变和C#的“声明站点”协变/逆变有什么区别?
在实现级别上的根本区别,当然是Java泛型是通过擦除实现的;虽然您获得了对泛型类型的愉快的语法和编译时类型检查的好处,但您并没有必要获得在C#中可能会获得的性能优势或运行时类型系统集成优势。
但就我的角度来看,更有趣的差异是Java的变量规则是在本地强制执行的,而C#的变量规则是在全局范围内强制执行的。
也就是说:某些变体转换是危险的,因为它们意味着某些非类型安全操作不会被编译器捕获。经典示例是:
老虎是哺乳动物。
X的列表与X协变。(假设如此。)
老虎列表因此是哺乳动物列表。
哺乳动物列表可以插入长颈鹿。
因此,您可以将长颈鹿插入老虎列表中。
这显然违反了类型安全性以及长颈鹿的安全性。
C#和Java使用两种不同的技术来防止此类型安全性违规。 C#表示,当声明I接口时,如果它被声明为协变,则接口中不能有任何接受T的方法。如果没有将T插入列表的方法,则永远不会将长颈鹿插入老虎列表中,因为没有插入任何东西的方法。
相比之下,Java表示在本地站点,我们可以对类型进行协变处理,并承诺不会在此处调用任何可能违反类型安全的方法。 我没有足够的Java功能经验,无法说出在什么情况下哪种更好。 Java技术肯定很有趣。

我同意这两种语言处理方式不同的观点。C#要求在接口上明确声明协变/逆变特性,以及限制只能通过这些接口使用此特性,这显然与Java有很大的区别。但是,这两种语言都确保在编译时无法提交堆污染。说实话,当涉及到使用协变和逆变时,我喜欢Java中事物的控制和简单性,因为你不需要明确预定义它。 - Edwin Dalorzo
@edalorzo,运行时控制怎么样? - Roman Royter
@Roman 如果您在编译时不允许进行堆污染,则在运行时,您的字节码或 IL 不可能出错。这就是泛型在两种语言中实现的整个重点,据我所知。唯一的区别在于,在 Java 中,通用类型是使用类型擦除实现的,这意味着在编译时会丢弃一些类型信息。因此,通过使用反射或遗留 API,可能会发生堆污染。然而,依我之见,这是您必须真正打算做的事情,而不是您在 Java 中无意中做的事情。 - Edwin Dalorzo
@edalorzo,Eric在上面提出了他的观点:我们承诺不会做坏事,而不是你根本无法这样做(因为它不会编译)。似乎后者更安全,因为它使有意或无意地这样做变得不可能。 - Roman Royter
@Roman 我完全同意。.Net平台实现具有可重用类型的泛型比不具备可重用类型的Java平台实现更好,因为它可以在编译时和运行时都确保类型安全。我无法反驳这一点。我的评论显然是有偏见的,只是关于每种语言表达简单性的问题,而不是它们解决方案的实现。显然,语言设计者有他们的原因,但这是另一个讨论的主题,而且我并不想深入探讨这个问题。 - Edwin Dalorzo

7

对于你提出的第二个问题,你不需要使用逆变性,只需声明第一个类型可以转换为第二个即可。再次使用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”是什么吗? - Edwin Dalorzo
你是对的,我已经在答案中将其删除了。'this' 将静态方法(在静态类中)转换为扩展方法,这意味着它可以像 IList<TSource> 的方法一样调用。有关更多信息,请参见 此链接。请注意,编译器还进行了一些类型推断,因此它可以在不明确告知 TSourceTDest 的情况下计算出它们是什么。 - markmuetz
非常感谢,你的答案很好。这就是我在寻找的答案。实际上,它可以在Java中以相同的方式实现 public static <P,C extends P> void copy(List<C> source,List<P> destiny),正如你指出的那样,完全避免了协变性问题。 - Edwin Dalorzo

4
您可以使用 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


所有可转换类型都应该可以转换为十进制数,还是如果条件不满足就期望在运行时发生异常? - Edwin Dalorzo
据我所知,所有内置的数字类型都是如此,显然无法控制自定义实现,因此异常肯定是可能的。 - Paul Tyng
这是一个很棒的答案,今天在办公室里,它引起了我同事们之间有趣的辩论。我们是一支由Java和C#开发人员组成的团队,你可以想象我们每天在这里进行的战争。 :) - Edwin Dalorzo

2

在.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;
}

如果列表中的任何对象不是数字或者没有实现IConvertible接口,你必须在Convert.ToDouble过程中捕获任何错误。

更新

在这种情况下,我个人会使用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;
}

2
你可以使用sum<T>(List<T> numbers) where T : IConvertible来添加通用约束。 - Paul Tyng
你不能将 List<int> 传递到这个方法中(会得到一个无效的转换错误)。如果你将参数类型更改为 IEnumerable<object>,并传入例如 myInts.Cast<object>(),它将起作用,但这需要在进入方法之前对集合进行强制转换。 - markmuetz
我理解你的观点。因此,这不能被静态检查为数字,如果我试图转换不兼容于double的东西,我会得到运行时异常。 - Edwin Dalorzo
@edalorzo -- 请查看更新。类型T现在将在编译时进行检查,以实现IConvertible,这意味着它可以转换为数字。 - ken
这里使用 lambda 表达式解决问题,给它加一分。我一直期待在 JDK 1.8 中看到它们的实现 :) - Edwin Dalorzo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接