DEV Community

sadık emre
sadık emre

Posted on

C#’ta Parametre Geçişi: Referans Tiplerde Gözden Kaçan Nokta

İki referans aynı adresi tutuyor

Bir metoda string ya da bir nesne parametre olarak verilip metot içinde bu parametreye yeni bir değer atandığında, çoğu zaman çağıran taraftaki değişken de değişmiş sanılır. Aşağıdaki örnek bu varsayımı çürütüyor — ve çürüme sebebi, çoğu geliştiricinin sandığından farklı.

Önce kafa karıştıran örnek

void TestMetot(string A)
{
    A = "Naber!";
}

string A = "Selam";
TestMetot(A);
Console.WriteLine(A); // Selam
Enter fullscreen mode Exit fullscreen mode

Metot içinde A'ya açıkça "Naber!" atanmasına rağmen çıktı "Selam". Derleyici için burada iki bağımsız A var: metot dışındaki A ve parametredeki A. Bunlar aynı isme sahip ama birbirinden habersiz iki ayrı kutu (değişken).

string A = "Selam" çalıştığında ortaya çıkan tablo:

Stack:
  metot dışındaki string A, RAM'de 0x100 bellek adresini tutar

Heap:
  0x100 adresine karşılık gelen değer "Selam"dır.
Enter fullscreen mode Exit fullscreen mode

TestMetot(A) çağrıldığında, parametredeki A adında yeni bir kutu açılıyor ve bu kutuya metot dışındaki A'nın tuttuğu adres (0x100) kopyalanıyor. A = "Naber!" çalıştığında ise sadece bu yeni kutu güncelleniyor, metot dışındaki A hiç etkilenmiyor.

Asıl soru: neden ikinci bir referans oluşuyor?

Buradaki gerçek soru "neden string beklenmedik davranıyor" değil. Asıl soru şu: metoda bir referans verildiğinde, C# neden onu olduğu gibi kullanmak yerine bir kopyasını çıkarıyor?

Cevap: Bu, string ya da başka bir reference type'a özgü bir istisna değil — C#'ta parametre geçişinin varsayılan ve tek kuralı bu. Metoda ne verilirse verilsin (sayı, string, nesne, fark etmez), çağıranın kutusunun kendisi değil, o kutunun o anki içeriğinin bir kopyası metoda girer. int için kopyalanan içerik doğrudan sayının kendisi, string/List için kopyalanan içerik bir adres — ama kopyalanma kuralı ikisinde de birebir aynı. Yani "referans tipi geçince otomatik bağlantı kurulur" diye bir istisna hiç yok; asıl yanlış varsayım burada.

Bu tasarımın bir sebebi var. Eğer metoda bir referans verildiğinde metot, çağıranın kutusunun kendisine erişebilseydi, şöyle bir risk her zaman var olurdu:

void Guncelle(List<int> A)
{
    A = new List<int>(); // çağıranın değişkenini TAMAMEN BAŞKA bir nesneye çevirebilirdi
}
Enter fullscreen mode Exit fullscreen mode

Bu otomatik yansısaydı, herhangi bir metoda bir nesne verildiğinde o metodun, çağırandaki değişkeni sessizce başka bir nesneye yönlendirme ihtimali her zaman var olurdu. Kodu okuyan biri her çağrıda "bu metot benim değişkenimi başka bir şeye çevirmiş olabilir mi?" diye düşünmek zorunda kalırdı.

Kutunun kopyalanması bu riski ortadan kaldırıyor. Metot kendi parametresini istediği gibi başka adreslere yönlendirebilir, ama bu sadece kendi kopyasını etkiler. Çağıranın hangi nesneye baktığı garanti altında kalır — metodun çağırandaki değişkeni gerçekten yönlendirebilmesi isteniyorsa, bu ref/out ile açıkça talep edilmek zorunda. "İkinci referans" bir yan etki değil, bilinçli bir güvenlik/öngörülebilirlik tercihi.

int için de aynı kural işliyor

void TestMetot(int A)
{
    A = 99;
}

int A = 5;
TestMetot(A);
Console.WriteLine(A); // 5
Enter fullscreen mode Exit fullscreen mode

Heap araya girmiyor ama mekanizma birebir aynı: parametredeki A kutusu açılıyor, metot dışındaki A'nın değeri kopyalanıyor, A = 99 sadece kopyayı değiştiriyor. Bu örnek genelde şaşırtmaz, çünkü value type olduğu bilinir — asıl fark string örneğinde "reference type olmasına rağmen" aynı sonucun çıkmasıdır.

Bazı örnekler neden çalışıyor gibi görünüyor?

void TestMetot(List<int> A)
{
    A.Add(5);
}

var A = new List<int>();
TestMetot(A);
Console.WriteLine(A.Count); // 1
Enter fullscreen mode Exit fullscreen mode

Burada da iki ayrı kutu var, ikisi de aynı adresi tutuyor. Fark, metot içinde yapılan işlemde: A.Add(5) kutunun içeriğini (adresi) değiştirmiyor, kutunun tuttuğu adresteki nesnenin kendisini güncelliyor. İki kutu hâlâ aynı adresi tuttuğu için değişiklik her iki taraftan da görünür.

  • Mutation (nesnenin içini değiştirmek): A.Add(...), sb.Append(...) → kutunun içeriğine dokunulmaz, otomatik yansır.
  • Reassignment (kutunun içeriğini değiştirmek): A = "Naber!", A = new List<int>() → kutu başka bir adres tutmaya başlar, yansımaz.

string immutable olduğu için (Add, Remove gibi mutation işlemi yoktur) üzerinde yapılan her işlem zorunlu olarak reassignment'tır — bu yüzden string ile bu tuzağa düşmek çok daha kolaydır.

ref/out ne zaman gerekir?

void TestMetot(ref string A)
{
    A = "Naber!";
}

string A = "Selam";
TestMetot(ref A);
Console.WriteLine(A); // Naber!
Enter fullscreen mode Exit fullscreen mode

ref verildiğinde metot ayrı bir kutu açmaz, doğrudan çağıranın kutusuna erişir — o yüzden reassignment de yansır. out aynı mekanizmayı kullanır, tek farkı: çağıranın başlangıç değeri vermesi gerekmez ama metodun out parametresine mutlaka bir değer ataması zorunludur, aksi hâlde derleme hatası alınır.

Neden önemli?

Bu davranış hiçbir exception fırlatmaz, hiçbir uyarı vermez. Bir metodun bilgi hesaplayıp parametre üzerinden geri döndürmesi beklenen her yerde — loglama, id üretme gibi yardımcı metotlarda — sonuç sessizce hep boş/varsayılan gelir ve fark edilmesi haftalar sürebilir.

İlgili konular

  • StringBuilder: string'in mutable karşılığı, .Append() mevcut nesneyi günceller.
  • in parametresi: büyük struct'ları kopyalamadan, salt-okunur geçirmek için.
  • Closure'larda değişken yakalama: lambda'lar dış değişkeni kutu olarak yakalar, değeri değil — aynı kök mantık.
  • record ve with: immutable tiplerde "değiştirme" aslında yeni nesne üretir, string'in Substring'ine benzer.

Top comments (0)