如何使用 .NET 反射检查可为空的引用类型

How to use .NET reflection to check for nullable reference type

C# 8.0 引入了可为空的引用类型。这是一个简单的 class 和一个可为 null 的 属性:

public class Foo
{
    public String? Bar { get; set; }
}

有没有办法检查 class 属性 通过反射使用可空引用类型?

在 .NET 6 中,添加了 API 来处理此问题,请参阅

在此之前,您需要自己阅读属性。这似乎有效,至少在我测试过的类型上是这样。

public static bool IsNullable(PropertyInfo property) =>
    IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes);

public static bool IsNullable(FieldInfo field) =>
    IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes);

public static bool IsNullable(ParameterInfo parameter) =>
    IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes);

private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable<CustomAttributeData> customAttributes)
{
    if (memberType.IsValueType)
        return Nullable.GetUnderlyingType(memberType) != null;

    var nullable = customAttributes
        .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
    if (nullable != null && nullable.ConstructorArguments.Count == 1)
    {
        var attributeArgument = nullable.ConstructorArguments[0];
        if (attributeArgument.ArgumentType == typeof(byte[]))
        {
            var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value!;
            if (args.Count > 0 && args[0].ArgumentType == typeof(byte))
            {
                return (byte)args[0].Value! == 2;
            }
        }
        else if (attributeArgument.ArgumentType == typeof(byte))
        {
            return (byte)attributeArgument.Value! == 2;
        }
    }

    for (var type = declaringType; type != null; type = type.DeclaringType)
    {
        var context = type.CustomAttributes
            .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
        if (context != null &&
            context.ConstructorArguments.Count == 1 &&
            context.ConstructorArguments[0].ArgumentType == typeof(byte))
        {
            return (byte)context.ConstructorArguments[0].Value! == 2;
        }
    }

    // Couldn't find a suitable attribute
    return false;
}

详情见this document

一般要点是 属性 本身可以有一个 [Nullable] 属性,或者如果没有,封闭类型可能有 [NullableContext] 属性。我们首先寻找 [Nullable],然后如果我们没有找到它,我们在封闭类型上寻找 [NullableContext]

编译器可能会将属性嵌入到程序集中,并且由于我们可能正在查看来自不同程序集的类型,因此我们需要进行仅反射加载。

如果 属性 是通用的,

[Nullable] 可能会用数组实例化。在这种情况下,第一个元素代表实际的 属性(其他元素代表通用参数)。 [NullableContext] 总是用一个字节实例化。

2 的值表示“可为空”。 1 表示“不可为空”,0 表示“不经意”。

我编写了一个库来进行 NRT 类型的反射 - 在内部它查看生成的属性并为您提供一个简单的 API:

https://github.com/RicoSuter/Namotion.Reflection

@rico-suter 的回答很棒!

以下内容适用于那些在真正的 McCoy 可用之前只需要快速剪切和粘贴解决方案的人(请参阅提案 https://github.com/dotnet/runtime/issues/29723)。

我根据上面@canton7 的 post 加上对 @rico-suter 代码中的想法的简要了解将其组合在一起。 @canton7 代码的变化只是抽象了属性源列表并包括了一些新的。

    private static bool IsAttributedAsNonNullable(this PropertyInfo propertyInfo)
    {
        return IsAttributedAsNonNullable(
            new dynamic?[] { propertyInfo },
            new dynamic?[] { propertyInfo.DeclaringType, propertyInfo.DeclaringType?.DeclaringType, propertyInfo.DeclaringType?.GetTypeInfo() }
        );
    }

    private static bool IsAttributedAsNonNullable(this ParameterInfo parameterInfo)
    {
        return IsAttributedAsNonNullable(
            new dynamic?[] { parameterInfo },
            new dynamic?[] { parameterInfo.Member, parameterInfo.Member.DeclaringType, parameterInfo.Member.DeclaringType?.DeclaringType, parameterInfo.Member.DeclaringType?.GetTypeInfo()
        );
    }

    private static bool IsAttributedAsNonNullable( dynamic?[] nullableAttributeSources, dynamic?[] nullableContextAttributeSources)
    {
        foreach (dynamic? nullableAttributeSource in nullableAttributeSources) {
            if (nullableAttributeSource == null) { continue; }
            CustomAttributeData? nullableAttribute = ((IEnumerable<CustomAttributeData>)nullableAttributeSource.CustomAttributes).FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
            if (nullableAttribute != null && nullableAttribute.ConstructorArguments.Count == 1) {
                CustomAttributeTypedArgument attributeArgument = nullableAttribute.ConstructorArguments[0];
                if (attributeArgument.ArgumentType == typeof(byte[])) {
                    var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)(attributeArgument.Value ?? throw new NullReferenceException("Logic error!"));
                    if (args.Count > 0 && args[0].ArgumentType == typeof(byte)) {
                        byte value = (byte)(args[0].Value ?? throw new NullabilityLogicException());
                        return value == 1; // 0 = oblivious, 1 = nonnullable, 2 = nullable
                    }
                } else if (attributeArgument.ArgumentType == typeof(byte)) {
                    byte value = (byte)(attributeArgument.Value ?? throw new NullReferenceException("Logic error!"));
                    return value == 1;  // 0 = oblivious, 1 = nonnullable, 2 = nullable
                } else {
                    throw new InvalidOperationException($"Unrecognized argument type for NullableAttribute.");
                }
            }
        }
        foreach (dynamic? nullableContextAttributeSource in nullableContextAttributeSources) {
            if (nullableContextAttributeSource == null) { continue; }
            CustomAttributeData? nullableContextAttribute = ((IEnumerable<CustomAttributeData>)nullableContextAttributeSource.CustomAttributes).FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
            if (nullableContextAttribute != null && nullableContextAttribute.ConstructorArguments.Count == 1) {
                CustomAttributeTypedArgument attributeArgument = nullableContextAttribute.ConstructorArguments[0];
                if (attributeArgument.ArgumentType == typeof(byte)) {
                    byte value = (byte)(nullableContextAttribute.ConstructorArguments[0].Value ?? throw new NullabilityLogicException());
                    return value == 1;
                } else {
                    throw new InvalidOperationException($"Unrecognized argument type for NullableContextAttribute.");
                }
            }
        }
        return false;
    }

.NET 6 Preview 7 添加了反射 API 以获取可空性信息。

Libraries: Reflection APIs for nullability information

显然,这只会帮助那些以 .NET 6+ 为目标的人。

Getting top-level nullability information

Imagine you’re implementing a serializer. Using these new APIs the serializer can check whether a given property can be set to null:

private NullabilityInfoContext _nullabilityContext = new NullabilityInfoContext();

private void DeserializePropertyValue(PropertyInfo p, object instance, object? value)
{
    if (value is null)
    {
        var nullabilityInfo = _nullabilityContext.Create(p);
        if (nullabilityInfo.WriteState is not NullabilityState.Nullable)
        {
            throw new MySerializerException($"Property '{p.GetType().Name}.{p.Name}'' cannot be set to null.");
        }
    }

    p.SetValue(instance, value);
}