C # tipe generik batasan untuk semua nullable

111

Jadi saya punya kelas ini:

public class Foo<T> where T : ???
{
    private T item;

    public bool IsNull()
    {
        return item == null;
    }

}

Sekarang saya mencari batasan tipe yang memungkinkan saya untuk menggunakan semuanya sebagai parameter tipe yang bisa null. Itu berarti semua jenis referensi, serta semua jenis Nullable( T?):

Foo<String> ... = ...
Foo<int?> ... = ...

harus memungkinkan.

Menggunakan classsebagai batasan tipe hanya memungkinkan saya untuk menggunakan tipe referensi.

Informasi Tambahan: Saya sedang menulis aplikasi pipa dan filter, dan ingin menggunakan nullreferensi sebagai item terakhir yang lolos ke pipa, sehingga setiap filter dapat ditutup dengan baik, melakukan pembersihan, dll ...

jkammerer.dll
sumber
1
@Tim yang tidak mengizinkan Nullables
Rik
Tautan ini dapat membantu Anda: social.msdn.microsoft.com/Forums/en-US/…
Réda Mattar
2
Ini tidak mungkin dilakukan secara langsung. Mungkin Anda bisa memberi tahu kami lebih banyak tentang skenario Anda? Atau mungkin Anda dapat menggunakan IFoo<T>sebagai jenis kerja dan membuat instance melalui metode pabrik? Itu bisa dibuat untuk bekerja.
Jon
Saya tidak yakin mengapa Anda ingin atau perlu membatasi sesuatu dengan cara ini. Jika satu-satunya niat Anda adalah mengubah "if x == null" menjadi if x.IsNull () "ini tampaknya tidak berguna dan tidak intuitif bagi 99,99% pengembang yang terbiasa dengan sintaks sebelumnya. Kompilator tidak akan membiarkan Anda melakukannya" if (int) x == null ", jadi Anda sudah terlindungi.
RJ Lohan
1
Ini cukup banyak dibahas di SO. stackoverflow.com/questions/209160/… dan stackoverflow.com/questions/13794554/…
Maxim Gershkovich

Jawaban:

22

Jika Anda ingin melakukan pemeriksaan waktu proses di konstruktor Foo daripada melakukan pemeriksaan waktu kompilasi, Anda dapat memeriksa apakah jenisnya bukan referensi atau jenis nullable, dan memberikan pengecualian jika demikian.

Saya menyadari bahwa hanya melakukan pemeriksaan waktu proses mungkin tidak dapat diterima, tetapi untuk berjaga-jaga:

public class Foo<T>
{
    private T item;

    public Foo()
    {
        var type = typeof(T);

        if (Nullable.GetUnderlyingType(type) != null)
            return;

        if (type.IsClass)
            return;

        throw new InvalidOperationException("Type is not nullable or reference type.");
    }

    public bool IsNull()
    {
        return item == null;
    }
}

Kemudian kode berikut dikompilasi, tetapi yang terakhir ( foo3) melontarkan pengecualian dalam konstruktor:

var foo1 = new Foo<int?>();
Console.WriteLine(foo1.IsNull());

var foo2 = new Foo<string>();
Console.WriteLine(foo2.IsNull());

var foo3= new Foo<int>();  // THROWS
Console.WriteLine(foo3.IsNull());
Matthew Watson
sumber
31
Jika Anda akan melakukan ini, pastikan Anda melakukan pemeriksaan di konstruktor statis , jika tidak, Anda akan memperlambat konstruksi setiap instance kelas generik Anda (tidak perlu)
Eamon Nerbonne
2
@EamonNerbonne Anda tidak boleh mengajukan pengecualian dari konstruktor statis: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson
5
Pedoman tidak mutlak. Jika Anda menginginkan pemeriksaan ini, Anda harus menukar biaya pemeriksaan runtime vs. ketidaktepatan pengecualian dalam konstruktor statis. Karena Anda benar-benar menerapkan penganalisis statis poor-mans di sini, pengecualian ini tidak boleh dibuang kecuali selama pengembangan. Terakhir, meskipun Anda ingin menghindari pengecualian konstruksi statis dengan segala cara (tidak bijaksana), Anda harus tetap melakukan pekerjaan sebanyak mungkin secara statis dan sesedikit mungkin dalam konstruktor instans - misalnya dengan menyetel bendera "isBorked" atau apa pun.
Eamon Nerbonne
Kebetulan, saya rasa Anda tidak harus mencoba melakukan ini sama sekali. Dalam kebanyakan situasi, saya lebih suka menerima ini sebagai batasan C #, daripada mencoba dan bekerja dengan abstraksi yang bocor dan rawan kegagalan. Misalnya solusi yang berbeda mungkin hanya memerlukan kelas, atau hanya memerlukan struct (dan secara eksplisit membuat em nullable) - atau melakukan keduanya dan memiliki dua versi. Itu bukan kritik untuk solusi ini; hanya saja masalah ini tidak dapat diselesaikan dengan baik - kecuali, Anda bersedia untuk menulis penganalisis roslyn khusus.
Eamon Nerbonne
1
Anda bisa mendapatkan yang terbaik dari kedua dunia - pertahankan static bool isValidTypebidang yang Anda setel di konstruktor statis, lalu cukup periksa bendera itu di konstruktor instans dan lempar jika itu jenis yang tidak valid sehingga Anda tidak melakukan semua pekerjaan pemeriksaan setiap kali Anda membuat sebuah contoh. Saya sering menggunakan pola ini.
Mike Marynowski
20

