C# ile Reference ve Value Type Klonlama | Deep Copy | Shallow Copy | Immutable Type

Tolgahan İnan
Arvato Tech
Published in
8 min readFeb 20, 2024

Merhaba, bu yazımda referans ve value tiplerinin farkına, bu tipler baz alınarak oluşturulan değişkenlerin klonlanması esnasında oluşan durumlara, deep copy, shallow copy ve immutable type kavramlarına değiniyor olacağım.

Value Type | Reference Type Nedir

“C#” ve diğer gelişkin programlama dillerinde, verilerimizi saklayabilmek adına değişkenler oluşturur ve bu değişkenler üzerinden işlemlerimizi gerçekleştiririz. “C#” tutulmak istenen verinin tipine göre (numerik,sözel…) ön tanımlı değişken tipleri içermektedir.

İlgili değişkenlerin listesine ve alabildiği değer aralıklarına aşağıdaki tablodan erişebilirsiniz.

Tablo içerisindeki veriler incelendiğinde “string” ve “object” değişken tipleri için ön tanımlı bir değer aralığı olmadığını gözükmektedir.

Bu durumun nedeni ilgili değişken tipleri için dinamik hafıza alokasyonu yapılmasıdır. Bir örnek üzerinden açıklayacak olur isek ;

Tanımlanabilecek herhangi bir “int (tam sayı)” tipinde değişken için verebileceğiniz en yüksek değer “2147483647” sayısıdır. Eğer bu sayıdan daha büyük bir sayı içeren herhangi bir değişken tanımlamak isterseniz veri tipini değiştirmeniz gerekmektedir (“long”), aksi taktirde “int” tipinde tanımlanmış bir değişkene, limit dışı bir değer ataması yapamayacaksınızdır. Dolayısıyla bu değişkeni hafızada tutabilmek için her daim belirli bir bellek alanı yeterli olacaktır (32 bit).

Öte yandan herhangi bir “string (metin)” tipinde bir değişken için tanımlayacağınız metinde herhangi bir karakter limiti yoktur. Tanımlanan metnin içeriği, ilgili değişkenin hafızada tuttuğu alanı değiştirecektir. Örneğin “Medium” kelimesi ile “MediumButDumpTextAdded” kelimesinin string bir değişkene atandığında kullanacağı bellek alanı farklı olacaktır. Burada atanan değere göre bellekte ayrılan yer miktarı dinamik olarak değişmektedir “dynamic memory allocation”.

Daha detay bilgi isteyenler için bir string değişkeni, birden fazla “char” tipinde değişkeninin birbirine linked list benzeri bir yapı ile bağlanmış versiyonudur. Dolayısı ile karakter sayısı arttıkça eklenen her bir karakter için bellek tüketimi artacaktır.

Peki, tüm bu bilgiler ışığında, Value Type ve Reference Type nedir.

Value Type : Bellekte tutacağı alanın boyutu ön tanımlı olan, değişkene atanan değerin belleğin “Stack” denilen bölgesinde tutulduğu değişken türleridir.

“byte”, “int”, “long” değişken tipleri birer “Value Type” örneğidir.

Reference Type : Bellekte tutacağı alanın boyutu ön tanımlı olmayan, tanımlanan verinin içeriğine göre dinamik bir şekilde bellek alokasyonu sağlayan, değişkene atanan değerin belleğin “Heap” denilen bölgesinde tutulduğu değişken türleridir.

“string”, “object”, “class”, “delegate” değişken tipleri birer “Reference Type” örneğidir.

Bir örnek üzerinden value ve reference tiplerin bellek üzerinde nasıl tutulduğunu açıklayacak olur isek ;

Aşağıdaki görselde bir metot içerisinde iki adet “int” tipinde yani “Value Type” değişken tanımlanmakta. Bu değişkenlere ait değerler direkt olarak belleğin “stack” bölgesinde tutulmakta ve belleğin stack bölgesinden direkt olarak değişkene atanan değere erişilebilmektedir. Ancak “Obj” tipinde tanımlanan “Reference Type” bir değişken için “Stack” bölgesinde ilgili değişkenin “Heap” bölgesinde tanımlanan değerine erişebilmek için özel olarak tanımlanmış bir bellek adresi bulunmaktadır. Ancak bu bellek adresi kullanılarak ilgili değişkene atanan değere erişilebilmektedir.

C# IClonable Interface | Nesne Klonlama

