Tidak ada atau standar perbandingan argumen generik dalam C #

288

Saya memiliki metode generik yang didefinisikan seperti ini:

public void MyMethod<T>(T myArgument)

Hal pertama yang ingin saya lakukan adalah memeriksa apakah nilai myArgument adalah nilai default untuk jenis itu, sesuatu seperti ini:

if (myArgument == default(T))

Tetapi ini tidak dapat dikompilasi karena saya belum menjamin bahwa T akan mengimplementasikan operator ==. Jadi saya beralih kodenya ke ini:

if (myArgument.Equals(default(T)))

Sekarang ini mengkompilasi, tetapi akan gagal jika myArgument adalah nol, yang merupakan bagian dari apa yang saya uji. Saya dapat menambahkan cek nol eksplisit seperti ini:

if (myArgument == null || myArgument.Equals(default(T)))

Sekarang ini terasa berlebihan bagi saya. ReSharper bahkan menyarankan agar saya mengubah bagian myArgument == null menjadi myArgument == default (T) yang merupakan tempat saya memulai. Apakah ada cara yang lebih baik untuk menyelesaikan masalah ini?

Saya perlu mendukung kedua jenis referensi dan tipe nilai.

Stefan Moser
sumber
C # sekarang mendukung Operator Bersyarat Null , yang merupakan gula sintaksis untuk contoh terakhir yang Anda berikan. Kode Anda akan menjadi if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU
1
@ wizard07KSU Itu tidak berfungsi untuk tipe nilai, yaitu mengevaluasi truedalam hal apa pun karena kami Equalsakan selalu dipanggil untuk tipe nilai karena myArgumenttidak dapat nulldalam kasus ini dan hasil Equals(a boolean) tidak akan pernah ada null.
jasper
Hampir sama duplikatnya (jadi jangan memilih untuk menutup): Tidak bisakah operator == diterapkan pada tipe umum dalam C #?
GSerg

Jawaban:

583

Untuk menghindari tinju, cara terbaik untuk membandingkan obat generik untuk kesetaraan adalah dengan EqualityComparer<T>.Default. Ini menghormati IEquatable<T>(tanpa tinju) juga object.Equals, dan menangani semua Nullable<T>nuansa "terangkat". Karenanya:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

Ini akan cocok dengan:

  • null untuk kelas
  • null (kosong) untuk Nullable<T>
  • nol / salah / dll untuk struct lainnya
Marc Gravell
sumber
28
Wow, betapa menyenangkannya mengaburkan! Ini jelas cara untuk pergi, pujian.
Nick Farina
1
Jelas jawaban terbaik. Tidak ada garis berlekuk dalam kode saya setelah menulis ulang untuk menggunakan solusi ini.
Nathan Ridley
13
Jawaban bagus! Bahkan lebih baik menambahkan metode ekstensi untuk baris kode ini sehingga Anda dapat pergi obj.IsDefaultForType ()
rikoe
2
@nawfal dalam kasus Person, p1.Equals(p2)akan tergantung pada apakah mengimplementasikan IEquatable<Person>pada API publik, atau melalui implementasi eksplisit - yaitu apakah kompiler dapat melihat Equals(Person other)metode publik . Namun; dalam generik , IL yang sama digunakan untuk semua T; a T1yang terjadi untuk mengimplementasikan IEquatable<T1>perlu diperlakukan secara identik dengan T2yang tidak - jadi tidak, itu tidak akan menemukan Equals(T1 other)metode, bahkan jika ada saat runtime. Dalam kedua kasus, ada juga yang nullharus dipikirkan (salah satu objek). Jadi dengan obat generik, saya akan menggunakan kode yang saya posting.
Marc Gravell
5
Saya tidak dapat memutuskan apakah jawaban ini mendorong saya menjauh atau mendekati kegilaan. +1
Steven Liekens
118

Bagaimana dengan ini:

if (object.Equals(myArgument, default(T)))
{
    //...
}

Menggunakan static object.Equals()metode ini menghindari keharusan bagi Anda untuk melakukan nullpemeriksaan sendiri. Kualifikasi panggilan secara eksplisit object.mungkin tidak perlu tergantung pada konteks Anda, tetapi saya biasanya mengawali staticpanggilan dengan nama jenis hanya untuk membuat kode lebih mudah larut.

Kent Boogaart
sumber
2
Anda bahkan dapat menjatuhkan "objek." bagian karena itu berlebihan. if (Equals (myArgument, default (T)))
Stefan Moser
13
Memang benar, tetapi mungkin tidak tergantung pada konteksnya. Mungkin ada contoh Equals () metode yang mengambil dua argumen. Saya cenderung untuk secara eksplisit mengawali semua panggilan statis dengan nama kelas, jika hanya untuk membuat kode lebih mudah dibaca.
Kent Boogaart
8
Perlu dicatat bahwa itu akan menyebabkan tinju dan dalam beberapa kasus mungkin penting
nightcoder
2
Bagi saya ini tidak berfungsi ketika menggunakan bilangan bulat yang sudah dikotak. Karena itu akan menjadi objek dan default untuk objek adalah nol, bukan 0.
riezebosch
28

Saya dapat menemukan artikel Microsoft Connect yang membahas masalah ini secara terperinci:

Sayangnya, perilaku ini dirancang dan tidak ada solusi mudah untuk memungkinkan penggunaan dengan tipe parameter yang mungkin berisi tipe nilai.

Jika tipe tersebut dikenal sebagai tipe referensi, kelebihan standar ditentukan pada variabel tes objek untuk kesetaraan referensi, meskipun suatu tipe dapat menentukan kelebihan kustomnya sendiri. Compiler menentukan overload mana yang digunakan berdasarkan tipe statis dari variabel (penentuannya bukan polimorfik). Oleh karena itu, jika Anda mengubah contoh untuk membatasi parameter tipe generik T ke tipe referensi yang tidak disegel (seperti Pengecualian), kompiler dapat menentukan kelebihan beban spesifik untuk digunakan dan kode berikut akan dikompilasi:

public class Test<T> where T : Exception

Jika jenisnya dikenal sebagai tipe nilai, lakukan tes kesetaraan nilai spesifik berdasarkan tipe yang tepat digunakan. Tidak ada perbandingan "default" yang baik di sini karena perbandingan referensi tidak bermakna pada tipe nilai dan kompiler tidak dapat mengetahui perbandingan nilai spesifik mana yang akan dipancarkan. Kompiler dapat memancarkan panggilan ke ValueType.Equals (Object) tetapi metode ini menggunakan refleksi dan cukup tidak efisien dibandingkan dengan perbandingan nilai tertentu. Oleh karena itu, bahkan jika Anda menentukan batasan tipe-nilai pada T, tidak ada yang masuk akal bagi kompiler untuk menghasilkan di sini:

public class Test<T> where T : struct

Dalam kasus yang Anda sajikan, di mana kompiler bahkan tidak tahu apakah T adalah tipe nilai atau referensi, maka tidak ada apa pun yang dihasilkan yang akan valid untuk semua jenis yang mungkin. Perbandingan referensi tidak akan valid untuk tipe nilai dan semacam perbandingan nilai tidak terduga untuk tipe referensi yang tidak kelebihan beban.

Inilah yang dapat Anda lakukan ...

Saya telah memvalidasi bahwa kedua metode ini berfungsi untuk perbandingan referensi dan tipe nilai umum:

object.Equals(param, default(T))

atau

EqualityComparer<T>.Default.Equals(param, default(T))

Untuk melakukan perbandingan dengan operator "==", Anda harus menggunakan salah satu metode ini:

Jika semua case T berasal dari kelas dasar yang diketahui, Anda dapat membiarkan kompiler tahu menggunakan batasan tipe generik.

public void MyMethod<T>(T myArgument) where T : MyBase

Kompiler kemudian mengenali cara menjalankan operasi MyBasedan tidak akan melemparkan "Operator '==' tidak dapat diterapkan ke operan kesalahan tipe 'T' dan 'T'" yang Anda lihat sekarang.

Pilihan lain adalah membatasi T untuk semua jenis yang mengimplementasikan IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

Dan kemudian gunakan CompareTometode yang didefinisikan oleh antarmuka IComparable .

Eric Schoonover
sumber
4
"Perilaku ini dirancang dan tidak ada solusi mudah untuk memungkinkan penggunaan dengan tipe parameter yang mungkin mengandung tipe nilai." Sebenarnya Microsoft salah. Ada solusi mudah: MS harus memperluas opq ceq agar berfungsi pada tipe nilai sebagai operator bitwise. Kemudian mereka dapat memberikan intrinsik yang hanya menggunakan opcode ini, misalnya objek.BitwiseOrReferenceEquals <T> (nilai, default (T)) yang hanya menggunakan ceq. Untuk kedua nilai dan jenis referensi ini akan memeriksa kesetaraan bitwise dari nilai (tetapi untuk jenis referensi, referensi persamaan bitwise sama dengan objek. Referensi sama)
Qwertie
1
Saya pikir tautan Microsoft Connect yang Anda inginkan adalah connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie
18

Coba ini:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

yang harus dikompilasi, dan lakukan apa yang Anda inginkan.

Lasse V. Karlsen
sumber
Bukankah <code> default (T) </code> berlebihan? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> harus melakukan trik.
Joshcodes
2
1) apakah Anda mencobanya, dan 2) apa yang kemudian Anda bandingkan terhadap, objek pembanding? The EqualsMetode IEqualityComparermembutuhkan dua argumen, dua benda untuk membandingkan, sehingga tidak ada, tidak berlebihan.
Lasse V. Karlsen
Ini bahkan lebih baik daripada jawaban IMHO yang diterima karena menangani tinju / unboxing dan jenis lainnya. Lihat jawaban pertanyaan "closed as dupe" ini: stackoverflow.com/a/864860/210780
ashes999
7

(Diedit)

Marc Gravell memiliki jawaban terbaik, tetapi saya ingin memposting potongan kode sederhana yang saya upayakan untuk menunjukkannya. Jalankan saja ini di aplikasi konsol C # sederhana:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

Satu hal lagi: dapatkah seseorang dengan VS2008 mencoba ini sebagai metode ekstensi? Saya terjebak dengan 2005 di sini dan saya ingin tahu apakah itu akan diizinkan.


Sunting: Ini cara membuatnya berfungsi sebagai metode ekstensi:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}
Joel Coehoorn
sumber
3
Itu "bekerja" sebagai metode ekstensi. Yang menarik karena berfungsi bahkan jika Anda mengatakan o.IsDefault <object> () ketika o adalah nol. Scary =)
Nick Farina
6

Untuk menangani semua jenis T, termasuk di mana T adalah tipe primitif, Anda harus mengkompilasi dalam kedua metode perbandingan:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }
Nick Farina
sumber
1
Perhatikan bahwa fungsi telah diubah untuk menerima Fungsi <T> dan mengembalikan T, yang menurut saya tidak sengaja dihapus dari kode penanya.
Nick Farina
Sepertinya ReSharper mengacaukanku. Tidak menyadari peringatannya tentang kemungkinan perbandingan antara tipe nilai dan nol bukanlah peringatan kompilator.
Nathan Ridley
2
FYI: Jika T ternyata menjadi tipe nilai maka perbandingan terhadap nol akan diperlakukan sebagai selalu salah oleh jitter.
Eric Lippert
Masuk akal - runtime akan membandingkan pointer ke tipe nilai. Namun, pemeriksaan Equals () berfungsi dalam kasus tersebut (yang menarik, karena tampaknya sangat dinamis untuk mengatakan 5.Equals (4) yang melakukan kompilasi).
Nick Farina
2
Lihat jawaban EqualityComparer <T> untuk alternatif yang tidak melibatkan tinju et
Marc Gravell
2