Saya tidak tahu bagaimana menerapkan setara dengan OR di obat generik. Namun saya dapat mengusulkan untuk menggunakan kata kunci default untuk membuat null untuk tipe nullable dan nilai 0 untuk struktur:

public class Foo<T>
{
    private T item;

    public bool IsNullOrDefault()
    {
        return Equals(item, default(T));
    }
}

Anda juga bisa mengimplementasikan versi Nullable Anda:

class MyNullable<T> where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(MyNullable<T> value)
    {
        return value != null ? value.Value : default(T);
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T> { Value = value };
    }
}

class Foo<T> where T : class
{
    public T Item { get; set; }

    public bool IsNull()
    {
        return Item == null;
    }
}

Contoh:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Foo<MyNullable<int>>().IsNull()); // true
        Console.WriteLine(new Foo<MyNullable<int>> {Item = 3}.IsNull()); // false
        Console.WriteLine(new Foo<object>().IsNull()); // true
        Console.WriteLine(new Foo<object> {Item = new object()}.IsNull()); // false

        var foo5 = new Foo<MyNullable<int>>();
        int integer = foo5.Item;
        Console.WriteLine(integer); // 0

        var foo6 = new Foo<MyNullable<double>>();
        double real = foo6.Item;
        Console.WriteLine(real); // 0

        var foo7 = new Foo<MyNullable<double>>();
        foo7.Item = null;
        Console.WriteLine(foo7.Item); // 0
        Console.WriteLine(foo7.IsNull()); // true
        foo7.Item = 3.5;
        Console.WriteLine(foo7.Item); // 3.5
        Console.WriteLine(foo7.IsNull()); // false

        // var foo5 = new Foo<int>(); // Not compile
    }
}
Ryszard Dżegan
sumber
Nullable <T> asli dalam kerangka kerja adalah struct, bukan kelas. Menurut saya bukan ide yang baik untuk membuat pembungkus jenis referensi yang akan meniru jenis nilai.
Niall Connaughton
1
Saran pertama menggunakan default sudah sempurna! Sekarang template saya dengan tipe generik yang dikembalikan dapat mengembalikan null untuk objek dan nilai default untuk tipe bawaan.
Casey Anderson
13

Saya mengalami masalah ini untuk kasus sederhana yang menginginkan metode statis generik yang dapat mengambil apa pun "nullable" (baik jenis referensi atau Nullables), yang membawa saya ke pertanyaan ini tanpa solusi yang memuaskan. Jadi saya datang dengan solusi saya sendiri yang relatif lebih mudah untuk dipecahkan daripada pertanyaan yang dinyatakan OP dengan hanya memiliki dua metode kelebihan beban, satu yang mengambil Tdan memiliki kendala where T : classdan yang lain yang mengambil T?dan memiliki where T : struct.

Saya kemudian menyadari, solusi itu juga dapat diterapkan pada masalah ini untuk membuat solusi yang dapat diperiksa pada waktu kompilasi dengan membuat konstruktor pribadi (atau dilindungi) dan menggunakan metode pabrik statis:

    //this class is to avoid having to supply generic type arguments 
    //to the static factory call (see CA1000)
    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return Foo<TFoo>.Create(value);
        }

        public static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return Foo<TFoo?>.Create(value);
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo(T value)
        {
            item = value;
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return new Foo<TFoo>(value);
        }

        internal static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return new Foo<TFoo?>(value);
        }
    }

Sekarang kita bisa menggunakannya seperti ini:

        var foo1 = new Foo<int>(1); //does not compile
        var foo2 = Foo.Create(2); //does not compile
        var foo3 = Foo.Create(""); //compiles
        var foo4 = Foo.Create(new object()); //compiles
        var foo5 = Foo.Create((int?)5); //compiles