Oluşturduğumuz herhangi bir değişken üzerinden işlem yapmak istediğimiz ancak işlem sonucunda değişkeninin içeriğinin değişmesini istemediğimiz bir senaryo varsayalım.

Bu durumda ilgili değişkenin birebir bir şekilde klonunu oluşturarak, klon değişken üzerinde değişiklikler yapıp, ana değişkenimizin düzenlemelerden etkilenmemesini sağlayabiliriz. Buradaki klonlama işlemini kompleks değişken türlerinde hızlı bir biçimde gerçekleştirebilmek için yardımımıza “ICloneable” ara yüzü (interface) yetişmekte. Bu ara yüzü uygulayan herhangi bir değişken türü, ilgili ara yüze ait “Clone” metodunu kalıtmakta, ve ilgili değişkenin nasıl klonlanacağına dair düzenlemeleri bu metot içerisinde belirtebilmektedir.

Bir örnek üzerinden ilerleyecek olur isek, kendimize bir değişken tipi oluşturarak, bu değişkenin klonladığımız ve oluşan klon değişken üzerinde düzenlemeler yaptığımız bir C# kodu yazalım.

Bu örneği gerçekleştirebilmek adına oluşturacağımız değişkenin bir e-ticaret siparişine ait bilgileri tutan bir sınıf üzerinden oluşturulduğundu varsayalım. İlgili sınıfın “sipariş edilen ürün kodu” , “sipariş edilen ürün miktarı”, “alıcı adı” ve “alıcı adresi” bilgilerini içerdiğini varsayalım.


public class Order : ICloneable
{
public string ProductCode { get; set; }
public int Quantity { get; set; }
public Customer Customer { get; set; }

public Order(string productCode, int quantity, Customer customer)
{
this.Customer = customer;
this.ProductCode = productCode;
this.Quantity = quantity;
}

public object Clone()
{
throw new NotImplementedException();
}
}
// Müsteriye ait bilgiler müsteri özelinde oluşturulan yeni bir sınıf
// içerisinde tanımlanarak siparis özelinde oluşturulmus sınıf icerisinde
// tanımlanarak kullanılmıştır.
public class Customer
{
public string Name { get; set; }
public string Address { get; set; }

public Customer(string name, string address)
{
this.Address = address;
this.Name = name;
}
}

Yukarıda bulunan kod bloğu baz alındığında sipariş sınıfının üç farklı değişken tipi içerdiği görülmektedir. Bunlar “string”, “int” ve “class” tipleridir. Bu sınıf için birden fazla değişken türü içeren kompleks bir değişken diyebiliriz.

Ayrıca sipariş sınıfının “ICloneable” ara yüzünü uyguladığı ve “Clone” metodunu henüz implemente etmediğini görüyoruz. Bu metodu aşağıdaki şekilde özelleştirir isek ;

     public object Clone()
{
//İlgili sınıfa ait klon metodu çağırıldıgında ilgili degisken
//türünde bir obje üret ve MemberwiseClone() metodunu kullanarak
//kopyalanacak sınıfa ait tüm değişkenleri tek tek (memberByMember)
//yeni üretilen obje içerisine kopyala
Order clone = (Order)this.MemberwiseClone();
return clone;
}

Sipariş sınıfımızın nasıl klonlanacağını da özelleştirdiğimize göre artık klon sipariş objeleri üretebiliriz. Bunun için aşağıda bulunan kod bloğu çalıştırılabilir.

    static void Main()
{

// Bir sipariş objesi oluşturuyoruz
Order originalOrder = new Order("ProductCode-XXX", 3, new Customer("Tolgahan Inan", "Gungoren / Istanbul"));
// Oluşturduğumuz sipariş objesinin klonlanmış halini,
// yeni olusturulan klonSiparis objesi içine atıyoruz
Order clonedOrder = (Order)originalOrder.Clone();
}

Artık elimizde üzerinde düzenleme yapabileceğimiz bir klon sipariş objemiz bulunmakta. Ancak bu obje üzerinde değişiklik yaparken dikkatli olunmalıdır. Çünkü “Memberwise.Clone()” metodu ile klonlanan siparişe objesine ait değişkenler “Shallow Copy (Sığ kopyalama)” işlemine maruz kalmıştır. Peki bu ne demek ve neden dikkatli olunmalı.

Bu durumu kod örneği ve çıktılar üzerinden değerlendirelim.

