多维数组、可为 null 的引用类型和类型转换

Tim*_*imo 9 c# jagged-arrays multidimensional-array nullable-reference-types

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

T x = ...;
T? y = x;
Run Code Online (Sandbox Code Playgroud)

但是,我无法理解多维和锯齿状数组的转换规则。

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
Run Code Online (Sandbox Code Playgroud)

最后一个例子给了我

错误 CS0029:无法将类型 'string[ , ][]' 隐式转换为 'string?[ , ]?[]'

既然它适用于锯齿状数组(请参阅 参考资料b),为什么它不适用于多维数组呢?

R# 还告诉我

'c' 可以声明为不可空

string?[]?[]? c = new string[1][];
//          ^ redundant
Run Code Online (Sandbox Code Playgroud)

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

dbc*_*dbc 6

这里的问题似乎是复杂数组类型声明是从左到右从外到内还是从内到外读取的规范不一致。然而,考虑到这些不一致,编译器和运行时似乎确实按指定运行。

让我们回顾一下规格:

  1. 不带可为空注释的数组类型声明将使用左侧的元素类型来读取,但是使用等级声明时将从外到内读取。

    如需确认,请参阅C# 7.0 规范 17.2.1 数组

    数组类型被写为non_array_type,后跟一个或多个rank_specifiers。

    ...在最终的非数组元素类型之前从左到右读取rank_specifiers。

    示例: 中的类型T[][,,][,]是 的二维数组的三维数组的单维数组int

    以及8.2.1概述中的语法规范

    reference_type
        : class_type
        | interface_type
        | array_type
        | delegate_type
        | 'dynamic'
        ;
    
    class_type
        : type_name
        | 'object'
        | 'string'
        ;
    
    interface_type
        : type_name
        ;
    
    array_type
        : non_array_type rank_specifier+
        ;
    
    non_array_type
        : value_type
        | class_type
        | interface_type
        | delegate_type
        | 'dynamic'
        | type_parameter
        | pointer_type      // unsafe code support
        ;
    
    rank_specifier
        : '[' ','* ']'
        ;
    
    delegate_type
        : type_name
        ; 
    
    Run Code Online (Sandbox Code Playgroud)

    值得注意的是,rank_specifier 语法不包括可为空的注释

  2. 可空注释会中断数组等级声明的从外到内的读取,并引入从内到外的读取。

    可空注释的语法可以在可空引用类型规范中找到:

    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
        ; 
    
    Run Code Online (Sandbox Code Playgroud)

    nullable_reference_type 中的 non_nullable_reference_type 必须是不可为 null 的引用类型(类、接口、委托或数组)。

    值得注意的是,rank_specifier语法没有被修改。相反,仅?允许可为null 的注释在数组类型的末尾

    因此 string?[,]?[]实际上从内到外读作(string?[,]?) []- 二维字符串数组的锯齿状一维数组 - 而string? [,][]从外到内读取为一维字符串数组的锯齿状二维数组。

    要进行确认,请注意以下内容在 .NET 7 中成功编译:

    string? [,][]  d = new string[1,2][];    // Compiles (no surprise)
    string? []?[,] f = new string[1,2][];    // Also compiles (surprise!)
    Assert.That(d.GetType() == f.GetType()); // No failure
    
    Run Code Online (Sandbox Code Playgroud)

    演示小提琴 #1在这里

  3. 对于复杂的嵌套锯齿状数组类型,GetType().Name显然应该从内到外读取。

    我找不到 MSFT 对此进行记录的任何地方,但是mono 源清楚地表明,数组的类型名称是通过获取元素的类型名称并将排名规范附加到末尾来递归构造的。

    为了确认,以下代码行

    Console.WriteLine($"typeof(string? [,][]) = {typeof(string? [,][])}");
    Console.WriteLine($"typeof(string? [,][]).GetElementType().Name = {typeof(string? [,][]).GetElementType()!.Name}");
    
    Run Code Online (Sandbox Code Playgroud)

    输出

    string? [,][]  d = new string[1,2][];    // Compiles (no surprise)
    string? []?[,] f = new string[1,2][];    // Also compiles (surprise!)
    Assert.That(d.GetType() == f.GetType()); // No failure
    
    Run Code Online (Sandbox Code Playgroud)

现在,如果您发现令人反感且难以理解,您需要写一些类似的内容

string? []?[,] f = new string[1,2][];
Run Code Online (Sandbox Code Playgroud)

您可以考虑引入扩展方法来隐藏不一致之处,例如:

public static partial class ArrayExtensions
{
    public static T[]?[,] ToArrayOfNullable<T>(this T [,][] a) => a;
    public static T[,]?[] ToArrayOfNullable<T>(this T [][,] a) => a;
}
Run Code Online (Sandbox Code Playgroud)

这些方法应该是通用的,但是对于您在代码中使用的要注入可为空注释的每个独特的锯齿状和多维数组排名模式,您将需要多种方法。完成后,您将能够编写:

var nullableArray = (new string?[1,2][]).ToArrayOfNullable();
nullableArray[0, 0] = null; // No warning
nullableArray[0, 0] = new string? [] { null }; // No warning
Run Code Online (Sandbox Code Playgroud)

您的代码将看起来干净(在扩展方法之外)。

笔记:

  • 正如评论中所述,Guru Stron已提交Nullable 引用类型和多维数组数组 -关于此错误 CS0029 #50967,当前标记为Backlog。如果不一致的问题得到解决,ArrayExtensions.ToArrayOfNullable()则需要更正或退出,因为它将不再编译。

演示 #2在这里