CodeGym /Kurslar /C# SELF /C#-da obyektlərin müqayisəsi:

C#-da obyektlərin müqayisəsi: EqualsGetHashCode()

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Giriş

Proqramlaşdırmada biz daim nəyisə müqayisə edirik: ədədlər, sətirlər, obyektlər. Amma C#-da obyektlərin "bərabərliyi" əslində nə deməkdir? Bu, həmişə göründüyü kimi aydın olmur. Bu gün biz obyektlərin necə müqayisə olunduğunu müəyyən edən iki əsas metodu müzakirə edəcəyik: Equals()GetHashCode(). Bu metodları başa düşmək proqramlarının düzgün işləməsi üçün kritikdir, xüsusilə Dictionary və ya HashSet kimi kolleksiyalardan istifadə edəndə.

C#-da iki əsas bərabərlik növü var:

Referans bərabərliyi (Reference Equality):

Bu o deməkdir ki, iki referans tipli dəyişən yaddaşda eyni obyektə işarə edir. Bu, referans tipləri üçün == operatoru ilə yoxlanılır.

MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
MyClass obj3 = obj1;

Console.WriteLine(obj1 == obj2); // false (yaddaşda fərqli obyektlər)
Console.WriteLine(obj1 == obj3); // true (hər iki referans eyni obyektə işarə edir)

Dəyər bərabərliyi (Value Equality):

Bu o deməkdir ki, iki fərqli obyekt (və ya iki dəyər tipi) eyni məzmuna (öz sahələrinin/property-lərinin dəyərlərinə) malikdir. Adətən obyektləri müqayisə edəndə bunu istəyirik. Bunun üçün Equals() metodu istifadə olunur.

// Tutaq ki, bizdə Point klası var
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
Point p3 = new Point(30, 40);

// p1 və p2 — fərqli obyektlərdir, amma istəyirik ki, onlar "dəyərə görə bərabər" sayılsın
Console.WriteLine(p1.Equals(p2)); // ? Equals() implementasiyasından asılıdır
Console.WriteLine(p1.Equals(p3)); // ?

2. Equals() metodu

Equals() metodu System.Object bazasında təyin olunub, hansı ki, C#-da bütün tiplər ondan miras alır. Onun əsas məqsədi — iki obyektin dəyərə görə bərabər olub-olmadığını müəyyən etməkdir.

Equals() metodunun default davranışı

Dəyər tipləri üçün (struct, int, bool və s.): Default Equals() implementasiyası (System.ValueType-dən miras alınır) bütün sahələrin bit-bit müqayisəsini aparır. Əgər bütün sahələr bərabərdirsə, obyektlər bərabər sayılır. Bu, adətən gözlənilən kimi işləyir.

Referans tipləri üçün (class, string, array və s.): Default Equals() implementasiyası ( System.Object-dən miras alınır) referans bərabərliyini yoxlayır. Yəni obj1.Equals(obj2) default olaraq yalnız obj1obj2 yaddaşda eyni obyektə işarə edirsə true qaytarır.
class Person // Referans tipi
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 }; // Başqa obyekt, amma eyni məzmun

Console.WriteLine(person1.Equals(person2)); // false (default olaraq referansları müqayisə edir)

Gördüyün kimi, referans tipləri üçün default davranış çox vaxt bizə lazım olan deyil! İstəyirik ki, iki "Alice" 30 yaşı ilə bərabər sayılsın, hətta yaddaşda fərqli obyektlər olsalar belə.

Öz klasin üçün Equals() metodunu override etmək

Obyektlərin dəyərə görə bərabərliyini təmin etmək üçün öz klasin üçün Equals() metodunu override etməlisən.

Equals() override qaydaları (kontrakt):

  • Refleksivlik: x.Equals(x) həmişə true olmalıdır.
  • Simetriya: Əgər x.Equals(y) true qaytarırsa, y.Equals(x)true olmalıdır.
  • Tranzitivlik: Əgər x.Equals(y)y.Equals(z) hər ikisi true qaytarırsa, x.Equals(z)true olmalıdır.
  • Uyğunluq: x.Equals(y) dəfələrlə çağırılanda nəticə dəyişməməlidir, obyektlər dəyişməyibsə.
  • Null uyğunluğu: x.Equals(null) həmişə false olmalıdır.

Nümunə: Person klası üçün Equals() override etmək

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    // Equals() override
    public override bool Equals(object? obj)
    {
        // 1. null yoxla
        if (obj == null) return false;

        // 2. Tip yoxla
        if (obj.GetType() != this.GetType()) return false; 

        // 3. Tipi cast et
        Person other = (Person)obj;

        // 4. Sahələri dəyərə görə müqayisə et
        return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && 
               Age == other.Age;
    }
}

// İstifadə:
Person person1 = new Person("Alice", 30);
Person person2 = new Person("Alice", 30);
Person person3 = new Person("Bob", 25);

Console.WriteLine(person1.Equals(person2)); // true (artıq dəyərə görə müqayisə edir!)
Console.WriteLine(person1.Equals(person3)); // false
Console.WriteLine(person1.Equals(null));    // false

3. GetHashCode() metodu

GetHashCode() metodu da System.Object-da təyin olunub. O, tam ədəd (hash-code) qaytarır, hansı ki, obyekti tez və mümkün qədər unikal identifikasiya edir.

GetHashCode() nə üçün lazımdır?

Hash-code-lar hash-table əsaslı kolleksiyalarda işləməni optimallaşdırmaq üçün istifadə olunur. Belə kolleksiyalara daxildir:

  • Dictionary<TKey, TValue> (hash-code açarın tez tapılması üçün istifadə olunur)
  • HashSet<T> (hash-code elementlərin unikal olub-olmadığını yoxlayır)
  • Hashtable

Obyekti HashSet-ə əlavə edəndə və ya onu Dictionary-də açar kimi istifadə edəndə, kolleksiya əvvəlcə obyektin hash-code-unu hesablayır. Bu, ona imkan verir ki, dərhal müəyyən "basket"ə və ya eyni hash-code-a malik elementlər qrupuna "atlayıb" getsin, bütün elementləri bir-bir yoxlamaq əvəzinə. Sonra isə həmin "basket"də Equals() metodu ilə dəqiq müqayisə aparılır.

GetHashCode() override qaydaları (kontrakt):

Əgər sən Equals() override edirsənsə, GetHashCode() metodunu da MÜTLƏQ override etməlisən! Bu, C#-da ən vacib qaydalardan biridir.

  • Uyğunluq: Əgər Equals() iki obyekt üçün true qaytarırsa, GetHashCode() həmin obyektlər üçün eyni dəyəri qaytarmalıdır. (Əksinə deyil: fərqli obyektlər eyni hash-code ala bilər — buna "kolliziya" deyilir.)
  • Daimilik: GetHashCode() bir obyekt üçün, onun müqayisədə istifadə olunan sahələri dəyişməyibsə, həmişə eyni dəyəri qaytarmalıdır.
  • Sürət: GetHashCode() çox sürətli olmalı və böyük hesablama tələb etməməlidir.

Niyə bu qədər vacibdir? Əgər sən Equals() override etmisən, amma GetHashCode() override etməmisən, kolleksiyalar (xüsusilə hash-kolleksiyalar) səhv işləyəcək:

  • Dictionary sənin açarını tapa bilməyəcək.
  • HashSet dublikatları əlavə edəcək, çünki onları unikal hesab edəcək.

Bu ona görə baş verir ki, default olaraq GetHashCode() obyektin referansına əsaslanan hash qaytarır (referans tipləri üçün). Əgər Equals() artıq dəyərə görə müqayisə edirsə, eyni dəyərə malik, amma fərqli referanslı obyektlər fərqli hash-code alacaq və kolleksiya onları "eyni" kimi görməyəcək.

Person klası üçün GetHashCode() override etmək

Yaxşı praktika — hash-code-u Equals()-da istifadə olunan eyni sahələrə əsasən generasiya etməkdir. .NET-də bunun üçün HashCode.Combine() statik metodu var, çox rahatdır.

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public override bool Equals(object? obj)
    {
        if (obj == null || obj.GetType() != this.GetType()) return false;
        Person other = (Person)obj;
        return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && 
               Age == other.Age;
    }

    // GetHashCode() override
    public override int GetHashCode()
    {
        // HashCode.Combine ilə sahələrin hash-lərini birləşdiririk.
        return HashCode.Combine(Name.ToLowerInvariant(), Age); 
    }
}

// Kolleksiyada istifadə:
public class Program
{
    public static void Main(string[] args)
    {
        Person p1 = new Person("Alice", 30);
        Person p2 = new Person("Alice", 30);
        Person p3 = new Person("Bob", 25);

        HashSet
  
    uniquePeople = new HashSet
   
    (); uniquePeople.Add(p1); uniquePeople.Add(p2); // p2 dəyərə görə p1-ə bərabər sayılır, əlavə olunmayacaq Console.WriteLine($"Unikal insanlar sayı: {uniquePeople.Count}"); // Çıxış: 1 uniquePeople.Add(p3); Console.WriteLine($"Unikal insanlar sayı: {uniquePeople.Count}"); // Çıxış: 2 } } 
   
  

Vacib məqam: GetHashCode()-da, Equals-da case-insensitive müqayisə olunan string sahələr üçün (məsələn, StringComparison.OrdinalIgnoreCase), hash-code da case-insensitive olmalıdır (məsələn, hash hesablamağa qədər Name.ToLowerInvariant() istifadə et). Əks halda Equals() true qaytaracaq (Alice == alice), amma GetHashCode() fərqli dəyərlər qaytaracaq və kontrakt pozulacaq.

4. ==!= operatorlarının overload edilməsi

Klaslar (referans tipləri) üçün == operatoru default olaraq referans bərabərliyini yoxlayır. Onu overload edib, dəyər bərabərliyini yoxlaya bilərsən, Equals() kimi.

== overload qaydaları:

  • Əgər == overload edirsənsə, mütləq != də overload etməlisən.
  • Tövsiyə olunur ki, overload olunmuş == Equals() ilə eyni davranış göstərsin.
  • Həmçinin GetHashCode()Equals() də override edilməlidir, əgər == overload olunursa.
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // Konstruktor, Equals, GetHashCode əvvəlki kimi

    // == operatorunun overload-u
    public static bool operator ==(Person? left, Person? right)
    {
        if (ReferenceEquals(left, null)) // left null-dursa yoxla
        {
            return ReferenceEquals(right, null); // Hər ikisi null-dursa, bərabərdir
        }
        return left.Equals(right); // Yoxsa bizim override olunmuş Equals() istifadə et
    }

    // != operatorunun overload-u (== overload olunanda mütləqdir)
    public static bool operator !=(Person? left, Person? right)
    {
        return !(left == right);
    }
}

// İstifadə:
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
Person p3 = null;
Person p4 = null;

Console.WriteLine(p1 == p2); // true (artıq overload olunmuş == istifadə edir)
Console.WriteLine(p1 == p3); // false
Console.WriteLine(p3 == p4); // true

5. record — avtomatik bərabərlik

C# 9-dan başlayaraq record tipi əlavə olunub. Bu, referans tipidir, amma avtomatik olaraq dəyərə görə bərabərliyi (və Equals(), GetHashCode(), ToString()==/!= operatorlarını) bütün sahə/property-lərinə əsasən override edir. Bu, record-u dəyişməz data obyektləri üçün ideal edir.

public record PersonRecord(string Name, int Age);

// İstifadə:
PersonRecord r1 = new PersonRecord("Bob", 25);
PersonRecord r2 = new PersonRecord("Bob", 25);
PersonRecord r3 = new PersonRecord("Charlie", 40);

Console.WriteLine(r1 == r2); // true (avtomatik dəyərə görə müqayisə!)
Console.WriteLine(r1.Equals(r2)); // true
Console.WriteLine(r1.GetHashCode() == r2.GetHashCode()); // true
Console.WriteLine(r1 == r3); // false

record referans tipi üçün dəyər tipinin davranışını istəyəndə həyatı xeyli asanlaşdırır.

6. Tövsiyələr

Əgər Equals() override edirsənsə, həmişə GetHashCode() da override et! Bu qaydanı pozsan, hash-kolleksiyalarda gözlənilməz davranışlar olacaq.

Equals()GetHashCode() eyni sahələrdən istifadə etməlidir. O sahələr ki, obyektləri "dəyərə görə bərabər" edir, hash hesablamasında da istifadə olunmalıdır.

Dəyişən (mutable) tiplərlə ehtiyatlı ol. Əgər Equals()GetHashCode()-da istifadə olunan sahələr obyekt yaradıldıqdan sonra dəyişə bilərsə, obyektin hash-code-u dəyişə bilər. Bu, hash-kolleksiyalar üçün çox pisdir, çünki obyekt "itə" bilər (onun hash-code-u dəyişir və kolleksiya onu öz "basket"ində tapa bilmir). Hash-kolleksiyalarda açar və ya element kimi dəyişməz (immutable) tiplərdən istifadə etmək daha yaxşıdır.

Dəyişməz data obyektləri üçün record istifadə etməyi düşün. Bu, dəyərə görə bərabərliyin implementasiyasını xeyli asanlaşdırır.

==!= operatorlarını yalnız referans tipləri üçün overload et, əgər həqiqətən lazımdırsa. Dəyər tipləri üçün == artıq dəyərə görə müqayisə edir. Əgər overload edirsənsə, davranışın Equals() ilə uyğun olduğuna əmin ol.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION