Bagikan melalui


Cara menentukan kesetaraan nilai untuk kelas atau struktur (Panduan Pemrograman C#)

Rekaman secara otomatis menerapkan kesetaraan nilai. Pertimbangkan untuk menentukan record alih-alih class ketika jenis Anda memodelkan data dan harus menerapkan kesetaraan nilai.

Saat Anda menentukan kelas atau struktur, Anda memutuskan apakah masuk akal untuk membuat definisi kustom kesetaraan nilai (atau yang setara) untuk jenis tersebut. Biasanya, Anda menerapkan kesetaraan nilai ketika Anda ingin menambahkan objek jenis ke koleksi, atau ketika tujuan utamanya adalah untuk menyimpan sekumpulan bidang atau properti. Anda dapat mendasarkan definisi kesetaraan nilai Anda pada perbandingan semua bidang dan properti dalam jenis, atau Anda dapat mendasarkan definisi pada subset.

Dalam kedua kasus, dan dalam kedua kelas dan struktur, implementasi Anda harus mengikuti lima jaminan kesetaraan (untuk aturan berikut, asumsikan bahwa x, y dan z tidak null):

  1. Properti refleksif: x.Equals(x) mengembalikan true.

  2. Properti simetris: x.Equals(y) mengembalikan nilai yang sama dengan y.Equals(x).

  3. Properti transitif: jika (x.Equals(y) && y.Equals(z)) mengembalikan true, maka x.Equals(z) mengembalikan true.

  4. Pemanggilan x.Equals(y) berturut-turut mengembalikan nilai yang sama selama objek yang dirujuk oleh x dan y tidak dimodifikasi.

  5. Nilai non-null tidak sama dengan null. Namun, x.Equals(y) menjadi pengecualian ketika x null. Itu melanggar aturan 1 atau 2, tergantung pada argumen pada Equals.

Setiap struktur yang Anda tentukan sudah memiliki implementasi default kesetaraan nilai yang diwarisinya dari System.ValueType ambil alih terhadap metode Object.Equals(Object). Implementasi ini menggunakan refleksi untuk memeriksa semua bidang dan properti dalam jenis. Meskipun implementasi ini menciptakan hasil yang benar, itu justru relatif lambat dibandingkan dengan implementasi kustom yang Anda tulis khusus untuk jenis tersebut.

Detail implementasi untuk kesetaraan nilai berbeda untuk kelas dan struktur. Namun, kelas dan struktur memerlukan langkah-langkah dasar yang sama untuk menerapkan kesetaraan:

  1. Mengambil alih metode virtualObject.Equals(Object). Dalam kebanyakan kasus, implementasi Anda terhadap bool Equals( object obj ) hanya boleh memanggil metode khusus jenis Equals yang merupakan implementasi antarmuka System.IEquatable<T>. (Lihat langkah 2.)

  2. Terapkan antarmuka System.IEquatable<T> dengan menyediakan metode khusus jenis Equals. Di sinilah perbandingan kesetaraan aktual dilakukan. Misalnya, Anda mungkin memutuskan untuk menentukan kesetaraan dengan hanya membandingkan satu atau dua bidang dalam jenis Anda. Jangan membuat pengecualian dari Equals. Untuk kelas yang terkait dengan pewarisan:

    • Metode ini hanya boleh memeriksa bidang yang dideklarasikan dalam kelas. Ini harus memanggil base.Equals untuk memeriksa bidang yang ada di kelas dasar. (Jangan panggil base.Equals jika jenis mewarisi langsung dari Object, karena implementasi Object dari Object.Equals(Object) melakukan pemeriksaan kesetaraan referensi.)

    • Dua variabel harus dianggap sama hanya jika jenis run-time variabel yang dibandingkan sama. Selain itu, pastikan bahwa IEquatable implementasi Equals metode untuk jenis run-time digunakan jika jenis run-time dan compile-time variabel berbeda. Salah satu strategi untuk memastikan jenis run-time selalu dibandingkan dengan benar adalah hanya menerapkan IEquatable di sealed kelas. Untuk informasi selengkapnya, lihat contoh kelas selanjutnya di artikel ini.

  3. Opsional tetapi disarankan: Kelebihan beban operator == dan !=.

  4. Ambil alih Object.GetHashCode sehingga dua objek yang memiliki kesetaraan nilai menghasilkan hash yang sama.

  5. Opsional: Untuk mendukung definisi untuk "lebih dari" atau "kurang dari," terapkan antarmuka IComparable<T> untuk jenis Anda, dan juga kelebihan beban operator <= dan >=.

Catatan

Anda dapat menggunakan rekaman untuk mendapatkan semantik kesetaraan nilai tanpa kode boilerplate yang tidak perlu.

Contoh kelas

Contoh berikut menunjukkan cara menerapkan kesetaraan nilai dalam kelas (jenis referensi).

namespace ValueEqualityClass;

class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; private set; }
    public int Y { get; private set; }

    public TwoDPoint(int x, int y)
    {
        if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.X = x;
        this.Y = y;
    }

    public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);

    public bool Equals(TwoDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // If run-time types are not exactly the same, return false.
        if (this.GetType() != p.GetType())
        {
            return false;
        }

        // Return true if the fields match.
        // Note that the base class is not invoked because it is
        // System.Object, which defines Equals as reference equality.
        return (X == p.X) && (Y == p.Y);
    }

    public override int GetHashCode() => (X, Y).GetHashCode();

    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}

// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
    public int Z { get; private set; }

    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        if ((z < 1) || (z > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.Z = z;
    }

    public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);

    public bool Equals(ThreeDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // Check properties that this class declares.
        if (Z == p.Z)
        {
            // Let base class check its own fields
            // and do the run-time type comparison.
            return base.Equals((TwoDPoint)p);
        }
        else
        {
            return false;
        }
    }

    public override int GetHashCode() => (X, Y, Z).GetHashCode();

    public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                // null == null = true.
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles the case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}

class Program
{
    static void Main(string[] args)
    {
        ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointC = null;
        int i = 5;

        Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
        Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
        Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
        Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

        TwoDPoint pointD = null;
        TwoDPoint pointE = null;

        Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

        pointE = new TwoDPoint(3, 4);
        Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
        Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
        Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

        System.Collections.ArrayList list = new System.Collections.ArrayList();
        list.Add(new ThreeDPoint(3, 4, 5));
        Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

/* Output:
    pointA.Equals(pointB) = True
    pointA == pointB = True
    null comparison = False
    Compare to some other type = False
    Two null TwoDPoints are equal: True
    (pointE == pointA) = False
    (pointA == pointE) = False
    (pointA != pointE) = True
    pointE.Equals(list[0]): False
*/

Pada kelas (jenis referensi), implementasi default pada kedua metode Object.Equals(Object) melakukan perbandingan kesetaraan referensi, bukan pemeriksaan kesetaraan nilai. Ketika pelaksana mengambil alih metode virtual, tujuannya adalah untuk memberinya nilai semantik kesetaraan.

Operator == dan != dapat digunakan dengan kelas bahkan jika kelas tidak membebaninya. Namun, perilaku default berfungsi untuk melakukan pemeriksaan kesetaraan referensi. Dalam kelas, jika Anda membebani metode Equals Secara berlebih, Anda harus membebani operator == dan !=, tetapi tidak diperlukan.

Penting

Contoh kode sebelumnya mungkin tidak menangani setiap skenario pewarisan seperti yang Anda harapkan. Pertimbangkan gambar berikut:

TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True

Kode ini melaporkan bahwa p1 sama dengan p2 meskipun ada perbedaan pada nilai z. Perbedaan diabaikan karena pengompilasi memilih implementasi TwoDPoint berdasarkan jenis waktu kompilasi IEquatable.

Kesetaraan nilai bawaan jenis record menangani skenario seperti ini dengan benar. Jika TwoDPoint dan ThreeDPoint adalah jenis record, hasil dari p1.Equals(p2) adalah False. Untuk informasi selengkapnya, lihat Kesetaraan dalam record hierarki pewarisan jenis.

Contoh struktur

Contoh berikut menunjukkan cara menerapkan kesetaraan nilai dalam kelas (jenis nilai):

namespace ValueEqualityStruct
{
    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
            {
                throw new ArgumentException("Point must be in range 1 - 2000");
            }
            X = x;
            Y = y;
        }

        public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);

        public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;

        public override int GetHashCode() => (X, Y).GetHashCode();

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
    }

    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // True:
            Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
            // True:
            Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
            // True:
            Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
            // False:
            Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
            // False:
            Console.WriteLine("(pointA == null) = {0}", pointA == null);
            // True:
            Console.WriteLine("(pointA != null) = {0}", pointA != null);
            // False:
            Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
            // CS0019:
            // Console.WriteLine("pointA == i = {0}", pointA == i);

            // Compare unboxed to boxed.
            System.Collections.ArrayList list = new System.Collections.ArrayList();
            list.Add(new TwoDPoint(3, 4));
            // True:
            Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));

            // Compare nullable to nullable and to non-nullable.
            TwoDPoint? pointC = null;
            TwoDPoint? pointD = null;
            // False:
            Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
            // True:
            Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

            TwoDPoint temp = new TwoDPoint(3, 4);
            pointC = temp;
            // True:
            Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

            pointD = temp;
            // True:
            Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }

    /* Output:
        pointA.Equals(pointB) = True
        pointA == pointB = True
        Object.Equals(pointA, pointB) = True
        pointA.Equals(null) = False
        (pointA == null) = False
        (pointA != null) = True
        pointA.Equals(i) = False
        pointE.Equals(list[0]): True
        pointA == (pointC = null) = False
        pointC == pointD = True
        pointA == (pointC = 3,4) = True
        pointD == (pointC = 3,4) = True
    */
}

Untuk struktur, implementasi default Object.Equals(Object) (yang merupakan versi yang ditimpa pada System.ValueType) melakukan pemeriksaan kesetaraan nilai dengan menggunakan pantulan untuk membandingkan nilai setiap bidang dalam jenis. Ketika pelaksana mengambil alih metode virtual Equals dalam struktur, tujuannya adalah untuk memberikan cara yang lebih efisien untuk melakukan pemeriksaan kesetaraan nilai dan secara opsional untuk mendasarkan perbandingan pada beberapa subset bidang atau properti struct.

Operator == dan != tidak dapat beroperasi pada struktur kecuali struktur secara eksplisit membebani mereka.

Lihat juga