Java 的使用站点差异与 C# 的声明站点差异相比如何?

发布于 2024-10-03 10:38:49 字数 136 浏览 0 评论 0原文

我的理解是,在 C# 中指定泛型的方差发生在类型声明级别:当您创建泛型类型时,您可以指定类型参数的方差。另一方面,在 Java 中,在使用泛型时指定了方差:当您创建某种泛型类型的变量时,您指定其类型参数如何变化。

每个选项的优点和缺点是什么?

My understand is that specifying variance for generics in C# happens at the type declaration level: when you're creating your generic type, you specify the variance for the type arguments. In Java, on the other hand, variance is specified where a generic is used: when you create a variable of some generic type, you specify how its type arguments can vary.

What are the pros and cons to each option?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

百善笑为先 2024-10-10 10:38:49

我只是要回答声明站点差异和使用站点差异之间的差异,因为虽然 C# 和 Java 泛型在许多其他方面存在差异,但这些差异大多与差异正交。

首先,如果我没记错的话,使用站点差异严格来说比声明站点差异更强大(尽管以简洁为代价),或者至少Java的通配符是(实际上比使用站点差异更强大)。这种增强的功能对于大量使用有状态构造的语言特别有用,例如 C# 和 Java(但 Scala 则少得多,特别是因为它的标准列表是不可变的)。考虑 List(或 IList)。由于它具有添加 E 和获取 E 的方法,因此它相对于 E 是不变的,因此不能使用声明站点方差。但是,对于使用站点差异,您只需输入 List<+Number> 即可获取 ListList<-Number> 的协变子集获取List的逆变子集。在声明站点语言中,库的设计者必须为每个子集创建单独的接口(如果允许类的多重继承,则为类),并让 List 扩展这些接口。如果库设计者不这样做(请注意,C# 的 IEnumerable 仅执行 IList 协变部分的一小部分),那么您就不走运了,并且您有要想解决同样的麻烦,就必须使用一种没有任何差异的语言。

这就是使用站点继承相对于声明站点继承的优点。声明站点继承相对于使用站点继承的优点对于用户来说基本上是简洁的(假设设计者努力将每个类/接口分离为其协变和逆变部分)。对于 IEnumerableIterator 之类的东西,最好不必在每次使用接口时都指定协方差。 Java 通过使用冗长的语法(除了双变量之外,Java 的解决方案基本上是理想的解决方案)使这一点变得特别烦人。

当然,这两种语言特性可以共存。对于自然协变或逆变的类型参数(例如在 IEnumerable/Iterator 中),请在声明中进行声明。对于自然不变的类型参数(例如在 (I)List 中),请在每次使用时声明您想要哪种类型的变化。只是不要为具有声明站点差异的参数指定使用站点差异,因为这只会让事情变得混乱。

还有其他更详细的问题我没有讨论(例如通配符实际上比使用站点差异更强大),但我希望这能回答您对内容的问题。我承认我对使用站点差异有偏见,但我试图描绘在与程序员和语言研究人员的讨论中出现的两者的主要优点。

I am just going to answer the differences between declaration-site and use-site variance, since, while C# and Java generics differ in many other ways, those differences are mostly orthogonal to variance.

First off, if I remember correctly use-site variance is strictly more powerful than declaration-site variance (although at the cost of concision), or at least Java's wildcards are (which are actually more powerful than use-site variance). This increased power is particularly useful for languages in which stateful constructs are used heavily, such as C# and Java (but Scala much less so, especially since its standard lists are immutable). Consider List<E> (or IList<E>). Since it has methods for both adding E's and getting E's, it is invariant with respect to E, and so declaration-site variance cannot be used. However, with use-site variance you can just say List<+Number> to get the covariant subset of List and List<-Number> to get the contravariant subset of List. In a declaration-site language the designer of the library would have to make separate interfaces (or classes if you allow multiple inheritance of classes) for each subset and have List extend those interfaces. If the library designer does not do this (note that C#'s IEnumerable only does a small subset of the covariant portion of IList), then you're out of luck and you have to resort to the same hassles you have to do in a language without any sort of variance.

