1. Giriş
Təsəvvür elə: sən internet-mağaza üçün sistem yazırsan və sənə lazım olur ki, satılmış bütün unikal məhsul kodlarını saxlayasan. Və ya, daha yaxşısı, sosial şəbəkə üçün app yazırsan və tez yoxlamaq lazımdır ki, bu istifadəçi adı artıq var, ya yox. Bəs böyük bir mətnin içində neçə unikal söz olduğunu hesablamaq istəsən?
Bütün bu hallarda bizə elə bir elementlər toplusu lazımdır ki, hər element yalnız bir dəfə olsun. Bax, burada səhnəyə HashSet<T> çıxır.
HashSet<T> — bu, unikal elementlərin sırasız toplusunu saxlayan kolleksiyadır. Əsas söz — unikal. Əgər sən HashSet-ə artıq olan elementi əlavə etməyə çalışsan, o sadəcə sənin cəhdini görməzlikdən gələcək və dublikatı əlavə etməyəcək. Elə bil "yalnız unikal adamlar üçün klub"dur: artıq içəridəsənsə, ikinci dəfə səni buraxmayacaqlar.
HashSet<T>-in əsas xüsusiyyətləri:
- Unikallıq: Kolleksiyada hər element yalnız bir dəfə olur, buna zəmanət verir.
- Performans: Elementin olub-olmadığını, əlavə və silməni çox tez yoxlayır. Adətən bu əməliyyatlar sabit vaxtda (O(1)) baş verir, elementlərin sayından asılı olmayaraq! Bunu mümkün edən mexanizm hashing-dir.
- Sırasızlıq: List<T>-dən fərqli olaraq, HashSet<T>-də elementlər heç bir xüsusi ardıcıllıqla saxlanmır. İndeks ilə element götürə bilməzsən (məsələn, "beşinci element").
- Hash-table əsaslı: HashSet<T> daxilində elementləri saxlamaq üçün hash-table istifadə edir, bu da ona yüksək performans verir. Hash-table-ların necə işlədiyini indi dərindən izah etməyəcəyik (bu, ayrıca, daha advanced dərsin mövzusudur), amma təsəvvür elə ki, hər element xüsusi ədədi koda (hash) "çevrilir", sonra da onu çox tez tapırlar.
Gəlin müqayisə edək: əgər sən unikal elementləri saxlamaq üçün List<T> istifadə etsəydin, hər dəfə əlavə etməzdən əvvəl bütün siyahını yoxlamalı olardın ki, element artıq var, ya yox. Böyük siyahılar üçün bu, çox yavaş olardı. HashSet<T> isə bunu bir anda edir!
2. HashSet<T> proqramçıya niyə lazımdır?
Unikal kolleksiyaların sirri
Proqramlaşdırmada tez-tez belə tapşırıq olur: elementləri təkrarsız saxlamaq lazımdır. Məsələn, app istifadəçilərinin email-lərini pars edirsən və əmin olmaq istəyirsən ki, dublikat yoxdur. Və ya qovluqdan oxunan unikal fayl adlarını toplamaq istəyirsən. Ən sadə həll — elə bir kolleksiya ki, artıq olanı ikinci dəfə əlavə etməyə imkan verməsin.
Əlbəttə, bunu List<T> ilə də həll etməyə cəhd edə bilərsən, əlavə etməzdən əvvəl əl ilə yoxlayaraq:
var users = new List<string>();
if (!users.Contains("vasya@example.com"))
users.Add("vasya@example.com");
Amma bu həll böyük həcmdə pis işləyir — Contains yoxlaması List-də bütün elementləri gözdən keçirməyi tələb edir, əgər istifadəçilər minlərlədirsə, proqram Windows XP-də köhnə komp kimi yavaşlayır.
HashSet<T> nə edir?
HashSet<T> isə əksinə, zəmanət verir ki, hər element yalnız bir dəfə saxlanacaq. O, hash-table bazasında qurulub (dictionary kimi), yəni əlavə, axtarış və silmə əməliyyatları çox tez baş verir — adətən sabit vaxtda, bütün elementləri gözdən keçirmədən.
3. HashSet<T> ilə işləməyin əsasları
Təyinat və yaradılma
Başlamaq üçün heç bir əlavə kitabxana qoşmağa ehtiyac yoxdur — class artıq System.Collections.Generic namespace-də var.
using System.Collections.Generic;
var emails = new HashSet<string>();
Kolleksiyanı dərhal ilkin dəyərlərlə doldurmaq olar, onları konstruktora ötürməklə:
var fruits = new HashSet<string> { "alma", "banan", "armud", "banan" };
// "banan" iki dəfə var, amma yalnız bir dəfə saxlanacaq!
Elementlərin əlavə olunması
Element əlavə etmək üçün Add metodundan istifadə olunur. Əgər belə element yoxdursa, metod true qaytarır. Əgər artıq varsa — heç nə baş vermir və metod false qaytarır.
bool added = emails.Add("vasya@example.com"); // true, element əlavə olundu
added = emails.Add("vasya@example.com"); // false, artıq var, əlavə olunmadı
Maraqlıdır: Add-ı bir dəyər üçün yüz dəfə çağırsan da — HashSet inciməyəcək, sadəcə təkrarları görməzlikdən gələcək.
Nəzarət: Contains
Elementin olub-olmadığını yoxlamaq üçün Contains metodundan istifadə et:
if (emails.Contains("vasya@example.com"))
Console.WriteLine("Belə email artıq var!");
Elementlərin silinməsi
Silinmə də tez baş verir:
emails.Remove("vasya@example.com");
Əgər element yoxdursa — heç bir problem deyil, metod sadəcə false qaytaracaq.
4. Praktik nümunə
Gəlin kurs boyu inkişaf etdirdiyimiz tələbə CRM-ini bir az çətinləşdirək.
Tələb
Tutaq ki, sistemimizdə hər istifadəçi unikal istifadəçi adına (login) sahib olmalıdır. Yeni istifadəçi əlavə etməzdən əvvəl unikal olub-olmadığını yoxlamaq lazımdır, əgər yoxdursa — xəbər veririk.
Kod nümunəsi
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Unikal loginləri saxlamaq üçün kolleksiya
var userNames = new HashSet<string>();
while (true)
{
Console.Write("İstifadəçi adını daxil et (və ya leave çıxmaq üçün): ");
string name = Console.ReadLine();
if (name == "leave")
break;
if (userNames.Add(name))
{
Console.WriteLine("Ad uğurla əlavə olundu!");
}
else
{
Console.WriteLine("Xəta: bu ad artıq tutulub, başqa birini yoxla.");
}
}
Console.WriteLine("İstifadəçilərin siyahısı:");
foreach (var user in userNames)
Console.WriteLine($"- {user}");
// Diqqət! Çıxışın ardıcıllığı təsadüfi ola bilər.
}
}
Bax belə sadə şəkildə unikalığı təmin etdik. Əl ilə yoxlamağa ehtiyac yoxdur — HashSet özü hər şeyi həll edir.
5. HashSet<T> içərisində necə işləyir? Hash-code nə üçündür?
Bənzətmə: saxlama hüceyrələri
Təsəvvür elə, səndə loginlərlə dolu böyük kart yığını var və 0-dan 1000-ə qədər hüceyrələri olan masa. Hər login üçün funksiya (GetHashCode) ilə hüceyrə nömrəsi hesablanır və kartı ora qoyursan. Əgər kartlar üst-üstə düşürsə — onlar eyni hüceyrəyə düşəcək və tez biləcəksən ki, login artıq var.
GetHashCode funksiyası
HashSet<T> elementləri sadəcə dəyərə görə yox, əvvəlcə onların hash-code-unı GetHashCode() metodu ilə hesablayır. Əksər daxili tiplər üçün (int, string, double və s.) bu artıq optimal şəkildə işləyir.
Maraqlı fakt: Əgər öz class tiplərini yaradıb onları HashSet<T>-də saxlamaq istəsən, mütləq həmin class-larda düzgün müqayisə və unikal kod almaq üçün metodları (Equals və GetHashCode) override et, ki, unikalıq düzgün işləsin. Amma bu barədə — növbəti dərslərdə.
HashSet<T> ilə işləyərkən tipik səhvlər
İnsanlar unikal dəyərlər kolleksiyasından istifadə etməyi öyrənəndə tez-tez belə bir tələyə düşürlər: elə bilirlər ki, HashSet<T> elementləri əlavə etdikləri ardıcıllıqla saxlayır. Elə deyil! Hash-set-lər heç bir ardıcıllığa zəmanət vermir, hər şey təsadüfi ola bilər. Əgər ardıcıllıq vacibdirsə — başqa kolleksiya tipi lazımdır, məsələn, SortedSet<T>, amma bu artıq başqa hekayədir.
İkinci məşhur səhv — indeksdən istifadə etmək cəhdi:
string name = userNames[0]; // Səhv! HashSet<T>-də indeks yoxdur.
Array və ya list-dən fərqli olaraq, burada elementə nömrə ilə müraciət etmək olmur. Elementləri yalnız foreach ilə keçmək olar.
Üçüncü yayılmış qarışıqlıq — hash-set-i fayla serialize və ya saxlamaq istəyəndə olur — ardıcıllıq müəyyən olmadığı üçün proqramı hər dəfə işə salanda elementlər fərqli düzülə bilər.
6. Kümə əməliyyatları: birləşmə, kəsişmə, fərq
HashSet<T> bir sıra metodlar təklif edir ki, onunla işləmək riyazi kümələrlə manipulyasiyaya bənzəyir. Məsələn: birləşmə, kəsişmə, fərq və simmetrik fərq.
Əsasları bunlardır:
| Metod | Nə edir |
|---|---|
|
Hash-set-ə other-dən bütün elementləri əlavə edir. |
|
Yalnız burada və other-də olan elementləri saxlayır. |
|
Cari set-dən other-də olan elementləri silir. |
|
Yalnız burada və ya other-də olan, amma hər ikisində olmayan elementləri saxlayır. |
Nümunə: kəsişmə və birləşmə
Gəlin nümunədə baxaq. İki adlar set-i var:
var groupA = new HashSet<string> { "Anya", "Boris", "Vera" };
var groupB = new HashSet<string> { "Vera", "Gleb", "Dasha" };
// Hər iki qrupda kim var tapırıq
var common = new HashSet<string>(groupA); // kopyalayırıq, yoxsa groupA dəyişər!
common.IntersectWith(groupB);
Console.WriteLine("Hər iki qrupda:");
foreach (var name in common)
Console.WriteLine(name); // "Vera" çıxacaq
// Hər iki qrupun bütün tələbələrini birləşdiririk, heç kim itməsin:
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);
Console.WriteLine("Bütün tələbələr:");
foreach (var name in all)
Console.WriteLine(name); // "Anya", "Boris", "Vera", "Gleb", "Dasha"
7. Əlavə metodlar və xüsusiyyətlər
Count — set-də neçə element olduğunu öyrənirik:
Console.WriteLine(userNames.Count);
Clear — hər şeyi silmək (həyatda CTRL+A, DELETE kimi):
userNames.Clear();
SetEquals, IsSubsetOf, IsSupersetOf — set-lər eynidirmi, biri digərinin içindədir, və s. Yoxlamaq üçün. Bu, "riyaziyyatçı — kim daha güclüdür" tipli oyunlarda (və ya proqramlarda) kömək edir.
if (groupA.IsSubsetOf(groupB))
Console.WriteLine("A qrupundakıların hamısı B qrupunda var");
8. Öz obyektlərini HashSet<T>-də saxlamaq
Yuxarıda qeyd etdiyimiz kimi, standart tiplər artıq düzgün hash hesablayır və bərabərlik üçün müqayisə olunur.
Amma məsələn, istifadəçiləri obyekt kimi saxlamaq istəsək, hansısa əlamətə görə müqayisə etmək lazımdır (məsələn, login):
class User
{
public string Login { get; set; }
public override bool Equals(object obj)
{
if (obj is User other)
return Login == other.Login;
return false;
}
public override int GetHashCode()
{
return Login.GetHashCode();
}
}
// İndi belə etmək olar:
var users = new HashSet<User>();
users.Add(new User { Login = "vasya" });
users.Add(new User { Login = "petya" });
users.Add(new User { Login = "vasya" }); // Əlavə olunmayacaq!
Əgər Equals və GetHashCode metodlarını override etməsən, HashSet bütün obyektləri fərqli sayacaq (login eyni olsa belə), çünki default olaraq yaddaş ünvanını müqayisə edir.
GO TO FULL VERSION