Herkese merhaba. C# üzerinde bool, integer, string gibi pek çok değişken türü sayabiliriz. Bu değişken türleri aslında değer (value) ya da referans (reference) tip dediğimiz veri tiplerinden türerler. Değer ve referans tipler bellekte farklı bölgelerde tutulmaktadır. .NET Framework bellek yönetimini önemli ölçüde kendisi yapar. Biz de tanımladığımız değişkenin hangi bölgede yer almasını istiyorsak bellek yönetimini belli ölçülerde kendimiz ayarlamış oluruz. C# üzerindeki bellek bölgelerini inceleyerek konumuza giriş yapalım. Örneklere github hesabımdan ulaşabilirsiniz.

C# Bellek Bölümleri

Daha önceden değişken tanımlama yazımda da belirttiğim gibi; C# belleği stack ve heap olmak üzere iki farklı bölgeye ayırır. Bu iki bölge de RAM üzerinde tutulmaktadır. Stack ve heap bölgelerinin arasındaki farkları aşağıdaki gibi belirtebiliriz;

  • Stack statik bellek tahsisi için kullanılırken, heap dinamik bellek tahsisi için kullanılır.
  • Stack bellek yönetimi LIFO (Last In First Out) sırasını takip eder. Değişkenleri depolayarak yeni değişkenler için alan oluşturur. Heap ise herhangi bir sıra takip etmez. Sadece bellek bloklarını dinamik olarak depolar.
  • Stackte değişken tanımlandığı anda depolanmaya başlar. Heap bölgesinde ise nesne oluşturuldukça bellekte depolanmaya başlar.
  • Stack, kodda çalıştırılan değişkeni yürütmekten sorumludur. Öte yandan heap, nesneleri ve içerdiği tüm dataları takip eder.
  • Stack bölgesinde tutulan değişkenlere erişim daha hızlıyken heap bölgesine erişim daha yavaştır.

Ram Bölümlenmesi

Değer tipli değişkenler stack bölgesinde tutulurken, referans tipli değişkenler heap bölgesinde tutulurlar.

C# Değer ve Referans Tipler

Değer Tipler

Struct ve enumdan türetilen tiplerdir. Struct tipi, 16 byte’ı geçmeyen verilerde kullanılır. Örneğin; int, long, float, double, decimal, char, bool, byte, short gibi değişken türleri structan türetilmiş değer tiplerdir. .Net içerisinde ValueType’tan kalıtım alırlar, kalıtım vermezler. İnterface implemantasyonu yapabilirler. Struct tipinde üretilen bir değişken türü içerisinde default olarak parametresiz bir constructor metodu barındırır. Parametreli bir constructor metodu yazabiliriz ancak parametresiz constructor metodu üzerinde değişiklik yapamayız. Struct içerisinde fieldlar ilk değerlerini almadan bir metot içerisinde kullanılamazlar. Birçok geliştirme IDE’si değişkenlere default değer atadığı için geliştirme esnasında bunu farkedemeyebiliriz.
Daha iyi anlaşılması için örnek olarak bir koordinat tanımı yapılabilen bir uygulama düşünelim. Struct bir nesne oluşturup parametreli bir constructor metodu oluşturalım.

struct Coordinate
{
    public int X { get; set; }
    public int Y { get; set; }

    public Coordinate(int x, int y)
    {
        X = x; 
        Y = y; 
    }

    public int Sum()
    {
        return X + Y;
    }
}
static void Main(string[] args)
{
    Coordinate coordinate = new Coordinate(15,20);
    int result = coordinate.Sum();

    Console.WriteLine(result);
    Console.ReadLine();
}

Projeyi çalıştırdığımızda ekranda 35 değerini göreceğiz. Bunun yanında, ekrana bir de koordinat nesnesinin base type’ını yazdıralım.

Struct Base Type

Enum tipi, bir diğer değer tipli programlama ögesidir. Programa bir listeden seçim yaptırma veya seçilmiş değerin bir listede olup olmadığını kontrol etmek için kullanılır. Enum içerisindeki elemanlar indexe benzeyen bir yapıya sahiptir. Eğer elemana herhangi bir değer verilmezse, sayısal değer default olarak 0’dan başlar. İş kuralına göre bu sayısal değerleri kendimiz atayabiliriz. Yaygın bir kullanım olmasa da, negatif ve pozitif değerler bir arada tutulabilir. Tüm elemanlara aynı sayısal değeri verebiliriz, bu durumda da ortadaki elemanı getirir. Enumlar genellikle değişmesi güç olan değerler için kullanılır. Örneğin haftanın günlerini içeren bir enum oluşturalım.

