【问题标题】:C# generics - without lower bounds by design?C# 泛型 - 没有设计下限?
【发布时间】:2010-12-31 21:33:47
【问题描述】:

我正在阅读 Coders at Work 中对 Joshua Bloch 的采访,他对 Java 5 中泛型的引入感到遗憾。他不喜欢具体的实现,主要是因为对差异的支持(Java 的通配符)使它变得不必要地复杂。

据我所知,C# 3 没有任何明确的、有界通配符,例如您不能声明采用 Asset 集合或任何 Asset 子类(Java 中的void PriceBatch(Collection<? extends Asset> assets)?)的方法 PriceBatch。

有谁知道为什么没有在 C# 中添加通配符和边界?这些特性是为了让语言更简单而故意遗漏的,还是他们还没有实现?

编辑: 神圣的烟雾,来自 Eric Lippert 本人的 cmets!在阅读了他和 Paul 富有洞察力的 cmets 之后,我意识到至少支持上限,并且上面的示例可以翻译成 C# 为:

void PriceBatch<T>(ICollection<T> assets) where T : Asset

另一方面,正如 Eric 在他的第二条评论中所说,显然不支持下限,例如可能没有办法将这个(有点做作的)Java 代码直接翻译成 C#:

public class Asset {}
public class Derivative extends Asset {}
public class VanillaOption extends Derivative {}

public static <T extends Asset> void copyAssets(Collection<T> src, Collection<? super T> dst) {
    for(T asset : src) dst.add(asset);
}

Collection<VanillaOption> src = new ArrayList<VanillaOption>();
[...]
Collection<Derivative> dst = new ArrayList<Derivative>();
[...]
copyAssets(src, dst);

我说的对吗?如果是这种情况,C# 有上限但没有下限是否有特殊原因?

【问题讨论】:

  • 我不明白这个问题; C# 有界泛型,并且自 C# 2 起就有。你所得到的 Java 是否支持构造泛型类型的 call-site 协变和逆变?因为 C# 不支持它; C# 4 将支持 declaration-site 变化。
  • 或者你得到的事实是,泛型方法上的类型参数边界只是必须派生的边界,而不是必须派生的边界? C#不支持后者;我相信 Java 和 Scala。
  • 谢谢,我已经更新了问题。

标签: c# generics wildcard


【解决方案1】:

一个复杂的问题。

首先让我们考虑您的基本问题,“为什么这在 C# 中是非法的?”

class C<T> where T : Mammal {} // legal
class D<T> where Giraffe : T {} // illegal

也就是说,泛型类型约束可以说“T 必须是可以分配给 Mammal 类型变量的任何引用类型”,但不能说“T 必须是任何引用类型,其中的变量可以分配给 Giraffe ”。为什么会有差异?

我不知道。那是在我加入 C# 团队之前很久。简单的答案是“因为 CLR 不支持它”,但设计 C# 泛型的团队与设计 CLR 泛型的团队是同一团队,所以这真的不是什么解释。

我的猜测很简单,与往常一样,必须设计、实施、测试、记录并交付给客户的功能必须得到支持;没有人为此功能做过任何这些事情,因此它不在语言中。我没有看到提议的功能有很大的、令人信服的好处。没有引人注目的好处的复杂功能往往会在这里被削减。

不过,这只是猜测。下次我碰巧和从事仿制药工作的人聊天时——他们住在英国,所以不幸的是,他们不像是在我身边——我会问的。

至于你的具体例子,我认为保罗是正确的。您不需要下限约束就可以在 C# 中使用它。你可以说:

void Copy<T, U>(Collection<T> src, Collection<U> dst) where T : U 
{ 
    foreach(T item in src) dst.Add(item);
}

也就是说,把约束放在T上,而不是U上。

