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

76

C# 8.0 引入了可空引用类型。以下是一个带有可空属性的简单类:

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

有没有一种方法可以通过反射来检查类属性是否使用了可空引用类型?


编译并查看IL后,似乎会向类型(Foo)添加[NullableContext(2), Nullable((byte) 0)]。因此需要检查这个,但我需要深入了解如何解释这些规则! - Marc Gravell
7
可以,但这并不是一件简单的事情。幸运的是,它已经有文档记录了。 - Jeroen Mostert
啊,我明白了;所以 string? X 没有任何属性,而 string Y 在访问器上带有 [NullableContext(2)][Nullable((byte)2)] - Marc Gravell
3
如果一个类型仅包含可空类型(或非可空类型),那么这些都由NullableContext表示。如果有混合,则还要使用NullableNullableContext是一种优化,试图避免在各个地方都发出Nullable - canton7
6个回答

60
在.NET 6中,添加了NullabilityInfoContext 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;
}

请参阅此文档以获取详细信息。
总体意思是,属性本身可以具有[Nullable]属性,如果没有,则封闭类型可能具有[NullableContext]属性。我们首先查找[Nullable],然后如果没有找到,我们查找封闭类型上的[NullableContext]
编译器可能会将这些属性嵌入程序集中,因为我们可能正在查看来自不同程序集的类型,所以我们需要进行反射加载。 [Nullable]可能会与数组一起实例化,如果该属性是泛型的。在这种情况下,第一个元素表示实际属性(其他元素表示泛型参数)。[NullableContext]始终使用单个字节进行实例化。
2表示“可空”。1表示“不可空”,0表示“未知”。

1
没关系,我只是想提一下。这个可空类型的东西让我疯了;-) - gsharp
1
@gsharp 看起来,你需要传递定义属性的接口类型 -- 也就是 ICommon 而不是 IBusinessRelation。每个接口都定义了自己的 NullableContext。我已经澄清了我的答案,并添加了一个运行时检查。 - canton7
1
是的,非常感谢,我们聊天时我才开始意识到这一点,然后解决方案就很简单了。我的代码首先检查基础类型是否不是“普通”的可空类型,然后执行此检查以查看它是否为可空引用类型。因此,在这里,如果 PropertyType.IsValueType,则只需返回默认值 false。 - Arwin
1
非常简单和直接。 :) - MgSam
4
我已经将所有内容封装到一个库中,详见 https://github.com/RicoSuter/Namotion.Reflection - Rico Suter
显示剩余15条评论

35

.NET 6 Preview 7增加了反射API以获取空值信息。

库:反射API用于空值信息

显然,这只对目标为.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);
}

1
这仍然是一个好的了解,会影响我是否要费心编写代码来完成这个任务。谢谢! - Eric Sondergard
太棒了 - 一直在绞尽脑汁地寻找解决方案。谢谢! - undefined

13

回答晚了。

这是我最终采用的方案,感谢Bill Menees提供的帮助:

static bool IsMarkedAsNullable(PropertyInfo p)
{
    return new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable;
}

// 测试:


class Foo
{
        public int Int1 { get; set; }
        public int? Int2 { get; set; } = null;


        public string Str1 { get; set; } = "";
        public string? Str2 { get; set; } = null;

        
        public List<Foo> Lst1 { get; set; } = new();
        public List<Foo>? Lst2 { get; set; } = null;


        public Dictionary<int, object> Dic1 { get; set; } = new();
        public Dictionary<int, object>? Dic2 { get; set; } = null;
}

....

var props = typeof(Foo).GetProperties();
foreach(var prop in props)
{
    Console.WriteLine($"Prop:{prop.Name} IsNullable:{IsMarkedAsNullable(prop)}");
}


// outputs:

Prop:Int1 IsNullable:False
Prop:Int2 IsNullable:True
Prop:Str1 IsNullable:False
Prop:Str2 IsNullable:True
Prop:Lst1 IsNullable:False
Prop:Lst2 IsNullable:True
Prop:Dic1 IsNullable:False
Prop:Dic2 IsNullable:True


2
这应该是被接受的答案。简单明了。我唯一做出的更改是将NullabilityInfoContext创建为静态,以便可以重复使用。这样做有什么不利之处吗? - microsoftvamp
NullabilityInfoContext管理字典中的一些内部状态,我没有看到任何在调用Add之前进行锁定,所以我认为它不是线程安全的。 - Evil Pigeon

9

1
干得好,先生。其他答案都无法解决属性实现的复杂性,但是您的库似乎涵盖了所有情况。 - Timo
2
我正在使用这个库,但是在缓存周围有许多不正确(且低效)的锁用法,可能会在并发情况下失败。因此,我强烈建议管理从该库公开的并发类型。请参见https://twitter.com/casperOne/status/1388962185813102595以获取示例。 - casperOne
1
有趣的是,我在这个问题上的原因是因为我正在编写一个测试,以确保所有我的API模型都用适当的'RequiredAttribute'标记其属性,用于使用NSwag生成的客户端。感谢你的一切,Rico!我们喜欢你的项目。 - Eric Sondergard

0

只有string?有点棘手。其他可空类型很容易找到。对于字符串,我使用了以下方法,您需要通过反射传递一个PropertyInfo对象。

private bool IsNullable(PropertyInfo prop)
{
  return prop.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute");
}

0

这里有@rico-suter提供的一个很棒的答案!

下面是为那些只想要一个快速的剪切和粘贴解决方案直到真正的方法可用(请参见https://github.com/dotnet/runtime/issues/29723提议)。我根据@canton7上面的帖子加上对@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;
    }

谢谢提醒。我的代码现在递归检查“.DeclaringType”,并处理“ParameterInfo”。 - canton7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接