enum Days
{
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

static void Main(string[] args)
{
    int value = (int)Days.Friday;
    Console.WriteLine(Days.Friday);
    Console.WriteLine(value.ToString());
    Console.ReadLine();
}

Kullanıcıya bir listeyi farklı şekilde sıralama seçeneği sunan bir uygulama yapalım. Burada yer alacak olan sıralama seçeneklerini enum tipinde olacak şekilde tanımlayabiliriz.

enum OrderChoice
{
    Straight = 1,
    Reverse = 2,
    BigToSmall = 3,
    SmallToBig = 4
}

static void Main(string[] args)
{
    List values = new List();
    Random random = new Random();
    for (int i = 0; i < 5; i++)
    {
        values.Add(random.Next(0, 10));
    }

    Console.WriteLine("Listeyi sıralamak için lütfen seçim yapın:");
    Console.WriteLine("1 - Düz");
    Console.WriteLine("2 - Ters");
    Console.WriteLine("3 - Büyükten Küçüğe");
    Console.WriteLine("4 - Küçükten Büyüğe");
    string value = Console.ReadLine();
    OrderChoice orderChoice = (OrderChoice)int.Parse(value);
    WriteList(orderChoice, values);

    Console.ReadLine();
}

static void WriteList(OrderChoice orderChoice, List list)
{
    switch (orderChoice)
    {
        case OrderChoice.Straight:
            break;
        case OrderChoice.Reverse:
            list.Reverse();
            break;
        case OrderChoice.BigToSmall:
            list.Sort();
            list.Reverse();
            break;
        case OrderChoice.SmallToBig:
            list.Sort();                    
            break;
        default:
            break;
    }

    foreach (int item in list)
    {
        Console.Write(item + " ");
    }
}

Referans Tipler

Objectten türetilen nesneleri kapsar. Yani, class, interface, string, dizi, liste gibi newlenebilen her nesne referans tiplidir. Bu değişkenlere bir değer atandığında, atanan değer heap bölgesinde tutulurken, değişkenin adresi stack bölgesinde tutulmaktadır. İki referans tipli değişkeni birbirine eşitlemeyi denersek, bu iki değişkenin adresleri de eşitlenmiş olur. İki değişken de aynı adresi referans aldığı için birisinde yapılan değişiklik öbürünü de etkiler. Örneğin; içerisinde meslekleri barındıran iki farklı diziyi birbirine eşitleyelim.

static void Main(string[] args)
{
    string[] jobs = { "Developer", "Designer", "Product Owner" };
    string[] jobs2 = { "Engineer", "Analyst" };

    Console.WriteLine("Before:");
    Console.WriteLine("jobs[0] " + jobs[0]);
    Console.WriteLine("jobs2[0] " + jobs2[0]);

    jobs2 = jobs;

    Console.WriteLine("After:");
    Console.WriteLine("jobs[0] " + jobs[0]);
    Console.WriteLine("jobs2[0] " + jobs2[0]);

    jobs[0] = "Dev";
    Console.WriteLine("jobs2[0] " + jobs2[0]);

    Console.ReadLine();
}

Referans Tip Ataması

Örnekte görüldüğü üzere jobs2 dizisi jobs dizisine eşitlendiğinde artık aynı adresi paylaşırlar ve jobs içerisinde yapılan her değişiklik jobs2 içerisinde de geçerli olur. Peki bu durumda jobs2’nin başlangıçta gösterdiği adrese ne olur? Garbage Collector, jobs2’nin başlangıçtaki adresini hiçbir tanım göstermediği için bellekten kaldırır.

String

Stringlerin de referans tipli olduğunu söylemiştim. Ancak string nesneleri değer tipli gibi hareket ederler. Bu tür davranan nesnelere immutable denir. String veri türü bir kere tanımlandıktan sonra bir daha değiştirilemez. Örnek üzerinden inceleyelim.

static void Main(string[] args)
{
    string value = "Hello World!";
    value.Replace("World", "Internet");
    Console.WriteLine(value);
    value = value.Replace("World", "Internet");
    Console.WriteLine(value);
    Console.ReadLine();
}

İlk uygulamada Replace metodu uygulandıktan sonra ekranda Hello Internet! yazması gerekir. Ancak onun yerine ekranda Hello World! yazısı karşımıza çıkar. İkinci uygulamada ise Replace metodu ile değiştirilen metni tekrar value değişkenine atadık. Bunun sonucunda da ekranda Hello Internet! yazısını elde ettik. Peki neden ilk uygulamada çalışmadı? Şöyle ki, ilk uygulamamızda Hello World! yazısını Hello Internet! olarak bellekteki heap bölgesine işaretledi. Ancak bizim bu bölgede bulunan bilgiye erişmemiz için stackte tutulan adres bilgisine ihtiyacımız var. Elimizde adres bilgisi bulunmadığı için heapteki bu bilgiye erişemiyoruz. İkinci uygulamada ise, yaptığımız değişikliği stackte adresi bulunan bir değişkene atadığımız için erişebilmekteyiz.

String veri türünün diğer değer tipli veri türlerinden farkı da null değer alabiliyor olmasıdır. Değer tipli veri türleri nullable yeteneği verilmeden null değer alamazlar. Bunun yanı sıra string değerinin boş (string.Empty) veya null değere sahip olması da bellekte farklı bir durumu gösterir. Çünkü, null değerine sahip değişkenlere bellekte yer ayrılmazken, boş değere bellekte bir yer ayrılmaktadır.

Benim değer ve nesne tipli değişkenler için notlarım bu kadar. Özellikle bilinçli bellek yönetimi yapabilmek için bu konunun bilinmesi gerektiğini düşünüyorum. Bir sonraki yazıya kadar kendinize çok iyi bakın! 😊