Search code examples
c#multidimensional-arrayjagged-arraysnullable-reference-types

Multidimensional arrays, nullable reference types and type conversion


With C# 8's nullable reference types, we can write (for reference types):

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

However, I'm having trouble understanding the conversion rules for multidimensional and jagged arrays.

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

The last example gives me

error CS0029: Cannot implicitly convert type 'string[,][]' to 'string?[,]?[]'

Given that it works for jagged arrays (see b), why doesn't this work for multidimensional arrays?

Also R# tells me that

'c' can be declared non-nullable

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

I get that the compiler can proof that c itself is not null but I'm confused that the third ? is redundant. Given that jagged arrays are created from left to right (i.e. new int[2][] instead of new int[][2]), I'd expect the second ? to be redundant and not the third one.


Solution

  • The issue here seems to be specification inconsistencies in whether complex array type declarations are to be read from left to right as outer-to-inner or inner-to-outer. However the compiler and runtime do seem to be functioning as specified, given those inconsistencies.

    Let's review the specifications:

    1. Array type declarations without nullable annotations are to be read with the element type on the left, but then with rank declarations read outer-to-inner.

      For confirmation see the C# 7.0 Specification 17.2.1 Arrays:

      An array type is written as a non_array_type followed by one or more rank_specifiers.

      ... the rank_specifiers are read from left to right before the final non-array element type.

      Example: The type in T[][,,][,] is a single-dimensional array of three-dimensional arrays of two-dimensional arrays of int.

      And the grammar specification in 8.2.1 General

      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
          ; 
      

      Of note, the rank_specifier grammar does not include nullable annotations.

    2. Nullable annotations interrupt the outer-to-inner reading of array rank declarations and introduce an inner-to-outer reading.

      The grammar for nullable annotations can be found in 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
          ; 
      

      The non_nullable_reference_type in a nullable_reference_type must be a nonnullable reference type (class, interface, delegate or array).

      Of note, the rank_specifier grammar was not modified. Instead the nullable annotation ? is allowed only at the end of an array type.

      Thus string?[,]?[] is actually read inner-to-outer as (string?[,]?) [] -- a jagged 1d array of 2d arrays of strings -- whereas string? [,][] is read outer-to-inner as a jagged 2d array of 1d arrays of strings.

      To confirm, note that the following compiles successfully in .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
      

      Demo fiddle #1 here.

    3. For complex nested jagged array types, GetType().Name apparently should be read inner-to-outer.

      I cannot find anywhere that this is documented by MSFT, however the mono source clearly demonstrates that the type name of an array is constructed recursively by getting the type name of the element, and appending the rank specification to the end.

      To confirm, the following lines of code

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

      Output

      typeof(string? [,][]) = System.String[][,]
      typeof(string? [,][]).GetElementType().Name = String[]
      

    Now, if you find it objectionable and incomprehensible that you need to write something like

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

    you might consider introducing extension methods to hide the inconsistency such as:

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

    The methods should be generic, but you will need multiple methods for each unique pattern of jagged & multidimensional array ranks you use in your code into which you want to inject nullable annotations. Having done that, you will be able to write:

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

    And your code will look clean (outside the extension method).

    Notes:

    Demo #2 here.