多维数组、可空引用类型和类型转换

9

使用 C# 8 的可空引用类型,我们可以编写以下代码(适用于引用类型):

T x = ...;
T? y = x;

然而,我在理解多维数组和锯齿数组转换规则方面遇到了困难。

string[][] a = new string[1][];
string?[]?[] b = new string[1][];
string?[]?[]? c = new string[1][];
string?[,][] d = new string[1,2][];
string?[,][]? e = new string[1,2][];
string?[,]?[] f = new string[1,2][];   // compiler error

最后一个例子给了我

错误代码 CS0029:无法将类型“string[,] []”隐式转换为“ string?[,]?[]”

考虑到它适用于锯齿数组(请参见b),为什么它对多维数组不起作用呢?

此外,R#告诉我

'c'可以声明为非空

string?[]?[]? c = new string[1][];
//          ^ redundant

我明白编译器可以证明c本身不为空,但我困惑的是第三个?是多余的。考虑到锯齿数组是从左到右创建的(即new int[2][]而不是new int[][2]),我期望第二个?是多余的而不是第三个。


我没有收到“冗余”的消息(VS 2019)。 - Dennis_E
1
说实话,对我来说看起来像是编译器出了bug,因为可空引用类型实际上并不是类型,只是静态代码分析的元数据。 - Guru Stron
在SharpLab上也没有收到警告(但是有错误)。 - pinkfloydx33
2
github 上创建了问题。 - Guru Stron
@JeppeStigNielsen - GuruStron已经在GitHub上创建了一个问题(https://github.com/dotnet/roslyn/issues/50967),您想要什么答案? - dbc
显示剩余2条评论
1个回答

6
这里的问题似乎是在复杂数组类型声明中存在“规范不一致性”,即无法确定是从左到右作为外部到内部读取,还是从内到外读取。然而,鉴于这些不一致性,编译器和运行时似乎正在按照规定的方式运行。
让我们来审查一下规范:
数组类型声明如果没有可空注释,应该从左边的元素类型开始读取,然后按外到内的顺序读取等级声明。
参见 C# 7.0 Specification 17.2.1 Arrays
“数组类型写作非数组类型后跟一个或多个等级说明符......在最终的非数组元素类型之前读取等级说明符。”
例如: T[][,,][,] 中的类型是一个一维数组、三维数组、二维数组和 int 的单一维度数组。
以及 8.2.1 General 中的语法规范
值得注意的是,等级说明符语法不包括可空注释
可空注释打断了数组等级声明的外部到内部读取,并引入了内向外的读取。
可以在 Nullable Reference Types Specification 中找到可空注释的语法:
type : value_type | reference_type | nullable_type_parameter | type_parameter | type_unsafe ;
reference_type : ... | nullable_reference_type ;
nullable_reference_type : non_nullable_reference_type '?' ;
non_nullable_reference_type : reference_type ;
nullable_type_parameter : non_nullable_non_value_type_parameter '?' ;
non_nullable_non_value_type_parameter : type_parameter ;
在 nullable_reference_type 中的 non_nullable_reference_type 必须是一个非空引用类型(类、接口、委托或数组)。
需要注意的是,rank_specifier 语法未被修改。相反,可空注释 ? 只允许在数组类型的结尾
因此,string?[,]?[] 实际上按内到外读取,作为字符串的多个一维数组的二维数组,而string? [,][] 按外到内读取,作为多个一维数组的二维数组的交错二维数组。
要确认,请注意以下 .NET 7 中的编译成功代码:
string? [,][] d = new string[1,2][]; // 编译(毫无疑问) string? []?[,] f = new string[1,2][]; // 编译(惊喜!) Assert.That(d.GetType() == f.GetType()); // 不会失败
演示实例 here
对于复杂嵌套的交错数组类型,GetType().Name 显然应该按内到外读取。
我找不到任何地方有 MSFT 记录这一点,但 mono source 明确地展示了数组类型名称通过递归构建获取元素类型的类型名称,并将等级规范添加到末尾。
要确认,请看下面的代码行
Console.WriteLine($"typeof(string? [,][]) = {typeof(string? [,][])}"); Console.WriteLine($"typeof(string? [,][]).GetElementType().Name = {typeof(string? [,][]).GetElementType()!.Name}");
输出:
typeof(string? [,][]) = System.String[][,] typeof(string? [,][]).GetElementType().Name = String[]

现在,如果你觉得需要写类似下面这样的代码很不可接受或者难以理解:

string? []?[,] f = new string[1,2][];

您可以考虑引入扩展方法来隐藏不一致性,例如:
public static partial class ArrayExtensions
{
    public static T[]?[,] ToArrayOfNullable<T>(this T [,][] a) => a;
    public static T[,]?[] ToArrayOfNullable<T>(this T [][,] a) => a;
}

方法应该是通用的,但是对于你在代码中使用的不同的锯齿形和多维数组等级模式,你需要多个方法来注入可空注释。完成后,您将能够编写以下内容:
var nullableArray = (new string?[1,2][]).ToArrayOfNullable();
nullableArray[0, 0] = null; // No warning
nullableArray[0, 0] = new string? [] { null }; // No warning

而且你的代码看起来会很干净(在扩展方法之外)。

注:

演示#2 在这里


1
太神奇了,我没想到这个问题的根源是奇怪的语法。感谢您提供详细的答案。 - Timo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接