it-swarm-id.com

Tidak ada atau standar perbandingan argumen generik dalam C #

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 tipe referensi dan tipe nilai.

239
Stefan Moser

Untuk menghindari tinju, cara terbaik untuk membandingkan obat generik untuk kesetaraan adalah dengan EqualityComparer<T>.Default. Ini menghormati IEquatable<T> (tanpa tinju) serta object.Equals, dan menangani semua Nullable<T> "mengangkat" nuansa. 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
486
Marc Gravell

Bagaimana dengan ini:

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

Dengan menggunakan metode static object.Equals(), Anda tidak perlu melakukan null sendiri. Kualifikasi panggilan secara eksplisit dengan object. mungkin tidak perlu tergantung pada konteks Anda, tetapi saya biasanya mengawali static panggilan dengan nama jenis hanya untuk membuat kode lebih larut.

113
Kent Boogaart

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

Sayangnya, perilaku ini adalah desain dan tidak ada solusi yang mudah untuk memungkinkan penggunaan dengan tipe parameter yang mungkin mengandung tipe nilai.

Jika jenisnya dikenal sebagai tipe referensi, kelebihan standar ditentukan pada variabel tes objek untuk kesetaraan referensi, meskipun suatu jenis dapat menentukan kelebihan kustom 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 khusus 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, 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 melakukan operasi pada MyBase dan tidak akan membuang "Operator '==' tidak dapat diterapkan pada operan kesalahan tipe 'T' dan 'T'" yang Anda lihat sekarang.

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

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

Dan kemudian gunakan metode CompareTo yang didefinisikan oleh Antarmuka IComparable .

24
Eric Schoonover

Coba ini:

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

yang harus dikompilasi, dan lakukan apa yang Anda inginkan.

(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.


Edit: Berikut 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));
    }
}
7
Joel Coehoorn

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;
    }
6
Nick Farina

Akan ada masalah di sini -

Jika Anda ingin 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 bekerja dengan cara yang 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.

2
Reed Copsey

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 boleh 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 dapat menghindari tanda centang pada ValueType dengan langsung menuju sebuah pemeran 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 nilai (yang sudah tahu adalah bukan null) terhadap nilai defaultnya yang untuk semua jenis referensi adalah nol dan untuk tipe nilai biasanya berupa nol (jika tidak terpisahkan) ).

Dengan 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) ...
}
1
Damian Powell

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