为什么C#泛型不能像C++模板一样从一个泛型类型参数派生?

46
为什么C#泛型不能像C++模板一样从一个泛型类型参数派生?我知道这是不可能的,因为CLR不支持这个,但为什么呢?
我知道C++模板和C#泛型之间存在深刻的差异-前者是编译时实体,必须在编译期间解决,而后者是第一类运行时实体。
尽管如此,我仍然无法看出CLR设计人员为什么没有想出一种方案,最终使CLR泛型类型能够从其泛型类型参数之一派生。毕竟,这将是一个非常有用的功能,我个人非常需要它。
编辑:
我想知道一个硬核问题,修复它会导致如此高的代价来实现这个功能,这正好证明了它还没有被实现。例如,检查这个虚构声明:
class C<T> : T
{
}

作为Eric Lippert所注意到的,如果“ T是一个结构体怎么办? T是一个密封类类型怎么办? T是一个接口类型怎么办? T是C! T是从C派生的类? T是一个具有抽象方法的抽象类型? 如果T的可访问性比C低怎么办? T是System.ValueType怎么办?(您可以拥有一个继承自System.ValueType的非结构体吗?)System.Delegate,System.Enum等等呢?
正如Eric所说,“这些都是容易、显而易见的问题”。确实,他是正确的。我对一些既不容易也不明显的问题感兴趣,这些问题很难解决。

阅读这些帖子。我猜ROI是原因:http://blogs.msdn.com/ericlippert/archive/2009/10/05/why-no-extension-properties.aspx和http://blogs.msdn.com/ericgu/archive/2004/01/12/57985.aspx - Alex Reitbort
4
C++比C#更复杂,而且你可以在C++中做一些C#无法做到的事情,这是有关联的原因。因此,在可预见的未来,一些人会希望使用其他编程语言来完成某些项目,而不是使用C#。 - David Thornley
1
我不明白:C#编译器难道不应该在实例化给定模板时已经知道这些问题的答案吗?例如,有一个类C<T> : T并尝试在我的代码中编写C<int> i = new C<int>(),编译器不应该像我一开始写C_int i = new C_int()那样处理吗?(请注意,在后一种情况下,编译器将抛出错误“无法从密封类int派生”-但它确实处理了这种情况)。为什么所有这些问题都必须在尝试实例化模板之前回答呢? - Zuzu Corneliu
5个回答

67

首先,问问自己class C<T> : T { }会出现什么问题。立刻想到了很多问题:

T是结构体怎么办?T是密封类类型怎么办?T是接口类型怎么办?T是C<T>?T是从C<T>派生的类怎么办?T是具有抽象方法的抽象类型怎么办?T的可访问性比C小怎么办?T是System.ValueType怎么办?(你可以从System.ValueType继承非结构体吗?)System.Delegate,System.Enum等等呢?

这些是简单而明显的问题。该提议的功能会引发更加微妙的问题,涉及类型及其基类型之间的交互,其中每一个都必须仔细指定、实现和测试。我们肯定会错过一些问题,从而在未来造成破坏性变化,或者给运行时带来实现定义的行为。

代价将是巨大的,所以好处必须是巨大的。我没有看到巨大的好处。


7
您是否认为在C++模板中实现这个特性会更容易一些?因为它已经存在于那里,并且非常有用和异常强大。我可以给出一个例子,说明我在目前的工作中这个特性会非常有用。如果要使用Reflection.Emit动态生成一个类型,但是其基础类型只在运行时才能确定,那么从静态编译的泛型派生出其类型参数将非常有用,以便动态生成的类型可以从中派生。 - mark
35
对于 C++ 模板来说,实现起来要容易得多,因为每次构造模板时都会对其进行重新编译。如果类型参数是引用类型,则针对所有构造,C# 泛型只需编译一次并在运行时生成相同的代码。这是一个根本性的区别;这意味着 C# 泛型必须正确地处理任何可能的构造,而 C++ 模板只需要正确地处理源代码中有限数量的实际构造。 - Eric Lippert
2
好观点。但是泛型类型是开放类型。无法拥有任何开放泛型类型的实例。因此,直到实际实例化泛型类型,其实际内存布局可能仍未解决。事实上,即使在今天,聚合泛型参数类型实例的泛型类型具有不明确的内存布局,直到实例化为止。目前我还不确定是否不可能提出一组约束条件(需要新类型的约束)来使所讨论的继承工作。 - mark
3
@mark已经成功地提供了一个成本清单,而你有一份收益清单。这些成本非常高昂。你正在要求对类型系统进行更改。因此,为了作为一项特性具有价值,它必须对一小部分非常重要的项目有非常惊人的收益(即是必需品,不仅仅是一个需要痛苦和P/Invoke的大障碍),或者对大量项目有惊人的收益。后者绝对不是真的;大多数程序员都不知道(并且会故意保持不知道)做这件事的任何理由,因此不会实现任何收益。 - Brian
我刚刚意识到,我在不同的基类上工作时,要输入相同的代码两次,这让我很困扰。 - Joshua
显示剩余2条评论

