.NET içerisinde Değer Tipi ve Referans Tipi

.NET kodunun çalıştırılmasını sağlayan CLR yapısı iki çeşit veri tipini destekler; referans tipi (reference types) ve değer tipi (value types). FCL (Framework Class Library) içerisinde bulunan tiplerin çoğu referans tipi olsa da, programcıların kullandığı tiplerin çoğunu değer tipleri oluşturur. Referans tiplerine her zaman belleğin “Heap” kısmından yer ayrılır ve C#’ın new operatörü de bu nesnenin bellek adresini döndürür (Bellek adresi de nesnenin bitlerini kaynak gösterir). Şunu aklınızda tutmalısınız ki, referans tipleri ile çalışırken bazı performans özelliklerine dikkat etmelisiniz. Şu 4 maddeyi göz önünde bulundurun:

1. Yönetilen heap kısmından bellek ayrılmak zorundadır.

2.Heap bölgesine ayrılmış her nesnenin kendisi ile ilişkili ilave üyeleri mevcuttur.

3.Nesnenin içineki diğer bitler her zaman sınıfa ayarlanır. (Alanlar için)

4.Bir nesneye heap bölgesinden kaynak ayırabilmek bazen Garbage Collection’ı ortaya çıkartabilir.

Her tip referans tipi olsaydı, bir uygulamanın performansı fazlasıyla kayba uğrardı. Düşünün ki, her Int32 tipini kullandığınızda bellekten bir yer tahsis etmek zorundasınız. Ne kadar kötü bir performans alacağınızı farketmişsinizdir. Performans arttırmak için, sıkça tip kullanılan uygulamalarda, CLR “hafif siklet” olarak adlandırdığı değer tiplerini önerir. Değer tipi örnekleri için çoğunlukla belleğin stack bölgesinden yer tahsis edilir (Ayrıca bir referans tipi nesnenin içerisine bir alan olarak gömülebilirler). Bu tipin bir değişkeni, örneğine bir pointer içermez, bu değişken örneğinin kendisinin bir alanını içerir. Değer tipi örnekleri, garbage collector’ın kontrolü altına girmezler ve bu sayede yönetilen heap kısmının üzerindeki yükü azaltır.

.NET Framework SDK dökümanı hangi tiplerin değer tipi, hangi tiplerin referans tipi olduğunu açık bir şekilde gösterir. Bu dökümana baktığımızda sınıf tipleri referans tipleridir. Örneğin; System.Exception, System.IO.FileStream ve System.Random birer referans tipleridir. Diğer taraftan döküman structure (yapı) ve enum tiplerinin değer tipi olduğunu gösterir. Örneğin; System.Int32 yapısı, System.DayOfWeek enum tipi, System.Decimal yapıları birer değer tipidir.

Biraz daha derinlere inmeye çalışırsak, tüm structure yapıların System.ValueType abstract tipinden türediğini görürüz. System.ValueType tipinin de System.Object tipinden türetildiğini görürürz. Tüm enum tipleri System.Enum abstract tipinden türetilmişlerdir ki bu tip te yine System.ValueType tipinden türetilir. CLR ve diğer tüm programlama dilleri enum tiplerine ayrı bir davranış sergilerler. Burada kafanıza şu soru takılabilir. Görüldüğü üzere System.ValueType bir sınıf tipidir ve sınıf tipleri referans tipidir. O halde Nasıl bir değer tipi bir referans tipinden türeyebilir?

Kendi değer tipinizi tanımlamaya çalışırken değer tiplerini temel tip olarak kullanamazsınız. Çünkü tüm değer tipleri sealed (yani o sınıf için türetilme işlemini önler) olarak tanımlıdır. Bu nedenle yeni oluşturmak istediğini referans tipinde ya da değer tipinde bir tip için, temel tip olarak değer tiplerini kullanamazsınız. Bir örnekle açıklamaya çalışırsam; Boolean, Char, Int32, Uint64, Single, Double tiplerini temel tip şeklinde kullanıp yeni bir tip oluşturmak imkansızdır.

