69

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

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

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

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

现在,需要注意的是,当您int像以下两种语法一样声明数组时,它们没有显式声明 type int,只是区分 on 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# 编译器在这种情况下的行为方式不同?

4

6 回答 6

57

编译的版本使用数组初始化器来初始化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 };

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

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

于 2013-05-09T08:23:49.350 回答
27

宣言

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 }使用集合初始化器,在这种情况下,集合的类型是定义类型的位置,因此每个元素将改为转换为集合预期的类型。

于 2013-05-09T08:14:31.337 回答
10

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

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

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

于 2013-05-09T08:09:46.627 回答
2

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

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

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

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

于 2013-05-09T08:40:23.420 回答
1

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

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

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

于 2013-05-09T08:13:42.100 回答
1
object[] listInt = new int[] {0, 1};

是简写

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

这不起作用,因为int[]不与object[].

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

于 2013-05-09T08:44:09.073 回答