1. Giriş
Təsəvvür elə ki, sənin bir qutun var. Əgər bu qutu içində əşyanı saxlayırsa (məsələn, içində alma olan qutu), bu dəyər tipi kimidir. Məlumat birbaşa həmin "qutuda" (dəyişəndə) saxlanılır.
Digər tərəfdən, təsəvvür elə ki, səndə bir vizitka var və orada ünvan yazılıb. Vizitka ev deyil, sadəcə evin harada olduğunu göstərir. Bu isə referans tipi kimidir. Dəyişən bu halda məlumatı yox, sadəcə "vizitka" – yaddaşda həmin məlumatın ünvanını saxlayır.
Ən sadə nümunələr:
- int x = 5; // Dəyər tipi: x dəyişəni 5 rəqəmini birbaşa "saxlayır".
- string name = "Vasya"; // Referans tipi: name dəyişəni yaddaşda haradasa yerləşən "Vasya" sətrinə referans saxlayır.
Kim kimdir bu həyatda?
Sənin üçün rahat olsun deyə, əsas məlumat tiplərinin hansı kateqoriyaya aid olduğunu ümumi siyahı şəklində təqdim edirəm:
Dəyər tipləri (Value Types):- Primitiv tiplər: int, double, float, bool, char, byte, short, long, decimal və s.
- Strukturlar (struct): struct açarı ilə elan etdiyin bütün istifadəçi strukturları.
- Enumlar (enum): Adlandırılmış sabitlər toplusunu təyin etməyə imkan verən tiplər.
- Sətirlər (string): Sətirlər bəzi özəlliklərə malikdir (dəyişməzdirlər), amma referans tipidir.
- Bütün massivlər: Məsələn, int[], string[], YourCustomClass[].
- Bütün klasslar (class): class açarı ilə elan etdiyin bütün istifadəçi klassları.
- Delegatlar (delegate): Metodlara referansları təmsil edən tiplər.
- İnterfeyslər (interface): İnterfeyslər özləri obyekt deyil, amma interfeys tipli dəyişən həmin interfeysi reallaşdıran obyektə referans saxlaya bilər.
- Siyahılar, lüğətlər və digər kolleksiyalar: Məsələn, List<T>, Dictionary<TKey, TValue>.
- Ümumiyyətlə, struct və enum olmayan hər şey C#-da default olaraq referans tipidir.
2. Dəyişənlərin kopyalanması
Əsas praktik fərq də məhz buradadır. Bir dəyişəni digərinə mənimsədəndə əslində nə kopyalanır?
A) Dəyər tipinin kopyalanması (Value Type)
Dəyər tipini kopyalayanda tam, müstəqil bir kopya yaranır. Bu, sənədi kserokopiya etmək kimidir: birində etdiyin dəyişiklik digərinə təsir etmir.
int a = 10;
int b = a; // b artıq 10-dur, amma bu "öz kopyasıdır"
Console.WriteLine($"Başlanğıc dəyərlər: a = {a}, b = {b}"); // Başlanğıc dəyərlər: a = 10, b = 10
b = 15; // b-ni dəyişirik
Console.WriteLine($"b dəyişəndən sonra: a = {a}, b = {b}"); // b dəyişəndən sonra: a = 10, b = 15
İzah: b dəyişəni 10 dəyərinin öz kopyasını aldı. b 15-ə dəyişəndə, a öz orijinal 10 dəyərində qaldı. Onlar tamamilə müstəqildirlər.
B) Referans tipinin kopyalanması (Reference Type)
Referans tipini kopyalayanda yalnız referans (yəni obyektin yaddaşdakı "ünvanı") kopyalanır. Hər iki dəyişən eyni obyektə işarə edir. Bu, iki adama eyni vizitka vermək kimidir: hər ikisi eyni evin ünvanını bilir. Bir adam evdə nəsə dəyişsə (məsələn, divarı rəngləsə), digər adam da ora gedəndə həmin dəyişikliyi görəcək.
Gəlin bunu massiv nümunəsində aydınlaşdıraq, çünki massivlər referansın "sehrini" çox yaxşı göstərir (sətirlər isə bir az fərqlidir).
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2 və arr1 yaddaşda eyni massivə işarə edir!
// Başlanğıc dəyərlər: arr1[0] = 1, arr2[0] = 1
Console.WriteLine($"Başlanğıc dəyərlər: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");
arr2[0] = 42; // arr2 ilə massiv elementini dəyişirik
// arr2[0] dəyişəndən sonra: arr1[0] = 42, arr2[0] = 42
Console.WriteLine($"arr2[0] dəyişəndən sonra: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}");
İzah: Hər iki dəyişən (arr1 və arr2) yaddaşda eyni massivə referans saxlayır. arr2[0] dəyişəndə, əslində həmin massiv dəyişir və hər iki dəyişəndə də bu dəyişiklik görünür.
Sətirlərlə bağlı nüans (string)
C#-da sətirlər referans tipidir, amma dəyişməz (immutable) olduqları üçün bir az fərqli davranırlar. Yəni, sətir yaradıldıqdan sonra onu dəyişmək olmur. Hər hansı "dəyişiklik" əməliyyatı (məsələn, konkatenasiya, Replace()) əslində yeni sətir yaradır.
string str1 = "Hello";
string str2 = str1; // str2 də str1 kimi "Hello" obyektinə referans saxlayır
// str1 = "Hello", str2 = "Hello"
Console.WriteLine($"Başlanğıc dəyərlər: str1 = \"{str1}\", str2 = \"{str2}\"");
str2 = "Bye"; // Burada YENİ "Bye" obyekti yaradılır və str2 ona referans olur
// str1 = "Hello", str2 = "Bye"
Console.WriteLine($"str2 dəyişəndən sonra: str1 = \"{str1}\", str2 = \"{str2}\"");
İzah: Əvvəlcə str1 və str2 eyni "Hello" obyektinə referans saxlayırdı. str2-yə "Bye" mənimsədəndə, C# köhnə "Hello" obyektini dəyişmir. Sadəcə yeni "Bye" obyekti yaradılır və str2 ona referans olur. str1 isə hələ də köhnə "Hello" obyektinə baxır. Bu, çox vaxt yeni başlayanları çaşdırır.
3. Əsas fərqlərin cədvəli
| Xüsusiyyət | Dəyər Tipi (məsələn, struct, int) |
Referans Tipi (məsələn, class, string, massiv) |
|---|---|---|
| Nə kopyalanır | Məlumatın özü ("kserokopiya" kimi) | Obyektə referans ("yaddaş ünvanı") |
| Kopyalar arasında əlaqə | Yoxdur, kopyalar tam müstəqildir. Birini dəyişmək digərinə təsir etmir. | Bəli, bütün referanslar eyni obyektə baxır. Obyekti bir referansla dəyişsən, hamısı görəcək. |
| null ola bilər? | Yox (yalnız Nullable tiplər, məsələn int?). Həmişə dəyəri var. | Bəli. "Heç nəyə" (null) baxa bilər. null obyektə müraciət etsən, NullReferenceException alacaqsan. |
| Necə elan olunur | struct, həmçinin bütün primitiv tiplər (int, bool və s.), enum | class, interface, delegate, array, string, object |
| Təmizləmə mexanizmi | Scope-dan çıxanda avtomatik stekdən silinir. | Referanslar qalmayanda Garbage Collector tərəfindən silinir. |
4. Tətbiqdə nümunə
Təsəvvür elə ki, istifadəçi anketi üçün sadə konsol tətbiqi yazırsan. İmtahan balı üçün struktur, istifadəçi profili üçün isə klassımız var.
// Dəyər tipi: bal üçün struktur
struct Score
{
public int Points;
public string Grade; // Aydın olsun deyə əlavə etdik
}
// Referans tipi: istifadəçi profili üçün klass
class User
{
public string Name;
public Score ExamScore; // İçində struktur
}
Strukturun kopyalanması (Score)
Score score1 = new Score { Points = 100, Grade = "A" };
Score score2 = score1; // score1-in bütün tərkibi score2-yə kopyalanır
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=100, Grade=A
score2.Points = 88;
score2.Grade = "B";
Console.WriteLine("--- score2 dəyişəndən sonra ---");
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A (dəyişmədi!)
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=88, Grade=B
Nəticə: score1 dəyişməz qaldı. Çünki Score score2 = score1; zamanı score1-in bütün sahələri score2-yə kopyalandı. İndi hər ikisi öz müstəqil məlumat dəstinə sahibdir.
Klassın kopyalanması (User)
User u1 = new User
{
Name = "Anna",
ExamScore = new Score { Points = 95, Grade = "A" }
};
User u2 = u1; // u2 və u1 YADDAŞDA EYNİ istifadəçiyə referans saxlayır!
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}"); // u1: Name=Anna, Score=95
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}"); // u2: Name=Anna, Score=95
u2.Name = "İvan"; // u2 ilə adı dəyişirik
u2.ExamScore.Points = 60; // u2 ilə balı dəyişirik
u2.ExamScore.Grade = "C";
Console.WriteLine("--- u2 dəyişəndən sonra ---");
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}, Grade={u1.ExamScore.Grade}"); // u1: Name=İvan, Score=60, Grade=C
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}, Grade={u2.ExamScore.Grade}"); // u2: Name=İvan, Score=60, Grade=C
Nəticə: u1 də dəyişdi! Çünki u1 və u2 əvvəlcə yaddaşda eyni User obyektinə referans saxlayırdı. u2.Name = "İvan"; kimi dəyişikliklərdə əslində obyektin özünü dəyişirsən. Ona görə u1.Name də dəyişmiş olur. Hətta içindəki ExamScore strukturu da dəyişir, çünki o da həmin User obyektinin bir hissəsidir.
5. Metodlara ötürüləndə nə baş verir
Tiplərin metodlara necə ötürüldüyünü başa düşmək proqramın davranışını proqnozlaşdırmaq üçün çox vacibdir.
Dəyər tipinin metoda ötürülməsi (Value Type)
Dəyər tipi metoda ötürüləndə default olaraq dəyər üzrə ötürülür. Yəni, metod orijinal dəyişənin kopyasını alır. Metod daxilində bu kopyada edilən dəyişikliklər çöldəki orijinala təsir etmir.
void AddTen(int x)
{
Console.WriteLine($"Metodda (dəyişmədən əvvəl): x = {x}"); // Metodda (dəyişmədən əvvəl): x = 5
x = x + 10; // x indi 15-dir, amma bu lokal kopyadır
Console.WriteLine($"Metodda (dəyişəndən sonra): x = {x}"); // Metodda (dəyişəndən sonra): x = 15
// Bu lokal 'x' metoddan çıxanda "öləcək".
}
int num = 5;
Console.WriteLine($"Metoddan əvvəl: num = {num}"); // Metoddan əvvəl: num = 5
AddTen(num);
Console.WriteLine($"Metoddan sonra: num = {num}"); // Metoddan sonra: num = 5 (dəyişmədi!)
Nəticə: num dəyişməz qaldı (5). x isə AddTen daxilində num-un kopyası idi.
Referans tipinin metoda ötürülməsi (Reference Type)
Referans tipi metoda ötürüləndə də default olaraq dəyər üzrə ötürülür, amma burada referansın özü kopyalanır, obyekt yox. Yəni, metod daxilində "vizitka"nın kopyasını alırsan və hər iki referans eyni obyektə baxır.
void RenameUser(User u)
{
// Metodda (dəyişmədən əvvəl): u.Name = "Olga"
Console.WriteLine($"Metodda (dəyişmədən əvvəl): u.Name = \"{u.Name}\"");
u.Name = "Yeni ad"; // 'u' ilə obyektin property-sini dəyişirik
// Metodda (dəyişəndən sonra): u.Name = "Yeni ad"
Console.WriteLine($"Metodda (dəyişəndən sonra): u.Name = \"{u.Name}\"");
}
User user = new User { Name = "Olga" };
// Metoddan əvvəl: user.Name = "Olga"
Console.WriteLine($"Metoddan əvvəl: user.Name = \"{user.Name}\"");
RenameUser(user);
// Metoddan sonra: user.Name = "Yeni ad"
Console.WriteLine($"Metoddan sonra: user.Name = \"{user.Name}\"");
Nəticə: İstifadəçinin adı "Yeni ad" oldu. RenameUser metodu user obyektinə referansın kopyasını aldı və həmin obyektin Name property-sini dəyişdi.
Vacib əlavə: Bəs metod daxilində referans tipli dəyişənə yeni obyekt mənimsətsək nə baş verər?
void ReassignUser(User u)
{
u = new User { Name = "Tam yeni istifadəçi" }; // 'u' indi yeni obyektə baxır
Console.WriteLine($"Metodda (yenidən mənimsədikdən sonra): u.Name = \"{u.Name}\"");
}
User originalUser = new User { Name = "Orijinal istifadəçi" };
Console.WriteLine($"ReassignUser-dan əvvəl: originalUser.Name = \"{originalUser.Name}\"");
ReassignUser(originalUser);
Console.WriteLine($"ReassignUser-dan sonra: originalUser.Name = \"{originalUser.Name}\""); // "Orijinal istifadəçi" - dəyişmədi!
Nəticə: originalUser dəyişmədi! Çünki ReassignUser referansın kopyasını aldı. u = new User(...) ilə metod daxilində lokal u artıq yeni obyektə baxır, amma originalUser hələ də köhnə obyektə referans saxlayır. Bu, çox vacib məqamdır!
6. "Yeni başlayanların göz yaşları": tipik səhvlər və səbəbləri
Referans və dəyər tiplərini başa düşmək yeni başlayanlar üçün çətin ola bilər. Budur, ən çox rast gəlinən səhvlər və yanlış təsəvvürlər:
Massivlərin kopyalanmasında qarışıqlıq: Yeni başlayanlar çox vaxt arr2 = arr1; yazanda müstəqil massiv kopyası yaranacağını gözləyir. Əslində isə bu, sadəcə iki referansdır və hər ikisi eyni massivə baxır. Bu, bir oyuna iki pult bağlamaq kimidir: birində etdiyin hərəkət digərində də görünəcək. Müstəqil massiv kopyası yaratmaq üçün onu açıq şəkildə klonlamaq lazımdır (məsələn, int[] arr2 = (int[])arr1.Clone(); və ya Copy metodlarından istifadə et).
Sətirlərin "referans kimi" dəyişəcəyini gözləmək: string referans tipi olduğuna görə bəzən elə bilirlər ki, massiv kimi dəyişəcək. Amma dəyişməz olduqları üçün hər "dəyişiklik" əslində yeni sətir yaradır. Bu, gözlənilməz nəticələrə səbəb ola bilər və dövrədə çoxlu sətir əməliyyatı edəndə performansı azalda bilər (belə hallarda StringBuilder istifadə etmək daha yaxşıdır).
null-u unutmaq: Dəyər tipləri (nullable tiplər istisna olmaqla, məsələn int?, bool?) heç vaxt null ola bilməz, həmişə dəyəri var. Referans tipləri isə null ola bilər, yəni heç bir obyektə baxmaya bilər. null obyektə müraciət etsən, məşhur NullReferenceException alacaqsan. Referans dəyişənləri istifadə etməzdən əvvəl null-a bərabər olub-olmadığını yoxla.
Kiçik məlumatlar üçün klass əvəzinə strukturdan istifadə etməmək: Bəzən hamı vərdişlə hər şeyi klass kimi elan edir. Amma kiçik, bir-birinə bağlı məlumatlar üçün (məsələn, nöqtə Point { X, Y }, rəng Color { R, G, B }) struktur daha performanslıdır, çünki stekdə saxlanılır və dəyər üzrə kopyalanır, bu da garbage collector-un yükünü azaldır. Amma strukturlar dəyişməz, kiçik olmalı və içində referans tipli sahələr olmamalıdır (yoxsa onlar null ola və ya dəyişə bilər).
GO TO FULL VERSION