So verwenden Sie .NET Reflection, um nach nullbaren Referenztypen zu suchen

15

C # 8.0 führt nullfähige Referenztypen ein. Hier ist eine einfache Klasse mit einer nullbaren Eigenschaft:

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

Gibt es eine Möglichkeit zu überprüfen, ob eine Klasseneigenschaft über Reflektion einen nullbaren Referenztyp verwendet?

Schattenglanz
quelle
Beim Kompilieren und Betrachten der IL sieht es so aus, als würde dies [NullableContext(2), Nullable((byte) 0)]den Typ ( Foo) ergänzen - das ist also, worauf zu achten ist, aber ich müsste mehr graben, um die Regeln zu verstehen, wie man das interpretiert!
Marc Gravell
4
Ja, aber es ist nicht trivial. Glücklicherweise ist es ist dokumentiert .
Jeroen Mostert
ah ich sehe; so string? Xbekommt keine Attribute, und string Ywird [Nullable((byte)2)]mit [NullableContext(2)]auf den Zugriffs-
Marc GRA
1
Wenn ein Typ nur Nullables (oder Nicht-Nullables) enthält, wird dies alles durch dargestellt NullableContext. Wenn es eine Mischung gibt, dann auch Nullableverwendet. NullableContextist eine Optimierung, um zu vermeiden, dass Nullableüberall emittiert werden muss .
canton7

Antworten:

11

Dies scheint zu funktionieren, zumindest bei den Typen, mit denen ich es getestet habe.

Sie müssen das PropertyInfofür die Eigenschaft übergeben, an der Sie interessiert sind, und auch das, für Typedas diese Eigenschaft definiert ist ( kein abgeleiteter oder übergeordneter Typ - es muss der genaue Typ sein):

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

Weitere Informationen finden Sie in diesem Dokument .

Der allgemeine Kern ist, dass entweder die Eigenschaft selbst ein [Nullable]Attribut haben kann, oder wenn dies nicht der Fall ist, hat der einschließende Typ möglicherweise ein [NullableContext]Attribut. Wir suchen zuerst nach [Nullable], und wenn wir es nicht finden, suchen wir nach [NullableContext]dem umschließenden Typ.

Der Compiler bettet die Attribute möglicherweise in die Assembly ein. Da wir möglicherweise einen Typ aus einer anderen Assembly betrachten, müssen wir nur Reflexionen laden.

[Nullable]kann mit einem Array instanziiert werden, wenn die Eigenschaft generisch ist. In diesem Fall repräsentiert das erste Element die tatsächliche Eigenschaft (und weitere Elemente repräsentieren generische Argumente). [NullableContext]wird immer mit einem einzelnen Byte instanziiert.

Ein Wert von 2bedeutet "nullable". 1bedeutet "nicht nullbar" und 0bedeutet "ahnungslos".

canton7
quelle
Es ist wirklich schwierig. Ich habe gerade einen Anwendungsfall gefunden, der von diesem Code nicht abgedeckt wird. öffentliche Schnittstelle IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Wenn ich die Methode IBusinessRelationmit der Eigenschaft Nameaufrufe, erhalte ich false.
Gsharp
@gsharp Ah, ich hatte es nicht mit Schnittstellen oder irgendeiner Art von Vererbung versucht. Ich vermute, es ist eine relativ einfache Lösung (siehe Kontextattribute von Basisschnittstellen): Ich werde versuchen, sie später zu beheben
canton7
1
kein Biggie. Ich wollte es nur erwähnen. Dieses nullable Zeug macht mich verrückt ;-)
gsharp
1
@gsharp Wenn Sie es betrachten, müssen Sie den Typ der Schnittstelle übergeben, die die Eigenschaft definiert - das heißt ICommonnicht IBusinessRelation. Jede Schnittstelle definiert ihre eigene NullableContext. Ich habe meine Antwort geklärt und eine Laufzeitprüfung hinzugefügt.
canton7