【问题标题】:Why C# behaves differently on two int array syntaxes为什么 C# 在两种 int 数组语法上的行为不同
【发布时间】:2013-05-09 08:00:08
【问题描述】:

C# 中的数组在引用类型上是隐式协变的

object[] listString = new string[] { "string1", "string2" };

但不是值类型,所以如果你把string改成int,你会得到编译错误:

object[] listInt = new int[] {0, 1}; // compile error

现在,问题是当你像下面两种语法声明int数组时,它们没有显式声明类型int,只是区分new[],编译器会区别对待:

object[] list1 = { 0, 1 };       //compile successfully
object[] list2 = new[] {0, 1};    //compile error

你会得到object[] list1 = { 0, 1 };编译成功,但是object[] list2= new[] {0, 1};编译错误。

似乎 C# 编译器会处理

object[] list1 = { 0, 1 };

作为

object[] list1 = new object[]{ 0, 1 };

但是

object[] list2 = new[] { 0, 1 };

作为

object[] list2 = new int[]{ 0, 1 };  //error because of co-variant

为什么 C# 编译器在这种情况下会有不同的行为方式?

【问题讨论】:

  • +1,你来自this question
  • @Habib:是的,灵感来自它
  • 快速猜测,所有引用类型都采用相同的字节数作为引用,因此很容易隐式转换。
  • 老实说,我以前从未见过new[] { ... } 语法。你确定这在 C# 规范中是正确的吗?
  • @ja72:它叫做implicitly typed array。你可以在VS中试试这个

标签: c# arrays


【解决方案1】:

编译的版本使用一个数组初始化器来初始化list1。 C# 语言规范第 1.110 节(“数组初始值设定项”)规定:

数组初始值设定项由一系列变量初始值设定项组成, 用“{”和“}”标记括起来,用“,”标记分隔。每个 变量初始化器是一个表达式,或者,如果是 多维数组,嵌套数组初始化器。

上下文 使用哪个数组初始值设定项确定数组的类型 正在初始化。在数组创建表达式中,数组类型 紧接在初始化器之前,或者从 数组初始值设定项中的表达式。在字段或变量中 声明,数组类型是字段或变量的类型 声明。

当在字段或变量中使用数组初始值设定项时 声明,如:

int[] a = {0, 2, 4, 6, 8};

它只是等效数组创建表达式的简写:

int[] a = new int[] {0, 2, 4, 6, 8};

所以很明显这应该编译。

第二个版本使用显式数组创建表达式,您可以在其中具体指示编译器创建什么类型的数组。 §1.51.10.4(“数组创建表达式”)状态:

第三种形式的数组创建表达式称为 隐式类型数组创建表达式。它类似于 第二种形式,只是数组的元素类型不是 明确给出,但确定为最佳通用类型(§1.50.2.14) 数组初始值设定项中的一组表达式。

因此,第二个版本相当于

object[] list2 = new int[] { 0, 1 };

所以问题现在实际上变成了“为什么我不能将int[] 分配给object[]”,正如您在问题末尾提到的那样。答案也很简单,见第 1.109 节(“数组协方差”):

数组协方差特别不扩展到数组 值类型。例如,不存在允许int[] 的转换 被视为object[]

【讨论】:

    【解决方案2】:

    声明

    object[] listInt = new int[] {0, 1};
    

    无效,因为值类型不允许协变数组转换(并且int 是值类型)。或者,声明

    object[] listInt = new string[] {"0", "1"};
    

    是有效的,因为引用类型允许协变数组转换。这是因为x = (object)myString的赋值只涉及简单的赋值,而y = (object)myInt需要装箱操作。

    现在谈谈这两个声明之间的区别。在声明object[] list2 = new[] { 0, 1 } 中,由于类型推断的工作原理,它首先查看右侧表达式并得出结论new[] { 0, 1 } 应被视为new int[] { 0, 1 }。然后它尝试将此 int 数组分配给对象数组,由于值类型的协变转换问题而给出错误。不过,声明 object[] list1 = { 0, 1 } 使用集合初始化器,在这种情况下,集合的类型就是定义类型的位置,因此每个元素将改为转换为集合预期的类型。

    【讨论】:

      【解决方案3】:

      当您使用{} 时,您使用集合初始化器(请参阅:http://msdn.microsoft.com/en-us/library/vstudio/bb384062.aspx)。这些括号之间的值必须放在某个地方。因此,必须创建一个集合。编译器将分析上下文以找出什么样的集合。

      如果是第一个:object[] list1 = { 0, 1 };,显然应该创建一个集合。但它应该是什么样的?某处没有new 操作。只有一个提示:list1 的类型为 object[]。因此编译器创建该集合并用值填充它。

      在您的第二个示例object[] list1 = new[] { 0, 1 }; 中还有另一个提示:new[]。这个提示明确表示:将会有一个数组。该数组没有类型,因此它将尝试通过分析值来查找数组的类型。这些都是int,因此它将创建一个int 的数组并填充它。另一个提示object[] 完全被忽略了,因为创建的提示比它应该分配到的提示重要得多。现在编译器想要将此数组分配给 list1 和 BOOM:这不合适!

      【讨论】:

        【解决方案4】:

        object[] list1 = { 0, 1 }; 语句可以编译,因为编译器足够聪明,知道您正在尝试将数值类型数组转换为引用类型数组,因此它将 Int32 元素装箱为引用类型。

        您也可以显式地装箱原始类型:

        object[] list2 = Array.ConvertAll<int, Object>(new[] { 0, 1 }, input => (Object)input);

        当您将“int[]”或“Int32[]”指定为数组类型时,编译器不会为您隐式进行装箱,但似乎可以将其添加到 C# 中。

        【讨论】:

          【解决方案5】:

          数组初始化器是一种方便的编译器。如果我说“我正在声明一个对象数组并为其分配一个值”,那么编译器可以合理地假设您的 { 0, 1 } 是一个对象数组并将其解释为对象数组。尽管语法似乎是一个赋值,但它不是:您使用的是初始化程序。这个语法的简写是object[] list1 = new object[] { 0, 1 }

          当您说new[] { 0, 1 } 时,这是一个创建数组并对其进行初始化的表达式。该表达式的评估与您分配给它的内容无关 - 由于编译器检测到隐式整数类型,它会创建一个int[]。该表达式的简写版本是object[] list2 = new int[] { 0, 1 }

          如果您比较这两个语句的简写版本,很明显可以看出它们的不同之处。

          【讨论】:

            【解决方案6】:
            object[] listInt = new int[] {0, 1};
            

            是简写

            object[] listInt;
            listInt = new int[] {0, 1};
            

            这不起作用,因为int[] is not covariant with object[]

            当你说new[]时,它等同于new int[],因此同样适用。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-07-21
              • 2019-05-31
              • 2016-12-20
              • 2019-09-23
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多