Order originalOrder = new Order("ProductCode-XXX", 3, new Customer("Tolgahan Inan", "Gungoren / Istanbul"));
Order clonedOrder = (Order)originalOrder.Clone();

// Konsola henüz bir değişiklik yapmadan orjinal ve klon nesnemizi bastırıyoruz
Console.WriteLine(JsonConvert.SerializeObject(originalOrder));
Console.WriteLine(JsonConvert.SerializeObject(clonedOrder));
// Output iki obje için de aynı olacaktır.
/*{
"ProductCode": "ProductCode-XXX",
"Quantity": 3,
"Customer": {
"Name": "Tolgahan Inan",
"Address": "Gungoren / Istanbul"
}
}*/

// Simdi klon objemiz üzerinde düzenlemeler yapalım
clonedOrder.ProductCode = "ProductCode-YYY";
clonedOrder.Quantity = 5;
clonedOrder.Customer.Name = "Tolgahan";
clonedOrder.Customer.Address = "Fatih / Istanbul";

// Klon objemizi düzenledikten sonra orjinal objemiz ve clon objemizi tekrar
// Console üzerinde gösterelim

Console.WriteLine(JsonConvert.SerializeObject(originalOrder));
// Output | OriginalOrder | Düzenleme Sonrası
/* {
"ProductCode": "ProductCode-XXX",
"Quantity": 3,
"Customer": {
"Name": "Tolgahan",
"Address": "Fatih / Istanbul"
}
}*/
Console.WriteLine(JsonConvert.SerializeObject(clonedOrder));
// Output | CloneOrder | Düzenleme Sonrası
/* {
"ProductCode": "ProductCode-YYY",
"Quantity": 5,
"Customer": {
"Name": "Tolgahan",
"Address": "Fatih / Istanbul"
}
}*/

Yukarıdaki örnek incelendiğinde Klon siparişimizde yaptığımız değişikliklerin, bazı değişkenler özelinde orijinal siparişimizi de etkilediğini görmekteyiz. Orijinal sipariş üzerindeki “ProductCode (string-ReferenceType)” ve “Quantity (int-ValueType)” değişkenleri klon üzerinde yapılan düzenlemeden etkilenmez iken, “Customer (class-ReferenceType)” değişkeni klon üzerinde yapılan düzenlemeden etkilenmiş görünüyor. Bu durumun nedeninden ilgili kavramlara da değinerek aşağıda açıklayalım.

Deep Copy | Shallow Copy | Immutable Types Nedir

Deep Copy (Derin kopyalama) : Bu yaklaşım ile kopyalanan değişkenlere ait bellekte tutulan değerler, kopyalandığı değişkenin üzerine aktarılır. Kopya (klon) değişken bir “Reference Type” olsa dahi, değişkene ait değere erişmek için kullanılan adres referansı, orijinal değişkene ait adres referansından farklı olacaktır. Dolayısı ile orijinal ya da kopya değişken üzerinde yapılan düzenlemeler birbirini etkilemeyecektir.

Shallow Copy (Sığ kopyalama): Bu yaklaşım ile kopyalanan değişkenlere ait bellekte tutulan adres referansı, kopyalandığı değişken üzerine aktarılır. “Value Type” değişkenler için adres referansı direkt olarak değişkene ait değer ile eşleşirken, “Reference Type” değişkenler için direkt olarak adres referansı baz alınır.

Value ve Reference tiplerini açıkladığımız kısımdaki görsel baz alınır ise, Shallow Copy yaklaşımı ile kopyalan “int (value type)” tipindeki bir değişken için, değişkene ait değer örneğin “int a = 10” atamasındaki “10” değeri kopyalanır iken, “class (reference type)” tipindeki bir değişken için bu değişkene ait değerlerin içerildiği bellek bloğunu gösteren adres kopyalanır, örneğin “Customer customer = new Customer(“Tolgahan Inan”, “Gungoren / Istanbul”)” değişken tanımı için “AE479A0E (random memory adress)” gibi bir adres gösterici kopyalanır. Bu durum “Value Type” değişkenlere ait değerlerin “Stack”, “Reference Type” değişkenlere ait değerlerin “heap” üzerinde tutulması ile alakalı bir durumdur

Dolayısıyla, Shallow Copy yaklaşımı ile kopyalanmış bir “Reference Type” üzerinde değişiklik yapıldığında orijinal değişken de bu değişiklikten etkilenecektir. Ancak “Value Type” değişkenler için böyle bir durum söz konusu değildir.