Aşağıdaki örnek size referans tipi ve değer tiplerinin farklılığını açıklar:

using System;
namespace Program
{
 //Referans Tipi [Sınıf olduğundan]
 class ReferansTipi
 {
 public int x;
 }

//Değer Tipi [Struct olduğundan]
 struct DegerTipi
 {
 public int x;
 }

public class Program
 {
 public static void Main(string[] args)
 {
 ReferansTipi r1 = new ReferansTipi(); // Yer Tahsisi - Heap
 DegerTipi d1 = new DegerTipi(); // Yer Tahsisi - Stack

r1.x = 5; // Pointer referansı
 d1.x = 5; // Stack içerisinde değişim

Console.WriteLine(r1.x); // 5
 Console.WriteLine(d1.x); // 5
 }
 }
}

Bu kod parçasını çalıştırdığımızda bilgisayarımızın bellek kısmında aşağıdaki resimde görülen bir durum oluşur:

stack-heap

Harika(!) bir çizer olduğumu da bu resimden anlamışsınızdır. Şimdi aşağıdaki kodları yukarıda belirttiğimiz kodun içerisindeki Main() metodunun sonuna ekleyelim ve bellek kısmında nasıl bir değişiklik olduğuna bakalım:

ReferansTipi r2 = r1; // Sadece referansı (pointer) kopyalama

DegerTipi d2 = d1; // Yer Tahsisi - Stack ve üyeyi kopyalama

r1.x = 6; // r1.x ve r2.x değerlerini değiştirir.
d1.x = 7; // d1.x'i değiştirir, d2.x'i değiştirmez.

Console.WriteLine(r1.x); //6
Console.WriteLine(r2.x); //6
Console.WriteLine(d1.x); //7
Console.WriteLine(d2.x); //5

stack-heap1

Bu kod içerisinde bazı satırlara değinmek istiyorum:

DegerTipi d1 = new DegerTipi(); // Yer Tahsisi – Stack

Bu koda direkt olarak baktığımızda belleğin heap bölgesinden yer tahsis edildiği bilgisine varılır. Fakat, C# DegerTipi örneğinin bir değer tipi olduğunu bilir ve bununiçin stack bölgesinden yer ayıracak kodu üretir. C# ayrıca bu değer tipinin içerisindeki tüm alanların sıfıra set edildiğine emin olur. Aynı satır kod şu şekilde de yazılabilir;

DegerTipi d1;

Bu kod aynı şekilde thread kısmındaki stack bölgesinden yer ayırır ve tüm alanları sıfıraatar. Bu iki kod arasındaki tek fark, eğer new operatörünü kullanırsak C# düşünür ki bu örnek ilk kullanıma hazırdır. Hemen bu konuyu daha da açık hale getirelim;

DegerTipi d3 = new DegerTipi(); // Bu iki satır derlenir çünkü
int a = d3.x; // C#, d3'ün alanlarını sıfıra atandığını bilir.

DegerTipi d3; // Bu iki satır derlenimez çünkü C#, d3'ün alanlarını sıfıra
int a = d3.x; // atanmadığı düşünür. Use of possibly unassigned field 'd3' hatası alırız.

Eğer .NET içerisinde kendi değişken tipinizi tasarlamak isterseniz, bu tipin referans tipi mi yoksa değer tipi mi olacağına dikkatle karar vermelisiniz. Bazı durumlarda değer tipleri daha iyi performans verebilir. Eğer aşağıdaki 3 özellikte tasarlamak istediğiniz tip için uygunsa bu tipi değer tipi olarak tasarlamalısınız;

  1. Tip ilkel bir tip olursa (primitive types). Burada bahsetmek istediğim bu tipin içerisindeki alanlarda herhangi bir düzenlemesi gereken üyesi yoksa mantığıdır.
  2. Bu tipin diğer tiplerden kalıtım almasına gerek yoksa.
  3. Bu tipten türetilecek başka tipler yoksa.
    Yeni değişken tipinizin örnek boyutu da burada hesaba katılmalıdır. Çünkü varsayılan olarak, argümanlar metodlara değer ile geçirilirler (pass by value). Böylece değer tipi örneğinin içindeki alan kopyalanır ki bu da performansı azaltır. Tekrardan, geriye bir değer tipi döndüren metodlar, metodu çağıran tarafından bellekte örneğin içindeki alananın kopyası için yer tahsisi yaparlar ki bu da performansı azaltır.

