Jak używać refleksji .NET do sprawdzania typu odwołania zerowalnego

15

C # 8.0 wprowadza typy odwołania zerowalne. Oto prosta klasa z właściwością zerowalną:

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

Czy istnieje sposób na sprawdzenie, czy właściwość klasy używa typu odwołania zerowego poprzez odbicie?

shadeglare
źródło
kompilując i patrząc na IL, wygląda na to, że dodaje [NullableContext(2), Nullable((byte) 0)]to do type ( Foo) - więc to jest to, co należy sprawdzić, ale musiałbym wykopać więcej, aby zrozumieć zasady interpretowania tego!
Marc Gravell
4
Tak, ale to nie jest banalne. Na szczęście jest to udokumentowane .
Jeroen Mostert
o, rozumiem; tak string? Xdostaje żadnych atrybutów, i string Ydostaje [Nullable((byte)2)]się [NullableContext(2)]na dostępowych
Marc Gravell
1
Jeśli typ zawiera tylko wartości zerowalne (lub inne niż wartości zerowe), to wszystko jest reprezentowane przez NullableContext. Jeśli jest mieszanka, to również Nullableużywana. NullableContextto optymalizacja, której celem jest unikanie emisji w Nullabledowolnym miejscu.
kanton7

Odpowiedzi:

11

Wygląda na to, że działa, przynajmniej na typach, z którymi go testowałem.

Musisz podać PropertyInfowłaściwość, która Cię interesuje, a także właściwość, dla Typektórej ta właściwość jest zdefiniowana ( nie typ pochodny ani nadrzędny - musi to być typ dokładny):

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

Szczegółowe informacje można znaleźć w tym dokumencie .

Ogólną zasadą jest to, że albo sama właściwość może mieć [Nullable]atrybut, albo jeśli nie, typ otaczający może mieć [NullableContext]atrybut. Najpierw szukamy [Nullable], a następnie, jeśli go nie znajdziemy, szukamy [NullableContext]na podstawie typu zamykającego.

Kompilator może osadzić atrybuty w zestawie, a ponieważ możemy patrzeć na typ z innego zestawu, musimy wykonać ładowanie tylko do odbicia.

[Nullable]może być utworzony za pomocą tablicy, jeśli właściwość jest ogólna. W tym przypadku pierwszy element reprezentuje rzeczywistą właściwość (a kolejne elementy reprezentują ogólne argumenty). [NullableContext]jest zawsze tworzony z pojedynczym bajtem.

Wartość 2oznacza „nullable”. 1oznacza „nie dopuszczać do wartości zerowej” i 0oznacza „nieświadomy”.

kanton7
źródło
To naprawdę trudne. Właśnie znalazłem przypadek użycia, który nie jest objęty tym kodem. interfejs publiczny IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Jeśli wywołam metodę IBusinessRelationz właściwością Name, otrzymam wartość false.
gsharp,
@gsharp Ach, nie próbowałem tego z interfejsami ani jakimkolwiek rodzajem dziedziczenia. Domyślam się, że jest to stosunkowo łatwa poprawka (spójrz na atrybuty kontekstu z podstawowych interfejsów): Spróbuję to naprawić później
canton7
1
Nic takiego. Chciałem tylko o tym wspomnieć. Te nullable doprowadzają mnie do szału ;-)
gsharp
1
@gsharp Patrząc na to, musisz podać typ interfejsu, który definiuje właściwość - to znaczy ICommonnie IBusinessRelation. Każdy interfejs definiuje własny NullableContext. Wyjaśniłem swoją odpowiedź i dodałem do tego kontrolę czasu wykonywania.
kanton7