Bölüm 1 / 13
Değer ve referans tipleri
stack/heap, boxing/unboxing.
10 dakikalık okuma
Hat 02'nin 4. dersinde stack ile heap'in fiziğini gördün;
bu ders o fiziği C#'ın tip sistemine resmî olarak bağlıyor: hangi tip nerede yaşar,
atama neyi kopyalar, == ile Equals neyi karşılaştırır,
boxing denen gizli maliyet nerede saklanır ve struct'a ne zaman gerçekten ihtiyaç
olur. Mülakatın en klasik C# sorusu bu derstir — "değer tipi ile referans tipi
arasındaki fark nedir" sorusuna vereceğin cevabın derinliği, seviyeni tek başına
ele verir.
01Ne öğreneceksin
- İki tip ailesi: değer ve referans — üyeleri, atama ve parametre semantiği
- "Değer tipleri stack'te yaşar" ezberinin doğrusu: değişkenin yeri tipi değil bağlamı belirler
==,Equals,ReferenceEqualsüçlüsü ve string istisnası- Boxing/unboxing: mekanizması, gizlendiği üç yer, ölçülebilir maliyeti
- struct kararı: ne zaman değer tipi tasarlanır, mutable struct neden tuzaktır
02İki aile: neyi kopyaladığın her şeyi belirler
C#'taki her tip iki aileden birine aittir ve aradaki tek gerçek fark şudur: değişken neyi taşır? Değer tipinde değişken verinin kendisini taşır; referans tipinde değişken, heap'teki nesneye işaret eden referansı taşır. Atama her iki ailede de aynı şeyi yapar — değişkenin içindekini kopyalar — ama içerik farklı olduğu için sonuç dünyalar kadar ayrıdır:
| Değer tipleri | Referans tipleri | |
|---|---|---|
| Üyeler | int, double, decimal, bool, char, DateTime, tüm struct ve enum'lar, Nullable<T> | tüm class'lar, interface, delegate, diziler, string |
| Değişken taşır | verinin kendisini | heap'teki nesnenin referansını |
b = a sonrası | iki bağımsız kopya | aynı nesneye iki referans |
null olabilir mi | hayır (2. derste int? gelecek) | evet — referansın yokluğu |
default değeri | sıfırlanmış veri (0, false, 01.01.0001) | null |
struct NoktaS { public int X; } // değer tipi
class NoktaC { public int X; } // referans tipi
var s1 = new NoktaS { X = 1 };
var s2 = s1; // VERİ kopyalandı — iki bağımsız nokta
s2.X = 99;
Console.WriteLine(s1.X); // 1 — s1 etkilenmedi
var c1 = new NoktaC { X = 1 };
var c2 = c1; // REFERANS kopyalandı — nesne tek
c2.X = 99;
Console.WriteLine(c1.X); // 99 — aynı nesneye bakıyorlar
Metot parametreleri de aynı kuralla çalışır: C# varsayılan olarak argümanı
değerle geçirir — yani değişkenin içindekinin kopyasını. Değer tipinde veri
kopyalanır, metot orijinali değiştiremez; referans tipinde referans kopyalanır,
metot nesnenin içini değiştirebilir ama değişkeni başka nesneye
yönlendiremez. Hat 01'in 8. dersindeki ref/out tam bu noktada devreye
girer: referansın (ya da verinin) kendisini değil, değişkenin adresini
geçirir.
void Sifirla(NoktaC n) { n.X = 0; } // nesnenin içi değişir — dışarıdan görülür
void Kopar(NoktaC n) { n = new NoktaC(); } // yalnız yerel kopya yön değiştirdi — dışarı etkisiz
void Artir(int x) { x++; } // yerel kopya arttı — dışarı etkisiz
03Fizik katı: "değer tipleri stack'te" ezberini düzelt
Hat 02'nin 4. dersindeki adres uzayı haritasını hatırla: stack'te çağrı
çerçeveleri, heap'te new ile açılan nesneler. Yaygın ezber "değer
tipleri stack'te, referans tipleri heap'te yaşar" der — bu cümle eksiktir ve
mülakatta düzeltebilmen beklenir. Doğrusu: değer tipi verisi değişkeninin
bulunduğu yerde yaşar. Değişken bir metodun yereliyse veri stack çerçevesindedir;
ama bir sınıfın alanıysa (field), o sınıfın nesnesiyle birlikte heap'tedir:
class Siparis
{
public int Adet; // int değer tipi ama HEAP'te — nesnenin gövdesinde
public string Not; // referans nesnede, işaret ettiği string ayrıca heap'te
}
void Hesapla()
{
int toplam = 0; // yerel değer tipi → stack çerçevesinde
var s = new Siparis(); // s referansı stack'te, Siparis nesnesi heap'te
}
Aynı incelik dizilerde: int[] bir referans tipidir, dolayısıyla
milyon elemanlı bir int[]'in tüm elemanları heap'te yan yana durur —
"int değer tipi, o hâlde stack'te" çıkarımı burada da yanlış olurdu. Performans
iması Hat 02'den taşıdığın refleks: yerel değer tipleri neredeyse bedavadır (tahsis =
işaretçi kaydırma), heap tahsisi hem pahalıdır hem çöp toplayıcıya (GC) iş çıkarır —
GC'nin mekanizmasını bu hattın 9. dersi (cs-idisposable) açacak.
04Eşitlik üçlüsü: ==, Equals, ReferenceEquals
"İki şey eşit mi?" sorusu C#'ta üç ayrı soruya ayrışır ve karıştıranı üretimde sessiz hatalar bekler:
ReferenceEquals(a, b): tek soru sorar — iki referans aynı nesneyi mi gösteriyor? Değer tipleri için anlamsızdır (her çağrıda kutulanıp farklı çıkar).==operatörü: değer tiplerinde veriyi karşılaştırır. Referans tiplerinde varsayılan davranış referans karşılaştırmasıdır — aynı içerikli iki ayrı nesne==ile eşit değildir. Tip, operatörü aşırı yükleyerek bunu değiştirebilir.Equalsmetodu: varsayılanı==ile aynı hizada başlar ama tipler onu içerik eşitliği anlamına gelecek şekilde ezebilir (override). Değer tiplerinde varsayılanEqualsalan alan karşılaştırır — çalışır ama yansıma (reflection) kullandığı için yavaştır; performans kritik struct'larda elle ezilir veIEquatable<T>uygulanır (5. derste generics ile nedenini göreceksin: boxing'siz karşılaştırma).
var a = new NoktaC { X = 5 };
var b = new NoktaC { X = 5 };
Console.WriteLine(a == b); // False — farklı nesneler
Console.WriteLine(a.Equals(b)); // False — Equals ezilmedi
string s1 = "kalem";
string s2 = "ka" + "lem";
Console.WriteLine(s1 == s2); // True — string == İÇERİK karşılaştırır
String istisnası kafa karışıklığının klasiğidir: string bir
referans tipidir ama == ve Equals'ı içerik karşılaştıracak
şekilde tanımlanmıştır — çünkü metinde istenen doğal anlam budur. 3. ders
(cs-string) bu tipin diğer tuhaflıklarını (değişmezlik, kültür) tek tek açacak;
10. dersteki record ise "içerik eşitliğini derleyici benim yerime
yazsın" diyenlerin aracı olacak.
05Boxing ve unboxing: görünmez kutunun maliyeti
Her tip object'ten türediği için bir int,
object beklenen her yere verilebilir. Ama int'in verisi çıplak 4
bayttır, object ise heap nesnesi bekler. Köprü boxing'dir:
çalışma zamanı heap'te bir kutu nesne tahsis eder, değeri içine kopyalar, referansını
verir. Tersi — kutudan değeri geri çıkarmak — unboxing'dir ve kutunun
tam olarak o tipte olmasını şart koşar:
int sayi = 42;
object kutu = sayi; // boxing: heap tahsisi + kopya
int geri = (int)kutu; // unboxing: tip kontrolü + kopya
long yanlis = (long)kutu; // InvalidCastException! kutu int, long değil
long dogru = (long)(int)kutu; // önce int olarak aç, sonra dönüştür
Kimse kodda bilerek object kutu = sayi yazmaz; boxing'in tehlikesi
gizlendiği yerlerdir:
- Generic öncesi koleksiyonlar:
ArrayList.Add(42)— parametreobject, her ekleme bir kutu. Milyon elemanda milyon heap nesnesi. Generics'in (5. ders) varlık nedeninin yarısı budur. - Interface'e atama:
IComparable c = 42;— interface referans beklediği için değer kutulanır. objectparametreli API'ler:string.Format("{0}", 42)veConsole.WriteLine("{0}", 42)gibiparams object[]alan çağrılar her değer tipini kutular.
Tek boxing ucuzdur; sorun döngüdeki boxing'dir — Hat 02'nin 10. dersindeki refleksle aynı: döngü içindeki her çağrının maliyetini sor. Milyonlarca kutu hem tahsis süresi hem GC baskısı demektir; mini alıştırmada bunu kronometreyle ölçeceksin.
06struct kararı: ne zaman, nasıl, hangi tuzaklarla
Kendi değer tipini struct ile tanımlarsın; ama karar "hangisi modern"
değil, ölçülebilir bir mühendislik takasıdır. Hat 02'nin 4. dersindeki uyarı burada
kurala dönüşüyor — struct'ı şu üç koşul birlikte sağlanınca düşün:
- Küçük: kaba kılavuz ≤16 bayt (3-4 alan). Büyük struct'ın her ataması ve her parametre geçişi tam kopyadır — referans kopyalamaktan pahalıya gelir.
- Değer semantiği doğal: koordinat, tarih aralığı, para tutarı gibi
"kimliği değil içeriği önemli" veriler.
DateTimevedecimal'in struct olması tesadüf değil. - Değişmez (immutable): alanlar kurulduktan sonra değişmiyor. Bunu
derleyiciye
readonly structile söylet.
Mutable struct neden tuzak? Çünkü kopya semantiği, "değiştirdim sandım ama kopyayı değiştirdim" hatasını davet eder. C# derleyicisi en bilinen hâlini yakalar ama hepsini yakalayamaz:
var liste = new List<NoktaS> { new NoktaS { X = 1 } };
liste[0].X = 5; // DERLEME HATASI CS1612: indexer kopya döndürür,
// kopyayı değiştirmek anlamsız — derleyici korur
var dizi = new NoktaS[] { new NoktaS { X = 1 } };
dizi[0].X = 5; // ÇALIŞIR — dizi elemanın kendisine erişim verir
foreach (var n in liste) { /* n.X = 5; yazamazsın — foreach değişkeni salt okunur */ }
Dizi ile List arasındaki bu davranış farkı ("biri derleniyor, öbürü
derlenmiyor") mülakatların sevdiği bir derinlik sorusudur; açıklaması tek cümle:
dizi elemanı doğrudan adreslenir, List indexer'ı ise kopya döndüren bir
metottur. Kural basit: struct yazacaksan readonly struct yaz,
bu soruların tümü kökten kapanır. Varsayılanın ise her zaman class
olsun; struct, ölçtüğün sıcak noktanın optimizasyonudur. 10. ders (cs-record-struct)
bu kararı record ve record struct ile bir kat daha
zenginleştirecek.
07Sahada tip sistemi: kararların özeti
| Durum | Doğru refleks |
|---|---|
| Yeni bir veri tipi tanımlıyorsun | Varsayılan class; küçük + değişmez + değer semantikliyse readonly struct |
| İçerik eşitliği gerekiyor | Equals/== ez ya da record kullan (10. ders); sözlük anahtarıysa GetHashCode ile birlikte |
Döngüde object API'sine değer tipi veriyorsun | Boxing var — generic alternatife geç, ölç |
| Metot, çağıranın değişkenini değiştirmeli | ref/out — varsayılan kopya semantiğini bilinçli kır |
| Büyük struct'ı sık parametre geçiyorsun | Kopya maliyeti referansı geçti — in parametre ya da class'a dön |
Bu tablo hattın geri kalanının da temelidir: 2. ders null'u referans dünyasının sorunu olarak ele alacak, 5. ders generics ile boxing'i kökünden çözecek, 9. ders heap'in temizlikçisi GC'yi masaya yatıracak. "Atama neyi kopyalar?" sorusunu her satırda cevaplayabiliyorsan bu dersin işi tamam.
08Mini alıştırma — Kopyayı ve kutuyu kanıtla
Yaklaşık 20 dakika · dotnet run yeterli, proje kurulumu Hat 00'dan hazır
- Kopya deneyi: Bölüm 02'deki
NoktaS/NoktaCikizini yaz. Dört senaryonun çıktısını önce kâğıda tahmin et, sonra çalıştır: atama + değiştirme (her iki tip), metoda geçirip içini değiştirme, metoda geçiripnewile yeniden atama. - Boxing ölçümü:
Stopwatchile 10 milyonint'i önceArrayList'e, sonraList<int>'e ekle; süreleri veGC.CollectionCount(0)farkını yazdır. Farkın kaynağını bir cümleyle not et (kutu tahsisi + GC baskısı). - Unboxing kuralı:
object kutu = 42;üzerinden(long)kutudene, aldığınInvalidCastException'ı gör; doğru biçimini yaz. - Mutable struct tuzağı:
NoktaS'i hem diziye hemList'e koy, ikisinde de[0].X = 5dene. Hangisi derlenmedi, hata kodu ne, neden? Sonra struct'ıreadonly structyapıp alan atamasının da derlenmediğini gör.
09Kendini sına
Değer ve referans tiplerinde atama sırasıyla neyi kopyalar? İki aileden üçer üye say.
Atama her zaman değişkenin içindekini kopyalar: değer tipinde bu verinin kendisidir — sonuç iki bağımsız kopya; referans tipinde heap'teki nesneye işaret eden referanstır — sonuç aynı nesneye bakan iki değişken. Değer: int, double, decimal, bool, char, DateTime, tüm struct ve enum'lar. Referans: class'lar, interface, delegate, diziler ve string.
"Değer tipleri stack'te yaşar" cümlesi neden eksik? Doğrusunu bir örnekle anlat.
Değer tipi verisi değişkeninin bulunduğu yerde yaşar; yer,
tipi değil bağlamı izler. Metodun yerel değişkeniyse stack çerçevesindedir; ama
bir sınıfın alanıysa nesneyle birlikte heap'tedir — class Siparis { public
int Adet; } içindeki int heap'te durur. Aynı şekilde int[] bir referans
tipi olduğundan elemanları da heap'tedir.
Boxing/unboxing nedir? Kodda görünmeden ortaya çıktığı üç yer say.
Boxing, bir değer tipinin object beklenen yere verilirken heap'te kutu nesneye kopyalanmasıdır; unboxing kutudan değeri geri çıkarır ve kutunun tam o tipte olmasını şart koşar (int kutusunu long diye açmak InvalidCastException). Gizlendiği yerler: generic olmayan koleksiyonlar (ArrayList.Add), interface'e atama (IComparable c = 42) ve params object[] alan API'ler (string.Format("{0}", 42)). Tek başına ucuzdur; döngüde milyonlarca kutu tahsis + GC baskısı olarak faturalanır.
==, Equals ve ReferenceEquals ne zaman farklı sonuç verir? string neden istisna gibi görünür?
ReferenceEquals daima "aynı nesne mi" sorusudur. == değer tiplerinde veriyi, referans tiplerinde varsayılan olarak referansı karşılaştırır; tip operatörü aşırı yükleyebilir. Equals'ın varsayılanı aynı hizadadır ama içerik eşitliği için ezilebilir — değer tiplerinde varsayılanı alan alan karşılaştırır fakat yansıma yüzünden yavaştır. string referans tipidir ama == ve Equals içerik karşılaştıracak şekilde tanımlanmıştır; bu yüzden aynı içerikli iki string == ile eşittir, aynı içerikli iki sıradan nesne değildir.
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Çıktı tahmini — en sık kalıp: değer/referans atayıp birini değiştiren
kod parçası ("ekrana ne yazar?"); metoda parametre geçirip içeride değiştiren
varyantı (ref'siz swap çalışmaz — cevap: değişmez). (2) Sınıflandırma:
"Aşağıdakilerden hangisi değer tipidir?" şıklarında string tuzağı — string
referans tipidir; DateTime, decimal, enum değer tipidir. (3) Tanım:
boxing/unboxing açılımı, "object o = 5; işleminin adı nedir?"
(4) Kavram: stack/heap eşlemesi — Hat 02'nin 4. dersiyle birleşik sorulur;
"new ile açılan nesne nerede tutulur?" (heap), "yerel int nerede?" (stack).
Sözlüde nasıl anlatılır — 2 dakikalık taslak
"Değer tipi ile referans tipi farkı nedir?" için düzen: (1) Tek cümlelik çekirdek — "değişkenin taşıdığı şey farklıdır: biri veriyi, öbürü heap'teki nesnenin referansını taşır; atama bu yüzden birinde bağımsız kopya, öbüründe paylaşılan nesne üretir." (2) Fizik derinliği — "değer tipleri stack'te ezberini düzelterek anlatırım: sınıf alanı olan int nesneyle birlikte heap'tedir" — bu düzeltme, ezbercilerden ayrıştıran hamledir. (3) Boxing köprüsü — "iki dünya object üzerinden buluşunca boxing doğar; ArrayList çağındaki maliyetin çözümü generics oldu" diyerek dilin evrimini bildiğini göster. (4) Karar kapanışı: "Kendi tipimi tasarlarken varsayılanım class; küçük, değişmez, değer semantikli veride readonly struct'a geçer, kararı ölçümle doğrularım."
