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() və 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 obj1 və obj2 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) də true olmalıdır.
- Tranzitivlik: Əgər x.Equals(y) və y.Equals(z) hər ikisi true qaytarırsa, x.Equals(z) də 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. == və != 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() və 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() və ==/!= 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() və 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() və 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.
== və != 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.
GO TO FULL VERSION