简体   繁体   中英

Discovering the NullabilityInfo of a generic parameter?

I have successfully read and used the NullableAttribute/NullableContextAttribute (based on https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md and too much trial and errors) for properties, fields and method parameters.

However, I'm failing to analyze the nullabilities of a generic parameter. My goal is to obtain the nullability information of the IA type parameter (the tuple):

interface ICommand<T> { }

interface IA : ICommand<(string,List<string?>?)> { }

However, the Nullable and NullableContext attributes are clearly not enough. The code below dumps these attributes on the type tree:

        [Test]
        public void type_in_generic_definition()
        {
            var b = new StringBuilder();
            Dump( typeof( IA ), b, Environment.NewLine );
            Dump( typeof( IA ).GetInterfaces()[0], b, Environment.NewLine );
            Console.WriteLine( b );
        }

        void Dump( Type type, StringBuilder w, string newline )
        {
            newline = newline + "  ";
            w.Append( type.Name ).Append( newline );
            var p = GetNullableProfile( type );
            w.Append( $"Nullable: {(p != null ? string.Join( null, p.Select( v => v.ToString() ) ) : "null")}" ).Append( newline );
            var c = GetNullableContextValue( type );
            w.Append( $"NullableContext: {(c != null ? c.ToString() : "null")}" ).Append( newline );
            var d = GetNullableContextValueFromDeclaringType( type );
            w.Append( $"NullableContext (DeclaringType): {(d != null ? d.ToString() : "null")}" ).Append( newline );
            if( type.IsGenericType )
            {
                foreach( var sub in type.GetGenericArguments() )
                {
                    Dump( sub, w, newline );
                }
            }
            newline = newline.Substring( 0, newline.Length - 2 );
            w.Append( newline );
        }

        byte[]? GetNullableProfile( Type t )
        {
            var a = t.CustomAttributes.FirstOrDefault( a => a.AttributeType.Name == "NullableAttribute" && a.AttributeType.Namespace == "System.Runtime.CompilerServices" );
            if( a == null ) return null;
            object? data = a.ConstructorArguments[0].Value;
            Debug.Assert( data != null );
            if( data is byte b ) return new[] { b };
            return ((IEnumerable<CustomAttributeTypedArgument>)data).Select( a => (byte)a.Value! ).ToArray();
        }

        byte? GetNullableContextValue( Type t )
        {
            var a = t.CustomAttributes.FirstOrDefault( a => a.AttributeType.Name == "NullableContextAttribute" && a.AttributeType.Namespace == "System.Runtime.CompilerServices" );
            return a == null
                    ? null
                    : (byte)a.ConstructorArguments[0].Value!;
        }

        byte? GetNullableContextValueFromDeclaringType( Type t )
        {
            var parent = t.DeclaringType;
            while( parent != null )
            {
                var found = GetNullableContextValue( parent );
                if( found.HasValue ) return found;
                parent = parent.DeclaringType;
            }
            return null;
        }

This gives me this description that doesn't make any sense (at least to me) regarding the actual nullabilities of (string,List<string?>?) (I've commented the lines after the //).

    IA
      Nullable: null
      NullableContext: null
      NullableContext (DeclaringType): 1       // I'm working in a NRT aware context where 
                                               // reference types are by default not annotated
                                               // (not null). Ok.
      
    ICommand`1
      Nullable: null
      NullableContext: 2                       // Q0: On what scope does this apply?
      NullableContext (DeclaringType): 1       // Ok. It's my context.
      ValueTuple`2                             
        Nullable: null                         // Ouch! I was expecting a "122" here :(
        NullableContext: null
        NullableContext (DeclaringType): null
        String
          Nullable: 0                           // Q1: Explicit oblivious here. Was expecting 1!
          NullableContext: 1                    // Q2: Scope? Is it the 1 I'm after?
          NullableContext (DeclaringType): null
          
        List`1
          Nullable: 0                           // The same as the string!
          NullableContext: 1                    // But the list IS nullable!
          NullableContext (DeclaringType): null
          String
            Nullable: 0                            // The inner string is nullable :(
            NullableContext: 1
            NullableContext (DeclaringType): null

Where should I look for this? What did I miss?

The Nullable and NullableContext attributes are for compiler use only , this means that in order to use these attributes, at least in in any meaningful way would be if you were programming with roslyn source(aka. programming a/the compiler itself).

If you are not programming the compiler

Remeber that nullability is almost strictly a compiler time concept. This means it basically just exists so the compiler can determine through heuristics and other magic that you are using types in a way that would prevent NullReferenceExceptions during runtime, among other benefits like readability etc.

That's not to say we cant figure out if a type is Nullable<> at runtime. For this we have to remember a couple a rules on how Nullable<> works.

Nullable Types are a struct with an underlying non-nullable type( Nullable<>.Value ). The underlying type of a Nullable<T> cannot be a nullable type or a reference type.

So at runtime, to determine if any given type is Nullable<> or not we just have to check to see if we can extract the underlying type.

byte? GetNullableContextValue(Type t)
{
    // if it's a struct, and it's a Nullable<> then we will be able to extract the underlying type
    if (Nullable.GetUnderlyingType(t) != null)
    {
        return 1;
    }

    // the type was a class, object, or struct not wrapped in a Nullable<>
    return 0;
}

What this means is that a class can not be wrapped in a Nullable<> struct. That may seem pretty counter-intuitive because there is nothing stopping you from using CustomClass? variable = GetPossiblyNullCustomClass(); CustomClass? variable = GetPossiblyNullCustomClass(); in your source code.

The reason why it will still let you do this is for a couple of reasons, but mainly one, so the compiler can effectively track the path of objects in your source code and notify you of possible null reference exceptions that may occur(say if you have forgotten to initialize a variable, among many other kinds of heuristic checks). After those compile-time checks are done and the source is compiled any CustomClass? are ignored, and CustomClass is used instead.

I'm sure there is some really cool hacky way to find it, but at runtime with reflection I don't believe there is a way to determine if some class is a class? .

If you are programming Rosyln

Make sure you have access to System.Runtime.CompilerServices within your scope, you would do this by importing the source for the roslyn compiler and adding a reference to it in your project. To access the nullability flag simply access it directly on the attribute through reflection here's a short example of how you might do that:

NullableContextAttribute? foundAttribute = type.GetCustomAttribute<NullableContextAttribute>();

switch(foundAttribute.Flag)
{
    case 0:
    case 1:
    case 2:
        // do work;
        break;
    default:
        throw new Exception("Flag out of bounds.");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM