如何使用 .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:
@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);
}
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:
@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); }