Cómo usar la reflexión .NET para verificar el tipo de referencia anulable

15

C # 8.0 introduce tipos de referencia anulables. Aquí hay una clase simple con una propiedad anulable:

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

¿Hay alguna manera de verificar que una propiedad de clase use un tipo de referencia anulable mediante reflexión?

sombra
fuente
compilando y mirando el IL, parece que esto se agrega [NullableContext(2), Nullable((byte) 0)]al tipo ( Foo), así que eso es lo que debe verificar, ¡pero necesitaría cavar más para comprender las reglas de cómo interpretar eso!
Marc Gravell
44
Sí, pero no es trivial. Afortunadamente, está documentado .
Jeroen Mostert
Ah, ya veo; así que string? Xno obtiene atributos, y se string Ypone [Nullable((byte)2)]con [NullableContext(2)]los accesos
Marc Gravell
1
Si un tipo solo contiene valores nulables (o no nulables), entonces todo eso está representado por NullableContext. Si hay una mezcla, entonces se Nullableusa también. NullableContextEs una optimización para tratar de evitar tener que emitir por Nullabletodo el lugar.
canton7

Respuestas:

11

Esto parece funcionar, al menos en los tipos con los que lo he probado.

Debe pasar la PropertyInfopropiedad que le interesa y también la Typepropiedad en la que se define esa propiedad ( no un tipo derivado o primario; debe ser el tipo exacto):

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

Vea este documento para más detalles.

La esencia general es que, o bien la propiedad en sí misma puede tener un [Nullable]atributo, o si no lo tiene, el tipo que lo encierra podría tener un [NullableContext]atributo. Primero buscamos [Nullable], luego, si no lo encontramos, buscamos [NullableContext]en el tipo adjunto.

El compilador podría incrustar los atributos en el ensamblaje, y dado que podríamos estar viendo un tipo de un ensamblaje diferente, necesitamos hacer una carga de solo reflexión.

[Nullable]podría instanciarse con una matriz, si la propiedad es genérica. En este caso, el primer elemento representa la propiedad real (y otros elementos representan argumentos genéricos). [NullableContext]siempre se instancia con un solo byte.

Un valor de 2significa "anulable". 1significa "no anulable" y 0significa "ajeno".

canton7
fuente
Es realmente complicado. Acabo de encontrar un caso de uso que no está cubierto por este código. interfaz pública IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Si llamo al método IBusinessRelationcon la propiedad Nameme sale falso.
gsharp
@gsharp Ah, no lo había probado con interfaces, ni ningún tipo de herencia. Supongo que es una solución relativamente fácil (mire los atributos de contexto de las interfaces base): intentaré solucionarlo más tarde
canton7
1
No es gran cosa. Solo quería mencionarlo. Estas cosas anulables me están volviendo loco ;-)
gsharp
1
@gsharp Mirándolo, debe pasar el tipo de interfaz que define la propiedad, es decir ICommon, no IBusinessRelation. Cada interfaz define la suya NullableContext. Aclaré mi respuesta y agregué una verificación de tiempo de ejecución para esto.
canton7