So that's the advantages of use-site inheritance over declaration-site inheritance. The advantage of declaration-site inheritance over use-site inheritance is basically concision for the user (provided the designer went through the effort of separating every class/interface into its covariant and contravariant portions). For something like IEnumerable or Iterator, it's nice not to have to specify covariance every single time you use the interface. Java made this especially annoying by using a lengthy syntax (except for bivariance for which Java's solution is basically ideal).

Of course, these two language features can coexist. For type parameters that are naturally covariant or contravariant (such as in IEnumerable/Iterator), declare so in the declaration. For type parameters that are naturally invariant (such as in (I)List), declare what kind of variance you want each time you use it. Just don't specify a use-site variance for arguments with a declaration-site variance as that just makes things confusing.

There are other more detailed issues I haven't gone into (such as how wildcards are actually more powerful than use-site variance), but I hope this answers your question to your content. I'll admit I'm biased towards use-site variance, but I tried to portray the major advantages of both that have come up in my discussions with programmers and with language researchers.

但可醉心 2024-10-10 10:38:49

大多数人似乎更喜欢声明站点差异,因为它使库的用户更容易(同时对库开发人员来说有点困难,尽管我认为库开发人员必须考虑 。

但请记住,Java 和 C# 都不是良好语言设计的示例

虽然由于 Java 5 中的兼容 VM 改进和类型擦除,Java 获得了正确的方差并独立于 JVM 工作,但使用站点方差使使用有点麻烦,并且类型擦除的特定实现招致了当之无愧的批评。

C# 的声明站点差异模型减轻了库用户的负担,但在引入具体化泛型期间,他们基本上将差异规则构建到了虚拟机中。
即使在今天,由于这个错误,他们也不能完全支持协变/逆变(并且具体化集合类的非向后兼容引入将程序员分成了两个阵营)。

这对所有针对 CLR 的语言构成了困难的限制,也是替代编程语言在 JVM 上更加活跃的原因之一,尽管 CLR 似乎具有“更好的功能”。

让我们看看Scala:Scala 是一个运行在 JVM 上的完全面向对象的函数式混合体。
它们像 Java 一样使用类型擦除,但泛型的实现和(声明站点)方差都比 Java(或 C#)更容易理解、更简单、更强大,因为 VM 没有对方差必须如何执行强加规则。工作。 Scala 编译器会检查方差符号,​​并可以在编译时拒绝不健全的源代码,而不是在运行时抛出异常,而生成的 .class 文件可以在 Java 中无缝使用。

声明站点差异的一个缺点是,在某些情况下它似乎使类型推断变得更加困难。

同时,Scala 可以使用基本类型,而无需像 C# 那样将它们装箱到集合中,方法是使用 @specialized 注释,该注释告诉 Scala 编译器生成专用于该类型的类或方法的一个或多个附加实现。请求的原始类型。

Scala 还可以通过使用清单来“几乎”具体化泛型,这允许它们像在 C# 中一样在运行时检索泛型类型。

Most people seem to prefer declaration-site variance, because it makes it easier for users of the library (while making it a bit harder for the library developer, although I would argue that the library developer has to think about variance regardless of where the variance is actually written.)

But keep in mind, that neither Java nor C# are examples of good language design.

While Java got variance right and working independently of the JVM because of compatible VM improvements in Java 5 and type-erasure, the use-site variance makes usage a bit cumbersome and the particular implementation of type-erasure has drawn well-deserved criticism.

C#'s model of declaration-site variance takes the burden away from the user of the library, but during their introduction of reified generics they basically built the variance rules into the their VM.
Even today they can't fully support co-/contravariance because of this mistake (and the non backward-compatible introduction of the reified collection classes has split the programmers into two camps).

This poses a difficult restriction on all languages targeting the CLR and is one reason why alternative programming languages are much more lively on the JVM although it seems that the CLR has "much nicer features".

Let's look at Scala: Scala is an fully object-oriented, functional hybrid running on the JVM.
They use type erasure like Java, but both the implementation of Generics and the (declaration-site) variance are easier to understand, more straightforward and powerful than Java's (or C#'s), because the VM doesn't impose rules on how variance has to work. The Scala compiler checks the variance notations and can reject unsound source code at compile time instead of throwing exceptions at runtime, while the resulting .class files can seamlessly be used from Java.

One disadvantage of declaration site variance is that it seems to make type inference harder in some cases.

At the same time Scala can use primitive types without boxing them in collections like in C# by using the @specialized annotation which tells the Scala compiler to generate one or multiple additional implementations of a class or method specialized to the requested primitive type.

Scala can also "almost" reify generics by using Manifests which allows them to retrieve the generic types at runtime like in C#.

浮世清欢 2024-10-10 10:38:49

Java 风格泛型的缺点

一个后果是 Java 版本仅适用于引用类型(或装箱值类型),而不适用于值类型。在我看来,这是最大的缺点,因为它在很多情况下阻碍了高性能泛型,并且需要手动编写专门的类型。

它不保证像“此列表仅包含 x 类型的对象”这样的不变量,并且需要在每个 getter 处进行运行时检查。泛型类型确实存在。

使用反射时,您无法询问通用对象的实例它具有哪些通用参数。

Java 风格泛型的优点

您可以获得差异/可以在不同的泛型参数之间进行转换。

Disadvantages of Java style generics

One consequence is that the java version only works with reference types(or boxed value-types) and not value-types. IMO that's the biggest disadvantage since it prevents high performance generics in a lot of scenarios and requires manual writing of specialized types.

It doesn't guarantee an invariant like "This list only contains objects of type x" and needs runtime checks at every getter. The generic type really exists.

When using reflection you can't ask an instance of a generic object which generic parameters it has.

Advantages of Java style generics

You get variance/can cast between different generic parameters.

马蹄踏│碎落叶 2024-10-10 10:38:49

Java:自 Java 5 起使用站点变体泛型。自 1.0 起使用不同语法的损坏协变数组。没有泛型的运行时类型信息。

C#:自 C# 2.0 起使用站点方差泛型。在 C# 4.0 中添加了声明站点差异。自 1.0 以来,协变数组的语法不同(与 Java 相同)。 “具体化”泛型意味着类型信息在编译时不会丢失。

Scala:自该语言的早期版本(至少自 2008 年以来)以来,使用站点/声明站点都存在差异。数组不是单独的语言功能,因此您使用相同的泛型语法和类型差异规则。某些集合是在 VM 级别使用 JVM 数组实现的,因此与 Java 代码相比,您可以获得相同或更好的运行时性能。

详细说明 C#/Java 数组类型安全问题:您可以将 Dog[] 转换为 Pet[] 并添加 Cat 并触发编译时未捕获的运行时错误。 Scala 正确地实现了这一点。

Java: Use-site variance generics since Java 5. Broken covariant arrays with a different syntax since 1.0. No runtime type info of generics.

C#: Use-site variance generics since C# 2.0. Added declaration site variance in C# 4.0. Broken covariant arrays with a different syntax since 1.0 (identical issue to Java). "reified" generics meaning that type info isn't lost at compilation time.

Scala: Both use-site/declaration-site variance since early versions of the language (at least since 2008). Arrays are not a separate language feature, so you use the same generics syntax and type variance rules. Certain collections are implemented at the VM level with JVM arrays so you get equal or better runtime performance compared with Java code.

To elaborate on the C#/Java array type safety problem: You can cast a Dog[] to a Pet[] and add a Cat and trigger a runtime error that isn't caught at compilation time. Scala implemented this correctly.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文