42

调查一个错误,我发现这是由于 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[]);

输出是“True False True True”,而我预计“ bar is byte[]”会返回 False。显然 bar 既是 abyte[]又是sbyte[]? 其他有符号/无符号类型(如Int32[]vs )也会发生同样的情况UInt32[],但不适用于Int32[]vs Int64[]

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

4

4 回答 4

69

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

https://web.archive.org/web/20190203221115/https://blogs.msdn.microsoft.com/ericlippert/2009/09/24/why-is-covariance-of-value-typed-arrays-inconsistent/

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


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

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

例如,CLI 类型系统说,引用类型的子类型是与引用类型的超类型兼容的赋值。如果你有一个字符串,你可以将它存储在对象类型的变量中,因为它们都是引用类型,而字符串是对象的子类型。但事实并非如此。超类型与子类型的赋值不兼容。你不能在不先强制转换的情况下将只知道是对象的东西粘贴到字符串类型的变量中。

基本上“赋值兼容”意味着“将这些确切的位粘贴到这个变量中是有意义的”。从源值到目标变量的赋值必须是“表示保留”。有关详细信息,请参阅我的文章:

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

CLI 的规则之一是“如果 X 与 Y 的赋值兼容,则 X[] 与 Y[] 的赋值兼容”。

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

https://web.archive.org/web/20190118054040/https://blogs.msdn.microsoft.com/ericlippert/2007/10/17/covariance-and-contravariance-in-c-part-two-array-协方差/

这不是 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 规范在这个小问题上存在分歧,但我们愿意忍受这种不一致。

于 2009-07-24T17:42:22.743 回答
9

通过反射器运行代码段:

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[] });

C# 编译器正在优化前两个比较(foo is sbyte[]foo is byte[])。正如您所看到的,它们已经被优化foo != null并简单地始终false.

于 2009-07-24T17:34:39.637 回答
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
于 2009-07-24T17:37:50.473 回答
0

当然输出是正确的。bar “是” sbyte[] 和 byte[],因为它与两者兼容,因为 bar 只是一个对象,那么它“可能”是有符号或无符号的。

“is”被定义为“表达式可以转换为类型”。

于 2009-07-24T17:31:15.853 回答