C#型系统是否可靠且可判定?

C#型系统是否可靠且可判定?,c#,types,covariance,type-systems,C#,Types,Covariance,Type Systems,我知道Java的类型系统是不健全的(它无法对语义合法的结构进行类型检查)和不可判定的(它无法对某些结构进行类型检查) 例如,如果在类中复制/粘贴以下代码段并对其进行编译,编译器将因StackOverflowException(how apt)而崩溃。这是不可判定的 static class ListX<T> {} static class C<P> extends ListX<ListX<? super C<C<P>>>>

我知道Java的类型系统是不健全的(它无法对语义合法的结构进行类型检查)和不可判定的(它无法对某些结构进行类型检查)

例如,如果在类中复制/粘贴以下代码段并对其进行编译,编译器将因
StackOverflowException
(how apt)而崩溃。这是不可判定的

static class ListX<T> {}
static class C<P> extends ListX<ListX<? super C<C<P>>>> {}
ListX<? super C<Byte>> crash = new C<Byte>();
静态类ListX{}

静态类C

扩展了ListX

创建C#编译器无法在合理时间内解决的问题并不特别困难。它提出的一些问题(通常与泛型/类型推理有关)是NP难问题。埃里克·利珀特:

C型系统是可判定的吗

如果编译器在理论上总是能够决定程序类型是否在有限时间内检查,那么类型系统是“可判定的”

C类型系统不可判定。

C#有“名义”子类型——也就是说,当你声明一个类时,你给出类和接口的名称,并通过名称说出基类和接口是什么

C#也有泛型类型,并且在C#4中,泛型接口的协方差和逆变

这三件事——名义子类型、泛型接口和逆变——足以使类型系统不可判定(在子类型相互提及的方式上没有其他限制的情况下)

当这个答案最初写在2014年时,这是可疑的,但不为人所知。这一发现的历史很有趣

首先,C#generic type系统的设计者们想知道同样的事情,并在2007年写了一篇论文,描述了类型检查可能出错的不同方式,以及可以对标称子类型系统施加哪些限制以使其可判定

在我的博客上可以找到关于这个问题的更温和的介绍,这里:

);一位研究人员注意到了帖子中提到的问题并解决了它;我们现在知道,如果在混合中加入了一般性逆变,那么名词性子类型通常是不可判定的。您可以将图灵机编码到类型系统中,并强制编译器模拟其操作,并且由于“ThisTM停止吗?”问题是不可判定的,因此类型检查必须是不可判定的

有关详细信息,请参阅

C#型系统是否完好

如果我们保证在编译时进行类型检查的程序在运行时没有类型错误,那么类型系统是“可靠的”

C型系统不健全。

原因有很多,但我最不喜欢的是数组协方差:

Giraffe[] giraffes = new[] { new Giraffe() };
Animal[] animals = giraffes; // This is legal!
animals[0] = new Tiger(); // crashes at runtime with a type error
这里的想法是,大多数采用数组的方法只读取数组,而不写入数组,从长颈鹿数组中读取动物是安全的。Java允许这样做,所以CLR也允许这样做,因为CLR设计者希望能够在Java上实现变体。C#允许它是因为CLR允许它。结果是,每次您将任何内容写入基类的数组时,运行时都必须进行检查,以验证该数组不是不兼容派生类的数组。常见的情况会变慢,因此罕见的错误情况会出现异常

不过,这提出了一个好的观点:C#至少在类型错误的后果方面定义得很好。运行时的类型错误会以异常的形式产生正常的行为。它不像C或C++那样编译器可以并且会直接生成任意疯狂的代码。 C#型系统在设计上有一些其他方面是不健全的

    >P>如果您认为空引用异常是一种运行时类型错误,那么C*Pr.C 8是非常不健全的,因为它几乎没有防止这种错误的任何东西。C#8在支持静态检测空值错误方面有很多改进,但空值引用类型检查不完善;它既有假阳性也有假阴性。其思想是,即使不是100%可靠,一些编译时检查也比没有好

  • 许多强制转换表达式允许用户重写类型系统并声明“我知道此表达式在运行时将属于更特定的类型,如果我错了,则抛出异常”。(有些强制转换的意思正好相反:“我知道这个表达式是X类型的,请生成代码将其转换为Y类型的等效值”。这些通常是安全的。)因为这是一个开发人员明确表示他们比类型系统更了解的地方,所以很难将导致崩溃的原因归咎于类型系统

即使代码中没有强制转换,也有一些特性可以生成类似强制转换的行为。例如,如果你有一个动物列表,你可以说

foreach(Giraffe g in animals)
如果那里有一只老虎,你的程序就会崩溃。正如规范所指出的,编译器只是代表您插入一个cast。(如果你想绕过所有长颈鹿而忽略老虎,那就是
foreach(动物中的长颈鹿g.OfType())

  • C#的
    unsafe
    子集进行所有下注;您可以使用它任意打破运行时的规则。关闭安全系统会关闭安全系统,因此当您关闭健全性检查时,C#不健全也就不足为奇了

    • 当然,@Eric Lippert的答案是权威的。我只想强调,上面的方差问题只适用于数组

      当使用泛型时,它就消失了,因为这样你就只能有协方差、反方差或方差。这不允许以下应用程序之一:成员分配、成员查询或集合分配:

      IList<Giraffe> giraffes3 = new List<Giraffe>{new()};
      IList<Animal> animals3 = giraffes3; // ! does NOT compile!
      
      IReadOnlyList<Giraffe> giraffes1 = new List<Giraffe>{new()};
      IReadOnlyList<Animal> animals1 = giraffes1; // This is legal!
      animals1[0] = new Tiger(); // ! does NOT compile!
      
      不变性不允许集合分配:

      IList<Giraffe> giraffes3 = new List<Giraffe>{new()};
      IList<Animal> animals3 = giraffes3; // ! does NOT compile!
      
      IReadOnlyList<Giraffe> giraffes1 = new List<Giraffe>{new()};
      IReadOnlyList<Animal> animals1 = giraffes1; // This is legal!
      animals1[0] = new Tiger(); // ! does NOT compile!
      

      因此,只要您尽量避免在API中使用数组,并且仅在内部使用它们(例如,为了性能),您就应该很好了。

      实际上,多搜索一点
      Giraffe[] giraffes = {new()};
      Animal[] animals = giraffes; // This is legal!
      animals[0] = new Tiger(); // ! Runtime Exception !