Değer tiplerinin temel avantajı, bir nesne olarak belleğin yönetimli heap kısmında yer kaplamazlar. Tabi ki değer tiplerinin, referans tiplerine göre birkaç sınırlaması mevcuttur. Şimdi bu durumlara bir göz atalım;

1. Değer tipi nesnelerinin iki farklı gösterimi vardır. Boxed ve unboxed. Referans tipleri her zaman boxed formdadırlar.

2. Değer tipleri System.ValueType sınıfından türetilmişlerdir. Bu tip System.Object’te tanımlanan aynı metodları sağlar. Fakat, System.ValueType Equals() metodunu override eder. Böylece iki nesnedeki alanların içindeki değerler eşleşirse geriye true döndürür. Ek olarak, System.ValueType GetHashCode() metodunu da override eder ki bu method bir değer tipi için algoritma kullanarak bir hash kodu üretir.

3.Değer tiplerini temel sınıf (base class) olarak, herhangi bir değer tipi veya referans tipi üretemeyeceğimiz için, bir değer tipine yeni bir sanal (virtual) metod tanımlamamalıyız. Hiçbir metod soyut (abstract) olmamalıdır.

4. Referans tipi değişkenler belleğin heap kısmındaki nesnenin bellek adresini içerirler. Default olarak, referans tipinde bir değişken oluşturulduğunda, null’a atanır ki bunun anlamı bu değişken henüz geçerli bir nesneyi göstermiyordur. Null olarak atanmış bir referans nesnesini kullanmaya çalıştığımızda NullReferanceException hatası alırız. Buna karşı, değer tipi değişkenleri her zaman tanımlandığı tipin değerini içerir ve bu değer tipinin tüm üyeleri 0’a atanır. Değer tipi değişkeni bir referans içermediği için, bir değer tipine ulaşırken NullReferanceException hatası almamız imkansızdır.

5.Bir değer tipi değişkenini başka bir değer tipi değişkenine atadığımızda, alandan alana kopyalama gerçekleşir. Bir referans tipi değişkenini başka bir referans tipi değişkenine atadığımızda sadece bellek adresi kopyalanır.

6.Bir önceki maddeden dolayı, referans tipleri heap bölgesinde aynı nesneye referans olabileceklerinden, bir referans tipindeki değişkenin üzerindeki değişiklik diğer bir referans tipindeki değişkeni değiştirebilir. Diğer taraftan, bir değer tipindeki değişkenin diğer değer tipindeki değişkeni etkilemesi imkansızdır.

Değer tiplerinde Equals() metodunun varsayılan uygulaması olarak bit bit karşılaştırma yapması bazı durumlarda performansı yükseltebilir. Eğer iki adet değer tipi değişkeninizin içerisi pozitif sıfır ve negatif sıfır ise, sırasıyla eşit değil olarak karşılaştırılırlar. Ama Equals() metodunu varsayılan davranışı için override edebilirsiniz. Örneğin;

using System;

namespace Program
{
    public class Program
    {
        struct S
        {
            public double X;
        }

        public static void Main(string[] args)
        {
            var i = new S { X = 0.0 };
            var j = new S { X = -0.0 };

            Console.WriteLine(i.X.Equals(j.X)); // True
            Console.WriteLine(i.Equals(j)); // False
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.