为什么我的C#数组在转换为对象时会丢失类型符号信息?

Rng*_*bus 42 c# arrays types command-line-interface

调查一个bug,我发现这是由于c#中的这种奇怪:

sbyte[] foo = new sbyte[10];
object bar = foo;
Console.WriteLine("{0} {1} {2} {3}",
        foo is sbyte[], foo is byte[], bar is sbyte[], bar is byte[]);
Run Code Online (Sandbox Code Playgroud)

输出为"True False True True",而我希望" bar is byte[]"返回False.显然酒吧既是一个byte[]又是一个sbyte[]?同样的情况对于其他符号/无符号类型,如Int32[]VS UInt32[],但不是说Int32[]VS Int64[].

谁能解释这种行为?这是在.NET 3.5中.

Eri*_*ert 66

更新:我使用这个问题作为博客条目的基础,在这里:

http://blogs.msdn.com/ericlippert/archive/2009/09/24/why-is-covariance-of-value-typed-arrays-inconsistent.aspx

有关此问题的详细讨论,请参阅博客评论.谢谢你这个好问题!


您偶然发现了CLI类型系统和C#类型系统之间有趣且不幸的不一致.

CLI具有"赋值兼容性"的概念.如果已知数据类型S的值x与已知数据类型T的特定存储位置y"分配兼容",则可以将x存储在y中.如果没有,那么这样做不是可验证的代码,验证者将不允许它.

例如,CLI类型系统表示引用类型的子类型与引用类型的超类型兼容.如果你有一个字符串,你可以将它存储在一个object类型的变量中,因为它们都是引用类型而string是object的子类型.但事实恰恰相反; 超类型与子类型不兼容.如果没有先将其作为字符串变量,就不能将只有对象的东西粘贴到字符串类型的变量中.

基本上"赋值兼容"意味着"将这些精确位置于此变量中是有意义的".从源值到目标变量的赋值必须是"表示保留".有关详细信息,请参阅我的文章:

http://ericlippert.com/2009/03/03/representation-and-identity/

CLI的一个规则是"如果X与Y兼容,则X []与Y []"分配兼容.

也就是说,数组在赋值兼容性方面是协变的.这实际上是一种破碎的协方差; 有关详细信息,请参阅我的文章.

http://blogs.msdn.com/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx

这不是C#的规则.C#的数组协方差规则是"如果X是隐式可转换为引用类型Y的引用类型,则X []可隐式转换为Y []". 这是一个微妙的不同规则,因此你的情况令人困惑.

在CLI中,uint和int是赋值兼容的.但是在C#中,int和uint之间的转换是EXPLICIT,而不是IMPLICIT,这些是值类型,而不是引用类型.所以在C#中,将int []转换为uint []是不合法的.

但它在CLI中是合法的.所以现在我们面临着一个选择.

1)实现"is",这样当编译器无法静态确定答案时,它实际上会调用一个方法来检查所有C#规则以保持可转换性.这很慢,99.9%的时间与CLR规则相匹配.但我们将性能提升到100%符合C#规则.

2)实现"is",这样当编译器无法静态确定答案时,它会执行令人难以置信的快速CLR分配兼容性检查,并认为这表明uint []是一个int [],即使这会在C#中实际上并不合法.

我们选择后者.遗憾的是C#和CLI规范在这个小问题上存在分歧,但我们愿意忍受不一致.

  • @Joan:我不知道; 那是在我的时间之前.请记住,C#和CLR是在同一时间发展的,所有类型的决策都是基于关于语言和运行时规则将会是什么的不完整信息而做出的.我的_suspicion_就是这个人只是"陷入了困境",当我们意识到这一点时,为时已晚.这只是一个猜测.1999年的初始语言设计说明档案中没有出现关于此问题的任何内容. (5认同)
  • 好问题.显然我们更喜欢规范和实现是一样的.在它们不同的点上,我们更倾向于处于规范说明我们想要成为现实的情况.我们是否应该使规范指定我们不喜欢和不想要的语言功能,以便我们可以使实现保持一致?(当然,这将需要删除所有现有的编译时检查,以强制执行所需的语义.)鉴于这些选择,我们宁愿保持不一致.这是所有邪恶中最不重要的. (4认同)
  • 如果我们从来不必担心任何向后兼容性问题,那么我们可以解决问题,并以更低的费用进行改进,从而破坏变更.但这与事实相反.事实是,突破性变化会大幅增加升级成本.我们力求降低您的升级成本,以鼓励升级到更好的产品.有时,采取突破性改变的好处是值得的,但你不能仅仅希望降低成本. (4认同)
  • 嗨Eric,出于好奇,你们刚刚决定接受这种不一致还是以前没有预见过?就是想. (2认同)

Ben*_*man 9

通过Reflector运行片段:

sbyte[] foo = new sbyte[10];
object bar = foo;
Console.WriteLine("{0} {1} {2} {3}", new object[] { foo != null, false, bar is sbyte[], bar is byte[] });
Run Code Online (Sandbox Code Playgroud)

C#编译器正在优化前两个比较(foo is sbyte[]foo is byte[]).正如您所看到的,它们已经过优化,foo != null并且总是如此false.


Ben*_*n M 5

也很有趣:

    sbyte[] foo = new sbyte[] { -1 };
    var x = foo as byte[];    // doesn't compile
    object bar = foo;
    var f = bar as byte[];    // succeeds
    var g = f[0];             // g = 255
Run Code Online (Sandbox Code Playgroud)

  • 是的 - 现在你已经阅读了我的答案,你可以推断出这里发生了同样的事情.对于第一个,我们在编译时知道这违反了C#的规则.第二个,我们不知道.因此,我们必须(1)发出一个方法来实现C#语言的所有规则来进行转换,或者(2)使用CLR强制转换规则,这些规则与C#的规则略有不同,只占很小比例的奇怪案例.我们选择了(2). (2认同)