Akan ada masalah di sini -

Jika Anda mengizinkan ini berfungsi untuk semua jenis, default (T) akan selalu nol untuk tipe referensi, dan 0 (atau struct penuh 0) untuk tipe nilai.

Ini mungkin bukan perilaku yang Anda cari. Jika Anda ingin ini berfungsi secara umum, Anda mungkin perlu menggunakan refleksi untuk memeriksa tipe T, dan menangani tipe nilai yang berbeda dari tipe referensi.

Atau, Anda bisa meletakkan batasan antarmuka pada ini, dan antarmuka dapat memberikan cara untuk memeriksa terhadap default kelas / struct.

Reed Copsey
sumber
1

Saya pikir Anda mungkin perlu membagi logika ini menjadi dua bagian dan memeriksa null terlebih dahulu.

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

Dalam metode IsNull, kami mengandalkan fakta bahwa objek ValueType tidak bisa nol menurut definisi jadi jika nilai kebetulan kelas yang berasal dari ValueType, kami sudah tahu itu bukan nol. Di sisi lain, jika itu bukan tipe nilai maka kita bisa membandingkan nilai yang dilemparkan ke objek terhadap nol. Kita bisa menghindari tanda centang pada ValueType dengan langsung menuju ke pemain ke objek, tetapi itu berarti bahwa tipe nilai akan mendapatkan kotak yang mungkin ingin kita hindari karena itu menyiratkan bahwa objek baru dibuat pada heap.

Dalam metode IsNullOrEmpty, kami memeriksa kasus khusus string. Untuk semua jenis lainnya, kami membandingkan nilainya (yang sudah diketahui bukan nol) terhadap nilai defaultnya yang untuk semua jenis referensi adalah nol dan untuk jenis nilai biasanya berupa nol (jika tidak terpisahkan).

Menggunakan metode ini, kode berikut berperilaku seperti yang Anda harapkan:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}
Damian Powell
sumber
1

Metode penyuluhan berdasarkan jawaban yang diterima.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Pemakaian:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Alternatif dengan nol untuk menyederhanakan:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Pemakaian:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
dynamiclynk
sumber
0

Saya menggunakan:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}
kofifus
sumber
-1

Tidak tahu apakah ini berfungsi dengan kebutuhan Anda atau tidak, tetapi Anda bisa membatasi T untuk menjadi Tipe yang mengimplementasikan antarmuka seperti IComparable dan kemudian menggunakan metode ComparesTo () dari antarmuka itu (yang didukung / ditangani nullR oleh IIRC) seperti ini :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

Mungkin ada antarmuka lain yang bisa Anda gunakan juga IEquitable, dll.

caryden
sumber
OP khawatir dengan NullReferenceException dan Anda menjaminnya sama.
nawfal
-2

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

Operator '==' tidak dapat diterapkan ke operan tipe 'T' dan 'T'

Saya tidak bisa memikirkan cara untuk melakukan ini tanpa tes null eksplisit diikuti dengan memanggil metode atau objek Equals. Sama seperti yang disarankan di atas.

Anda dapat menemukan solusi menggunakan System.Comparison tetapi sebenarnya itu akan berakhir dengan lebih banyak baris kode dan meningkatkan kompleksitas secara substansial.

cfeduke
sumber
-3

Saya pikir kamu sudah dekat.

if (myArgument.Equals(default(T)))

Sekarang ini mengkompilasi, tetapi akan gagal jika myArgument nol, yang merupakan bagian dari apa yang saya uji. Saya dapat menambahkan cek nol eksplisit seperti ini:

Anda hanya perlu membalikkan objek tempat persamaan dipanggil untuk pendekatan null-safe yang elegan.

default(T).Equals(myArgument);
Scott McKay
sumber
Saya sedang memikirkan hal yang persis sama.
Chris Gessler
6
default (T) dari jenis referensi adalah nol dan menghasilkan NullReferenceException yang dijamin.
Stefan Steinegger