Как использовать .NET отражение для проверки обнуляемого ссылочного типа

15

C # 8.0 вводит обнуляемые ссылочные типы. Вот простой класс с обнуляемым свойством:

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

Есть ли способ проверить свойство класса использует обнуляемый ссылочный тип через отражение?

shadeglare
источник
компилируя и просматривая IL, похоже, что это добавляет [NullableContext(2), Nullable((byte) 0)]к типу ( Foo) - вот что нужно проверять, но мне нужно больше копать, чтобы понять правила, как это интерпретировать!
Марк Гравелл
4
Да, но это не тривиально. К счастью, это документально .
Йерун Мостерт
Ах я вижу; поэтому string? Xне получает атрибуты, и string Yполучает [Nullable((byte)2)]с [NullableContext(2)]на аксессорах
Марк Gravell
1
Если тип содержит только обнуляемые (или не обнуляемые), то это все представлено NullableContext. Если есть смесь, то Nullableиспользуется также. NullableContextэто оптимизация, чтобы попытаться избежать Nullableповсеместного излучения .
canton7

Ответы:

11

Похоже, это работает, по крайней мере, для типов, с которыми я тестировал

Вам нужно передать PropertyInfoинтересующее вас свойство, а также то, для Typeчего это свойство определено ( не производный или родительский тип - это должен быть точный тип):

public static bool IsNullable(Type enclosingType, PropertyInfo property)
{
    if (!enclosingType.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly).Contains(property))
        throw new ArgumentException("enclosingType must be the type which defines property");

    var nullable = property.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;
        }
    }

    var context = enclosingType.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означает «забывчивый».

canton7
источник
Это действительно сложно. Я только что нашел вариант использования, который не охватывается этим кодом. открытый интерфейс IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Если я вызываю метод IBusinessRelationсо свойством Property, Nameя получаю false.
gsharp
@gsharp Ах, я не пробовал это с интерфейсами или каким-либо другим наследованием. Я предполагаю, что это сравнительно легко исправить (посмотрите на атрибуты контекста из базовых интерфейсов): я постараюсь исправить это позже
canton7
1
нет, важная персона Я просто хотел упомянуть об этом. Это
занудство делает
1
@gsharp Глядя на это, вам нужно передать тип интерфейса, который определяет свойство - то есть ICommon, нет IBusinessRelation. Каждый интерфейс определяет свой собственный NullableContext. Я уточнил свой ответ и добавил для этого проверку во время выполнения.
canton7