36

好的,如果你不喜欢之前的答案,那么让我们换个思路。

你的问题包含了一个错误:我们需要一个 理由 实现一个功能。相反地,我们需要非常,非常好的理由来实现任何功能。功能在其前期成本、维护成本和机会成本方面都是极其昂贵的。(也就是说,你在功能X上花费的时间是你不能用来做功能Y的时间,这可能会阻止你永远做功能Z。)为了对我们的客户和利益相关者负责地提供价值,我们不能实现每个人都喜欢的所有功能。

运行时设计师无需证明他们为什么没有实现你认为特别好的功能。功能根据其成本与对用户的收益进行优先排序,而用户并没有特别强烈地要求这种继承方式。这种特定的功能会极大地改变运行时类型系统分析的工作方式,在消费泛型的每种语言中产生深远影响,并且我认为它提供的好处非常少。

我们在编译器中使用这种继承方式 -- 用C++编写 -- 生成的代码很难遵循,难以维护,也很难调试。我一直在尽力逐渐消除这样的代码。除非有极其强大的好处,否则我反对在C#中启用同样糟糕的模式。

描述那种巨大好处的任务落在了想要该功能的人身上,而不是落在了必须实现它的人身上。那么这个功能的显著好处是什么呢?


7
虽然我的问题可能会导致人们这样想,但我提出这个问题的原因是,作为一名以前的C++开发者,我认为从模板参数类型继承能力是一个非常有用的功能,在某些重要的库中广泛使用,比如ATL和boost(我不确定STL是否使用它)。虽然C++模板和.NET泛型之间存在根本性的差异,但对我来说,期望.NET泛型也具备这个功能很自然。我非常惊讶地发现它们没有这个功能,因此我推测微软希望这个功能,但选择不实现它。 - mark
2
无论如何,我接受挑战并尝试收集令人信服的理由,说明为什么这个功能非常重要。为此,我将开始另一个社区维基问题,并让大家提供理由。我会在发布后立即添加一个带有问题链接的评论。 - mark
为什么在.NET中我们不能像ATL那样拥有一些东西,它对于发布费率非常好,因为很少有人能够理解它 :-) - Ian Ringrose
3
我们在编译器中使用这种继承方式,你不觉得我们使用它的事实说明了它的用例吗? - user541686
@Mehrdad:好吧,某个人在过去认为这是一个好主意。不是我。我现在不记得使用场景了;上次看那段代码可能是在2009年写这个答案的时候。 - Eric Lippert
显示剩余2条评论

15

以下是可能有用的代码示例:

public class SpecialDataRow<T> : T where T : DataRow
{
    public int SpecialFactor { get; set; }
}

这将使得可以从DataRow创建“特殊”行,还可以从任何派生自DataRow的行(如生成的类型数据集)中进行创建。
我没有看到其他编写此类的方式。

如果您想要为多个DataRow的派生类添加方法,您可以使用扩展方法。 - oɔɯǝɹ
好的观点。但是您将把这些方法添加到所有实例中,而不仅仅是那些“特殊”的实例(在我看来是个小问题)。 - maliger
组合在这里非常有效。类似这样:public class SpecialDataRow : DataRow { SpecialDataRow(DataRow wrapped) {...} public int SpecialFactor { get; set; } /正常的DataRow方法转发到wrapped/ } - Gideon Engelberth
7
这是一种完全有效且可取的编写泛型派生类的方式,在许多情况下非常有帮助。通过强制指定 where T:RootClass,您可以消除其他帖子中提到的99%问题。 - Andrew Hanlon
@AndrewHanlon但是这个代码无法编译。为什么这段代码不好?我认为这是非常有用的代码。 - sorosh_sabz
@sorosh_sabz 已经过去了九年,所以我只能猜测我当时可能在谈论使用“递归”泛型类模式的情况。但很可能我还没有理解它们是如何通过“反转”泛型的使用来添加“特性”的。非常抱歉! - Andrew Hanlon

2
这会有什么用处呢?
请记住,尽管名称中含有“泛型”,但它们从未旨在支持泛型编程。
为了支持这样的功能,他们必须对CLR进行一些非常重大的更改。
您需要定义一个从编译时甚至不存在的类型派生的类。
为什么他们要费心去做这样的事情,并且在很大程度上损害他们的类型系统,只是为了添加这个功能呢?这值得吗?
如果您认为是这样,请告诉他们原因。在connect.microsoft.com上写下反馈,告诉他们为什么这个功能如此基础,必须被添加。

1
在需要为大量类型提供通用的功能添加方式的情况下,这将是一个有用的特性。例如,一个类可以表现得与现有的IDisposable类完全相同,除了IDisposable具有一个“生命周期计数器”,该计数器将通过“AddUser()”方法递增并通过“Dispose”递减;只有当生命周期计数器达到零时,底层对象才会被处理。 - supercat

1

C++ 模板不能与 C# 泛型进行比较。C++ 模板像宏一样被预处理,而 .NET 中的泛型由运行时处理。

不过还有其他人知道更多关于这方面的事情...


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