Yukarıda kodunu verdiğimiz sipariş — klon sipariş örneğine geri dönecek olur isek ;

Sipariş Class’ı kendi içerisinde 3 farklı tipte değişken içermekte ;

1-) ProductCode (string -Reference Type)

2-) Quantity (int -Value Type)

3-) Customer (class- Reference Type)

Kod örneği üzerinde sipariş klonlanarak oluşturulan klon Sipariş değişkeni üzerinde yapılan değişikliklerin “Customer” değişkeni için orijinal sipariş değişkeninin de değerini etkilediğini ancak “ProductCode” ve “Quantity” için ise değişkenin değerinin aynı kaldığını görüyoruz.

“MemberWise.Clone()” metodunun “Shallow Copy” yaklaşımı ile çalıştığını söylemiştik, bu durumda orjinal siparişimizin içerdiği “Reference Type” değişkenleri için, klon siparişimiz üzerinde yapılacak herhangi bir düzenlememizin orijinal siparişimizi de etkilemesini bekleriz (Bellek referansları aynı olduğu için). Burada klon siparişimiz üzerinde yapılan bir değişikliğin hiç bir şekilde orijinal siparişi etkilemesi için “Deep Copy” yaklaşımı sergilenmelidir.

Aşağıda verilen kod bloğu içerisinde “Clone” metodunun “Deep Copy” yaklaşımı ile düzenlenmiş haline erişebilirsiniz.

    public object Clone()
{
Order clone = (Order)this.MemberwiseClone();

/*
Oluşacak olan klon siparise ait Customer değerini yeniden
birebir aynı değerleri kullanılarak üreterek, klon siparisimize
yeni üretilen bellek referansı farklı Customer değişkenimizi veriyoruz
*/
clone.Customer = new Customer(this.Customer.Name, this.Customer.Address);

return clone;
}

Burada akıllara şu sorunun gelmesi oldukça doğal : “string” de bir Reference Type idi, klon sipariş üzerinde “ProductCode (string)” alanında yaptığımız değişiklik orjinal siparişi neden etkilemedi de Customer(class) alanında yaptığımız değişiklik etkiledi? Ya da clone metodu Deep Copy yaklaşımı benimsenerek güncellenir iken neden "ProductCode" alanı için herhangi bir işlem yapılmadı?

Sorunun yanıtı string değişken tipinin C# özelinde bir immutable olmasında gizli

Immutable Types : Immutable veri tipleri için en genel manada değiştirilemez veri tipleri diyebiliriz, bir değişken immutable bir türde ise, bu değişkene ait değer değiştirilemez.

Ama biz string olarak tanımladığımız bir değişkenin değerini değiştirebiliyoruz, ancak sen string için immutable bir tip dedin

Aslında string bir değişken için C# üzerinde düzenleme yaptığınızda, C# arka planda yeni bir string nesnesi oluşturur, yapılan düzenlemedeki değeri bu nesne üzerine aktarır ve değişkenimiz bu yeni nesneyi referans edinir.

Örneğimiz üzerinden görselleştirecek olur isek ;

Detaylandırmak gerekirse, string bir değişken “Reference Type” olmasına rağmen Shallow Copy işlemine uğrayıp, ilgili değişkenin değerine ait adres referansı, klonlanan değişkene taşınmasına karşın, bu değişken üzerinde yapılan herhangi bir değişiklik string tipinin immutable type olmasından ötürü bellekte (heap) yeni bir alanda tutulacak ve adres referansının güncellenmesini sağlayacaktır.

Örneğimizde oluşturduğumuz klon sipariş’e ait “ProductCode (string)” alanını düzenlediğimizde, eş zamanlı olarak bu alana ait adres referansını da güncellediğimiz için, orjinal sipariş ve klon sipariş aynı adres referansını göstermeyerek, birbirleri üzerindeki değişikliklerden “ProductCode (string)” alanı için etkilenmez hale geldiler.

Bu yazımda sizlere C# da Value Type, Reference Type, Obje klonlama, Deep Copy ve Shallow Copy ve Immutable Type kavramlarından bahsetmeye çalıştım, umarım açıklayıcı olmuştur.
Görüş ve önerileriniz için benimle aşağıdaki LinkedIn hesabı üzerinden iletişim kurabilirsiniz.
Tolgahan İnan | LinkedIn

--

--

Tolgahan İnan
Arvato Tech

Software Developer who tends to learn by mistaeks.