Comment utiliser la réflexion .NET pour vérifier le type de référence nullable

15

C # 8.0 introduit des types de référence nullables. Voici une classe simple avec une propriété nullable:

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

Existe-t-il un moyen de vérifier qu'une propriété de classe utilise un type de référence nullable via la réflexion?

ombre
la source
en compilant et en regardant l'IL, il semble que cela s'ajoute [NullableContext(2), Nullable((byte) 0)]au type ( Foo) - c'est donc ce qu'il faut vérifier, mais j'aurais besoin de creuser davantage pour comprendre les règles d'interprétation!
Marc Gravell
4
Oui, mais ce n'est pas anodin. Heureusement, il est documenté .
Jeroen Mostert
Ah, je vois; string? Xn'obtient donc aucun attribut, et string Yobtient [Nullable((byte)2)]avec [NullableContext(2)]les accesseurs
Marc Gravell
1
Si un type ne contient que des nullables (ou des non-nullables), alors tout est représenté par NullableContext. S'il y a un mélange, il est également Nullableutilisé. NullableContextest une optimisation pour essayer d'éviter d'avoir à émettre Nullablepartout.
canton7

Réponses:

11

Cela semble fonctionner, au moins sur les types avec lesquels je l'ai testé.

Vous devez transmettre le PropertyInfopour la propriété qui vous intéresse, ainsi Typeque celui sur lequel cette propriété est définie ( pas un type dérivé ou parent - ce doit être le type exact):

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

Consultez ce document pour plus de détails.

L'essentiel est que la propriété elle-même peut avoir un [Nullable]attribut, ou si ce n'est pas le cas, le type peut avoir un [NullableContext]attribut. Nous cherchons d'abord [Nullable], puis si nous ne le trouvons pas, nous recherchons [NullableContext]le type englobant.

Le compilateur peut incorporer les attributs dans l'assembly, et comme nous pouvons examiner un type d'un assembly différent, nous devons effectuer une charge de réflexion uniquement.

[Nullable]peut être instancié avec un tableau, si la propriété est générique. Dans ce cas, le premier élément représente la propriété réelle (et les autres éléments représentent des arguments génériques). [NullableContext]est toujours instancié avec un seul octet.

Une valeur de 2signifie "nullable". 1signifie "non annulable" et 0signifie "inconscient".

canton7
la source
C'est vraiment délicat. Je viens de trouver un cas d'utilisation qui n'est pas couvert par ce code. interface publique IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Si j'appelle la méthode IBusinessRelationavec la propriété, Nameje reçois faux.
gsharp
@gsharp Ah, je ne l'avais pas essayé avec des interfaces, ni aucune sorte d'héritage. Je suppose que c'est un correctif relativement facile (regardez les attributs de contexte des interfaces de base): je vais essayer de le réparer plus tard
canton7
1
pas de biggie. Je voulais juste le mentionner. Ce truc
nullable
1
@gsharp En le regardant, vous devez passer le type de l'interface qui définit la propriété - c'est-à-dire ICommon, non IBusinessRelation. Chaque interface définit la sienne NullableContext. J'ai clarifié ma réponse et ajouté une vérification d'exécution pour cela.
canton7