Cara menggunakan .NET refleksi untuk memeriksa jenis referensi yang dapat dibatalkan

15

C # 8.0 memperkenalkan jenis referensi yang dapat dibatalkan. Berikut adalah kelas sederhana dengan properti nullable:

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

Apakah ada cara untuk memeriksa properti kelas menggunakan tipe referensi nullable melalui refleksi?

shadeglare
sumber
kompilasi dan melihat IL, sepertinya ini menambah [NullableContext(2), Nullable((byte) 0)]ke jenis ( Foo) - sehingga ini apa untuk memeriksa, tapi aku perlu menggali lebih memahami aturan bagaimana menafsirkan itu!
Marc Gravell
4
Ya, tapi itu tidak sepele. Untungnya, ini didokumentasikan .
Jeroen Mostert
ah, saya mengerti; sehingga string? Xtidak mendapat atribut, dan string Ymendapat [Nullable((byte)2)]dengan [NullableContext(2)]di accesor
Marc Gravell
1
Jika suatu tipe hanya berisi nullables (atau non-nullables), maka itu semua diwakili oleh NullableContext. Jika ada campuran, maka Nullablegunakan juga. NullableContextadalah pengoptimalan untuk mencoba dan menghindari keharusan memancarkan Nullablesemua tempat.
canton7

Jawaban:

11

Tampaknya ini berfungsi, setidaknya pada tipe yang saya uji.

Anda harus melewati PropertyInfountuk properti yang Anda minati, dan juga Typeproperti yang ditentukan ( bukan tipe turunan atau induk - harus tipe yang tepat):

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

Lihat dokumen ini untuk detailnya.

Inti umum adalah bahwa properti itu sendiri dapat memiliki [Nullable]atribut di atasnya, atau jika tidak tipe terlampir mungkin memiliki [NullableContext]atribut. Pertama-tama kita mencari [Nullable], maka jika kita tidak menemukannya kita mencari [NullableContext]pada tipe penutup.

Kompiler mungkin menanamkan atribut ke dalam rakitan, dan karena kita mungkin melihat jenis dari rakitan yang berbeda, kita perlu melakukan beban hanya refleksi.

[Nullable]mungkin dipakai dengan array, jika properti itu generik. Dalam hal ini, elemen pertama mewakili properti aktual (dan elemen lebih lanjut mewakili argumen generik). [NullableContext]selalu dipakai dengan satu byte.

Nilai 2sarana "nullable". 1berarti "tidak dapat dibatalkan", dan 0berarti "tidak menyadari".

wilayah7
sumber
Benar-benar rumit. Saya baru saja menemukan use case yang tidak tercakup oleh kode ini. antarmuka publik IBusinessRelation : ICommon {}/ public interface ICommon { string? Name {get;set;} }. Jika saya memanggil metode IBusinessRelationdengan Properti Namesaya salah.
gsharp
@ gsharp Ah, saya belum mencobanya dengan antarmuka, atau warisan apa pun. Saya menduga ini adalah perbaikan yang relatif mudah (lihat atribut konteks dari antarmuka dasar): Saya akan mencoba memperbaikinya nanti
canton7
1
tidak masalah Saya hanya ingin menyebutkannya. Barang yang dapat dibatalkan ini membuatku gila ;-)
gsharp
1
@ gsharp Melihat itu, Anda harus melewati jenis antarmuka yang mendefinisikan properti - yaitu ICommon, tidak IBusinessRelation. Setiap antarmuka mendefinisikan sendiri NullableContext. Saya telah mengklarifikasi jawaban saya, dan menambahkan pemeriksaan runtime untuk ini.
canton7