Como usar a reflexão do .NET para verificar o tipo de referência anulável

15

O C # 8.0 apresenta tipos de referência que permitem valor nulo. Aqui está uma classe simples com uma propriedade anulável:

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

Existe uma maneira de verificar se uma propriedade de classe usa um tipo de referência anulável por meio de reflexão?

shadeglare
fonte
compilando e olhando para o IL, parece que isso adiciona [NullableContext(2), Nullable((byte) 0)]ao tipo ( Foo) - então é isso o que verificar, mas eu precisaria cavar mais para entender as regras de como interpretar isso!
Marc Gravell
4
Sim, mas não é trivial. Felizmente, está documentado .
Jeroen Mostert
ah entendo; então string? Xnão obtém atributos e string Yfica [Nullable((byte)2)]com [NullableContext(2)]os acessadores
Marc Gravell
11
Se um tipo contém apenas anuláveis ​​(ou não anuláveis), tudo isso é representado por NullableContext. Se houver uma mistura, Nullableuse também. NullableContexté uma otimização para tentar evitar a emissão Nullablepor todo o lugar.
canton7

Respostas:

11

Parece funcionar, pelo menos nos tipos com os quais eu testei.

Você precisa passar PropertyInfopara a propriedade em que está interessado e também para a Typequal essa propriedade está definida ( não um tipo derivado ou pai - deve ser o tipo exato):

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

Veja este documento para detalhes.

A essência geral é que a própria propriedade pode ter um [Nullable]atributo ou, se não, o tipo de fechamento pode ter um [NullableContext]atributo. Primeiro procuramos e [Nullable], se não o encontrarmos, procuramos [NullableContext]no tipo de anexo.

O compilador pode incorporar os atributos ao assembly e, como podemos observar um tipo de um assembly diferente, precisamos fazer uma carga somente de reflexão.

[Nullable]pode ser instanciado com uma matriz, se a propriedade for genérica. Nesse caso, o primeiro elemento representa a propriedade real (e outros elementos representam argumentos genéricos). [NullableContext]é sempre instanciado com um único byte.

Um valor de 2significa "anulável". 1significa "não anulável" e 0significa "inconsciente".

canton7
fonte
É realmente complicado. Acabei de encontrar um caso de uso que não é coberto por este código. interface pública IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Se eu chamar o método IBusinessRelationcom a propriedade Name, fico falso.
gsharp
@gsharp Ah, eu não tinha tentado com interfaces ou qualquer tipo de herança. Eu estou supondo que é uma correção relativamente fácil (olhada contexto atributos de interfaces base): Eu vou tentar corrigi-lo mais tarde
canton7
11
nada demais. Eu só queria mencionar isso. Este material anulável está me deixando louco ;-)
gsharp
11
@gsharp Olhando para isso, você precisa passar o tipo de interface que define a propriedade - ou seja ICommon, não IBusinessRelation. Cada interface define sua própria NullableContext. Esclarei minha resposta e adicionei uma verificação de tempo de execução para isso.
canton7