首页 > 解决方案 > 发现泛型参数的 NullabilityInfo?

问题描述

我已成功阅读并使用 NullableAttribute/NullableContextAttribute(基于https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md和太多的试验和错误)用于属性、字段和方法参数。

但是,我未能分析泛型参数的可空性。我的目标是获取 IA 类型参数(元组)的可空性信息:

interface ICommand<T> { }

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

但是,Nullable 和 NullableContext 属性显然是不够的。下面的代码将这些属性转储到类型树上:

        [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;
        }

这给了我这个描述(至少对我而言)对于(string,List<string?>?)(我已经在 // 之后的行注释)的实际可空性没有任何意义。

    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

我应该在哪里寻找这个?我错过了什么?

标签: c#reflectionnullable-reference-types

解决方案


属性仅供编译器使用Nullable这意味着要使用这些属性,至少以任何有意义的方式是如果您使用 roslyn 源代码进行编程(又名。对编译器本身进行编程)。NullableContext

如果您没有对编译器进行编程

请记住,可空性几乎是严格意义上的编译器时间概念。这意味着它基本上只是存在,因此编译器可以通过启发式和其他魔法来确定正在以一种可以在运行时阻止的方式使用类型NullReferenceExceptions,以及可读性等其他好处。

这并不是说我们无法确定一个类型是否Nullable<>在运行时。为此,我们必须记住一些关于如何Nullable<>工作的规则。

可空类型struct具有底层不可空类型(Nullable<>.Value)的。a 的基础类型Nullable<T> 不能是可为空的类型或引用类型。

因此,在运行时,要确定任何给定类型是否Nullable<>存在,我们只需检查是否可以提取底层类型。

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;
}

这意味着不能将类包装在Nullable<>结构中。这可能看起来很违反直觉,因为没有什么可以阻止您CustomClass? variable = GetPossiblyNullCustomClass();在源代码中使用。

它仍然允许您这样做的原因有几个原因,但主要是一个原因,因此编译器可以有效地跟踪源代码中对象的路径并通知您可能发生的空引用异常(例如,如果您在许多其他类型的启发式检查中忘记初始化变量)。在完成这些编译时检查并编译源代码后,任何内容CustomClass?都将被忽略,CustomClass而是被使用。

我确信有一些非常酷的 hacky 方法可以找到它,但是在运行时通过反射我不相信有一种方法可以确定某些class是否是class?.

如果你正在编程 Rosyln

确保您可以System.Runtime.CompilerServices在您的范围内访问,您可以通过导入 roslyn 编译器的源代码并在项目中添加对它的引用来做到这一点。要访问可空性标志,只需通过反射直接在属性上访问它,这是一个简短的示例,说明您可以如何做到这一点:

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.");
}

推荐阅读