【讨论】:

  • 感谢您的跟进。 《Effective Java》一书解释了一个有用的场景:假设您使用方法 PopAll 定义了一个类型 MyStack&lt;T&gt;,该方法将目标集合作为参数。如果您有MyStack&lt;Giraffe&gt;,您应该能够将其项目复制到List&lt;Animal&gt;,但要表达您必须说诸如public void PopAll&lt;X&gt;(ICollection&lt;X&gt; dst) where T : X之类的内容,这似乎确实是非法的。
  • 是的,这很有用。有趣的是,在 C# 中,虽然您不能直接这样做,但您可以通过在 Stack 上创建扩展方法来做到这一点!这引入了一个新的类型参数,然后可以对其进行约束。
  • 我可以看到的另一个例子是IImmutableList&lt;T&gt;System.Collections.Immutable 中的协方差;目前,IImmutableList&lt;T&gt; Add(T item) 阻止了这种情况,但IImmutableList&lt;TNew&gt; Add(TNew item) where TNew : T 可以保存它。 (嗯,所以一开始就不会在不可变类型上实现可变接口,但我想他们有理由采用这种设计。)在这里,扩展方法不会让你走得那么远。
【解决方案2】:

C# 4 引入了允许泛型中的协变和逆变的新功能。

还有其他 SO 帖子对此进行了更详细的讨论: How is Generic Covariance & Contra-variance Implemented in C# 4.0?

新功能不会在所有类型中自动启用此功能,但有一种新语法允许开发人员指定泛型参数是协变的还是逆变的。

C# 4 之前的 C# 版本具有与此类似的有限功能,因为它与委托和某些数组类型有关。对于委托,允许接受基本参数类型的委托。关于数组类型,我认为它是有效的,除非涉及到装箱。也就是说,客户数组可以是对象数组的大小写。但是,整数数组不能转换为对象数组。

【讨论】:

  • 数组也是:String[] 也是 Object[]。
  • 可变集合/数组必须是非变体的。否则会破坏类型安全。
【解决方案3】:

.net 已经有了等价的通配符,更符合逻辑命名的泛型类型约束,你可以毫无问题地做你描述的事情

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
         List<a> a = new List<a>();
         List<b> b = new List<b>();
         List<c> c = new List<c>();
         test(a);
         test(b);
         test(c);

        }

        static void test<T>(List<T> a) where T : a
        {
            return;
        }
    }
    class a
    {

    }
    class b : a
    {

    }
    class c : b
    {

    }
}

示例 2

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            ICollection<VanillaOption> src = new List<VanillaOption>();
        ICollection<Derivative> dst = new List<Derivative>();
         copyAssets(src, dst);
        }

        public static void copyAssets<T,G>(ICollection<T> src, ICollection<G> dst) where T : G {
            foreach(T asset in src) 
                dst.Add(asset);
        }
    }
    public class Asset {}
    public class Derivative : Asset {}
    public class VanillaOption : Derivative {}
}

此示例表示您在 java 中的示例的代码转换。

不过,我真的无法回答实际问题!

【讨论】:

  • 嗨,保罗,一个问题:您的代码示例的含义是用“where T:a”表示的通用约束......因为它不仅会“处理”一个实例的“a”本身,但是,任何对象,无论如何“间接地”从“a”下降:是否等同于Java中泛型中的通配符?只是为了我自己的利益而寻求澄清;没有暗示或有意对您的回答进行批评。在您的示例中,字母“a”具有三种不同的含义(类、内部变量和参数名称)这一事实让我更难理解,仅供参考。
  • 是的,有点不清楚,抱歉。我已经写了一个你的下界代码的例子,功能上是一样的,但它不使用下界!
  • 嗨,保罗,感谢您更新的答案。我的问题实际上是为什么 C#3 在这方面只有 Java 的方差支持的一个子集,以及它是否是有意决定做“正确”Java 做“错误”的事情,还是与 CLR 限制或诸如此类的事情有关。很难找到一个有意义的例子来说明下限何时有用,正如我人为的例子所说明的那样,也许这就是答案的一部分:)
  • ehnmark:C# 在 Java 之前实现了泛型。决定的一个因素可能是 .NET 在 C# 语言和 CLR 中都实现了泛型,而 Java 在 Java 语言中实现了泛型,而不是在 JVM 中实现。无需在 JVM 中实现任何支持可能会更直接地添加此功能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-21
  • 2011-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多