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

Posted

技术标签:

【中文标题】为啥我的 C# 数组在转换为对象时会丢失类型符号信息?【英文标题】:Why does my C# array lose type sign information when cast to object?为什么我的 C# 数组在转换为对象时会丢失类型符号信息? 【发布时间】:2010-11-13 19:39:30 【问题描述】:

调查一个错误,我发现这是由于 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 既是byte[] 又是sbyte[]?对于Int32[]UInt32[] 等其他有符号/无符号类型也会发生同样的情况,但对于Int32[]Int64[] 则不然。

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

【问题讨论】:

【参考方案1】:

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

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

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


您偶然发现了 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-covariance/

这不是 C# 的规则。 C# 的数组协方差规则是“如果 X 是隐式可转换为引用类型 Y 的引用类型,则 X[] 可隐式转换为 Y[]”。 这是一条略有不同的规则,因此您的处境令人困惑。

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

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

    实现“is”,以便当编译器无法静态确定答案时,它实际上会调用一个方法来检查所有 C# 规则的身份保留转换性。这很慢,而且 99.9% 的时间都符合 CLR 规则。但是我们会在性能上受到影响,以便 100% 符合 C# 的规则。

    实现“is”,以便当编译器无法静态确定答案时,它会执行令人难以置信的快速 CLR 赋值兼容性检查,并接受这样一个事实,即 uint[] 是 int[],尽管在 C# 中实际上是不合法的。

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

【讨论】:

嗨,Eric,出于好奇,你们是刚刚决定接受这种不一致,还是之前没有预见到?只是想知道。 @Joan:我不知道;那是在我的时代之前。请记住,C# 和 CLR 是同时发展的,并且各种决策都是在关于语言和运行时规则的不完整信息的基础上做出的。我的怀疑是这个只是“掉进了裂缝”,当我们意识到这一点时,为时已晚。这只是一个猜测。 1999 年的初始语言设计说明档案中没有关于此问题的任何内容。 好问题。显然,我们更喜欢规范和实现是相同的。在它们不同的地方,我们更愿意处于规范说明我们希望成为真实的情况。我们是否应该让规范指定我们不喜欢和不想要的语言特性,以便我们可以使实现保持一致? (当然,这将需要删除所有现有的强制执行所需语义的编译时检查。)鉴于这些选择,我们宁愿保持不一致。这是所有罪恶中最小的。 还有 Eric,当这些问题卡在 .NET 平台上时,您会不会感到困扰?我问是因为我总是想知道向后兼容性与不断改进的架构。如果允许您忽略向后兼容性,您是否认为整个 .NET 平台会无限优越?当我们在每次迭代中将我们的代码集成到第三方系统上时,我们必须做很多工作来“升级”。如果有人想从 .NET 2.0 跳到 3.5,为什么不应该这样呢?谢谢。 如果我们永远不必担心任何向后兼容性问题,那么可以,我们可以解决问题并进行改进,从而以更少的费用进行破坏性更改。但这与事实相反。事实是,重大更改会大大增加升级成本。我们力求降低您的升级成本,鼓励升级到更好的产品。有时,做出重大改变的好处值得付出代价,但你不能只希望付出代价。【参考方案2】:

通过反射器运行 ​​sn-p:

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

【讨论】:

【参考方案3】:

也很有趣:

    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

【讨论】:

我在这里遗漏了一些东西。这不是你所期望的。有什么奇怪的? 不是 g = 255,这是预期的,而是 bar as byte[] 不返回 null。 对——既然你已经阅读了我的回答,你可以推断出同样的事情正在这里发生。对于第一个,我们在编译时就知道这违反了 C# 的规则。对于第二个,我们不知道。因此,我们必须要么 (1) 发出一个实现 C# 语言的所有规则的方法来进行强制转换,要么 (2) 使用 CLR 强制转换规则,这与 C# 的规则略有不同,适用于一小部分奇怪的情况。我们选择了 (2)。【参考方案4】:

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

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

【讨论】:

但由于bar 属于object 类型,是任何其他类型的基本类型,这意味着您可以说bar is <any given type>,它会返回true,但事实并非如此.为什么你可以将 sbyte[] 强制转换为 byte[] 只是因为它恰好通过了引用类型?

以上是关于为啥我的 C# 数组在转换为对象时会丢失类型符号信息?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中将对象数组向上转换为另一种类型的对象数组?

为啥空数组类型转换为零? +[]

为啥我在使用 POST 控制器时会丢失所有数据?

为啥立体声 mp3 文件在使用 ffmpeg 从 mp4 转换时会丢失一个通道?

matlab 为啥要进行数据类型转换?

为啥扩展语法会将我的字符串转换为数组?