Jika Anda menginginkan konstruktor tanpa parameter, Anda tidak akan mendapatkan kelebihan beban, tetapi Anda masih dapat melakukan sesuatu seperti ini:

    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return Foo<TFoo>.Create<TFoo>();
        }

        public static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return Foo<TFoo?>.CreateNullable<TFoo>();
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo()
        {
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return new Foo<TFoo>();
        }

        internal static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return new Foo<TFoo?>();
        }
    }

Dan gunakan seperti ini:

        var foo1 = new Foo<int>(); //does not compile
        var foo2 = Foo.Create<int>(); //does not compile
        var foo3 = Foo.Create<string>(); //compiles
        var foo4 = Foo.Create<object>(); //compiles
        var foo5 = Foo.CreateNullable<int>(); //compiles

Ada beberapa kelemahan dari solusi ini, salah satunya adalah Anda mungkin lebih suka menggunakan 'baru' untuk membuat objek. Lain adalah bahwa Anda tidak akan dapat menggunakan Foo<T>sebagai jenis argumen generik untuk jenis kendala sesuatu seperti: where TFoo: new(). Terakhir adalah sedikit kode tambahan yang Anda butuhkan di sini yang akan meningkat terutama jika Anda membutuhkan banyak konstruktor yang kelebihan beban.

Dave M
sumber
8

Seperti yang disebutkan, Anda tidak dapat memiliki pemeriksaan waktu kompilasi untuk itu. Batasan umum di .NET sangat kurang, dan tidak mendukung sebagian besar skenario.

Namun saya menganggap ini sebagai solusi yang lebih baik untuk pemeriksaan run-time. Ini dapat dioptimalkan pada waktu kompilasi JIT, karena keduanya adalah konstanta.

public class SomeClass<T>
{
    public SomeClass()
    {
        // JIT-compile time check, so it doesn't even have to evaluate.
        if (default(T) != null)
            throw new InvalidOperationException("SomeClass<T> requires T to be a nullable type.");

        T variable;
        // This still won't compile
        // variable = null;
        // but because you know it's a nullable type, this works just fine
        variable = default(T);
    }
}
Aidiakapi
sumber
3

Batasan tipe seperti itu tidak mungkin. Menurut dokumentasi batasan tipe, tidak ada batasan yang menangkap tipe referensi dan nullable. Karena batasan hanya dapat digabungkan, tidak ada cara untuk membuat batasan seperti itu dengan kombinasi.

Namun, Anda dapat, untuk kebutuhan Anda, kembali ke parameter tipe yang tidak dibatasi, karena Anda selalu dapat memeriksa == null. Jika tipe adalah tipe nilai, pemeriksaan hanya akan selalu bernilai salah. Kemudian Anda mungkin akan mendapatkan peringatan R # "Kemungkinan perbandingan tipe nilai dengan null", yang tidak penting, selama semantiknya tepat untuk Anda.

Alternatif bisa digunakan

object.Equals(value, default(T))

alih-alih pemeriksaan null, karena default (T) di mana T: class selalu null. Ini, bagaimanapun, berarti bahwa Anda tidak dapat membedakan cuaca, nilai yang tidak dapat dinihilkan tidak pernah ditetapkan secara eksplisit atau hanya disetel ke nilai defaultnya.

Sven Amann
sumber
Saya pikir masalahnya adalah bagaimana memeriksa nilai itu tidak pernah ditetapkan. Berbeda dari nol tampaknya menunjukkan bahwa nilai telah diinisialisasi.
Ryszard Dżegan
Itu tidak membatalkan pendekatan, karena tipe nilai selalu ditetapkan (setidaknya secara implisit ke nilai default masing-masing).
Sven Amann
3

saya menggunakan

public class Foo<T> where T: struct
{
    private T? item;
}
ela
sumber
-2
    public class Foo<T>
    {
        private T item;

        public Foo(T item)
        {
            this.item = item;
        }

        public bool IsNull()
        {
            return object.Equals(item, null);
        }
    }

    var fooStruct = new Foo<int?>(3);
        var b = fooStruct.IsNull();

        var fooStruct1 = new Foo<int>(3);
        b = fooStruct1.IsNull();

        var fooStruct2 = new Foo<int?>(null);
        b = fooStruct2.IsNull();

        var fooStruct3 = new Foo<string>("qqq");
        b = fooStruct3.IsNull();

        var fooStruct4 = new Foo<string>(null);
        b = fooStruct4.IsNull();
Lihat Tajam
sumber
Pengetikan ini memungkinkan Foo <int> (42) baru dan IsNull () akan mengembalikan false, yang, meskipun benar secara semantik, tidak terlalu berarti.
RJ Lohan
1
42 adalah "Jawaban atas Pertanyaan Utama Kehidupan, Alam Semesta, dan Segalanya". Sederhananya: IsNull untuk setiap nilai int akan mengembalikan false (bahkan untuk nilai 0).
Ryszard Dżegan