İlkay İlknur

just a developer...

C# 7.2 - Value Typelarda Referans Semantiği Yenilikleri

Bu makaleye Github üzerinden katkıda bulunabilirsiniz.

Visual Studio 15.5 update'inin release olması ile beraber C#'ın bir sonraki minor release'i olan 7.2 versiyonu da artık RTM oldu. Daha önceki C# 7.1 yazımda da bahsettiğim üzere minor C# versiyonları Visual Studio içerisinde default olarak aktif olmuyor ve bu özellikleri kullanabilmemiz için ufak bir ayar yapmamız gerekiyor. Bu gerekli olan ayara da C# 7.1 Yenilikleri yazımdan ulaşabilirsiniz.

C# 7.2'de de yine C# 7.1'de olduğu gibi ufak ve faydalı yenilikler var. Ancak C# 7.2 sadece ufak yeniliklerin olduğu bir release değil. C# 7.2'nin ana teması, value type'larla çalışırken onları pass-by-reference mantığı ile kullanarak value typeların sahip olduğu kopyalanma özelliklerinin önüne geçip allocationı ve kopyalanma operasyonlarını azaltmak. Bu yazımızın konusu da bu tema kapsamında gelen yenilikler. C# 7.2 ile beraber gelen diğer yeniliklere de başka bir yazıda göz atacağız.

Bu Özellikler Neden Gerekliydi ?

Value typelar, özellikle de structlar performans kritik algoritmalarda, oyunlarda, 3D modelleme vs.. gibi alanlarda oldukça fazla kullanılmakta. Bunun en önemli nedeni value typeların heap allocationa neden olmaması nedeniyle Garbage Collector'ın getirebileceği tahmin edilemeyen yüklerden ve sorunlardan olabildiğince kaçınmak. Ancak value typelar bir metoda parametre olarak geçildiklerinde eğer herhangi bir ref parametre kullanılmazsa value typeların bir kopyası yaratılıp geçilir (pass-by-value) . Örneğin, .NET Framework içerisindeki System.Windows.Media.Media3D.Matrix3D structı içerisinde 16 tane double property bulunmakta. Bu da toplamda 128 byte ediyor. Dolayısıyla siz bu structı metotlara parametre olarak geçmeye kalkarsanız bu 128 bytelık struct sürekli olarak kopyalanarak metotlara geçilecektir. Elinizde bu structlardan oldukça fazla olduğunu düşünürseniz, bir metot çağırımı bile oldukça sıkıntılı sonuçlara yol açabilir. Bundan kaçınmak için tabi ki de C# içerisinde bazı çözümler var. Ancak C# 7.2 bu özelliklere ekstra yenilikler ekleyerek özellikle structlarla çalışmayı daha kolay ve özellikle güvenilir hale getiriyor. Dolayısıyla aşağıda anlatmaya çalışacağım yenilikleri bu konseptte değerlendirmekte fayda var.

in Parametreleri

Şu ana kadar C# içerisinde bir value type'ı bir metoda referansıyla parametre olarak geçmek istediğimizde out ve ref parametrelerini kullanabiliyorduk. Böylece value typeların metot çağırılırken kopyalanmasının önüne geçebiliyoruz ve referansıyla gönderebiliyoruz. Bu seçeneklerden, out parametreyi kullandığımızda, out parametrede çağırılan metot bu değişkene mutlaka bir değer ataması yapması gerekirken, ref parametrelerde ise değişken yine referansı ile metoda gelirken metodun değişken üzerinde bir değişiklik yapması zorunlu değil.

static void Out(out int x)
{
    x = 12;
}

static void Ref(ref int x)
{
    //Bazı durumlarda değiştirilebilir.
    if (x > 10)
    {
        x = 9;
    }
}

C# 7.2 ile beraber gelen in parametreleri ise yine değişkenin referansıyla metoda gönderilmesini sağlarken çağrılan metodun bu değişkeni değiştiremeyeceğinden emin olunmasını sağlıyor. Böylece çağırılan metot in parametreyi değiştiremeyeceği için gönderdiğiniz value type'ın özellikle de struct'ın değişmediğinden emin olabilirsiniz.

Örneğin,

static void In(in int x)
{
    x = 12;
}

Yukarıdaki gibi gelen bir in parametreye bir değer ataması yapmak isterseniz derleme hatası alıyorsunuz.

in parametre kabul eden bir metodu çağırmak istediğimizde iki şekilde çağırma yapabilmemiz mümkün. Birincisi out parametrelerde olduğu gibi in parametrenin başına in keywordünü eklemek.

static void Main(string[] args)
{
    int k = 1;
    In(in k);
}

Diğeri ise in keywordünü eklemeden çağırmak. Eğer in parametre yolladığınızı kodu okuyan kişinin bilmesini istiyorsanız parametrenin başına in yazmanız faydalı olabilir. Ama kullanım şekli tamamen size kalmış.

static void Main(string[] args)
{
    int k = 1;

    In(in k);            
    In(k);
}

in parametrelerin en büyük avantajı value typelar ile elde ediliyor.Reference typelarda etkisinin pek olmadığını belirtmemizde fayda var.

ref readonly returns

in parametrelerle, parametre referansıyla ve değiştirelemez bir şekilde metoda parametre geçilirken bu özellikle de dönüş değeri olan value type referansıyla döndürülür ve bu referans üzerinden döndürülen value type değiştirilemez. Bu özelliğin faydalı olabileceği kullanım alanlarında birini kısaca anlatmaya çalışalım.

Örneğin Point isimli bir struct olduğunu düşünelim.

struct Point
{
    public int X { getset; }
    public int Y { getset; }
}

Bunun için Default değerini kapsayacak bir property koymak istersek şu şekilde bir ekleme yapmamız gerekir.

struct Point
{
    public int X { getset; }
    public int Y { getset; }
    public static Point Default => new Point();
}

Burada her Default propertysi kullanıldığında stackde yeni bir Point yaratılır. Halbuki bunun yerine ref readonly olarak döndürürsek tek bir Point yaratıp sürekli olarak bunun referansını döndürebiliriz.

struct Point
{
    public int X { getset; }
    public int Y { getset; }
    private static Point defaultp = new Point();
    public static ref readonly Point Default => ref defaultp;
}

Bu şekilde Point structını tanımladıktan sonra Point üzerinden Default propertysine aşağıdaki gibi ulaşabiliriz.

static void Main(string[] args)
{
    ref readonly var defaultp = ref Point.Default;
}

Bu şekilde bir kullanımda defaultp değişkeni içerisinde readonly referans bulunduğu için Main metot üzerinde defaultp değişkeni üzerinden bir değer ataması yapılması mümkün olmayacaktır ve compiler kodu derlemeyecektir.

static void Main(string[] args)
{
    ref readonly var defaultp = ref Point.Default;
    //ref readonly !!!
    defaultp.X = 1; //---> Mümkün değil !!!
}

ref readonly olarak dönen bir değerin kopyasını almak istersek de dönen değeri ref readonly olmayan bir değişkene atamasını yapmamız yeterli.

static void Main(string[] args)
{
    var defaultp = Point.Default;
    //Default'in yeni bir kopyası oluşturuldu. 
    defaultp.X = 1; //---> Artık mümkün :)
}

Bu özelliği isterseniz metotların dönüş değerleri için de kullanabilirsiniz. Biz örnek olarak property yaptık ancak bu özellik metotların dönüş değerleri için de kullanılabilir.

readonly Structs

C# 7.2 öncesinde readonly olarak tanımlanan bir struct variable'ı üzerinde struct içerisindeki bir instance metodu çağırmak istediğimizde derleyeceği o metodun structı değiştirip değiştirmeyeceğini bilemediği için otomatik olarak structı arka planda geçici bir değişkene atayıp o değişken üzerinden ilgili metodu çalıştırıyordu. Bunun da performans olarak bazı sıkıntıları olabiliyordu. Örnek senaryo olarak https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-the-surprising-inefficiency-of-readonly-fields/ bu yazıyı okuyabilirsiniz. Bu performans sorunları nedeniyle de çoğu zaman readonly kullanmaktan vazgeçilebiliyordu.

C# 7.2 ile beraber struct tanımlamasının başına readonly yazarak structların readonly olmasını sağlayabiliyoruz. Böylece derleyici de yukarıda belirttiğim senaryoda geçici bir değişken yaratmak durumunda kalmıyor ve performans sıkıntılarının da önüne geçilmiş olunuyor.

readonly olarak tanımladığımız structların içindeki değerlere atamalar sadece constructor içerisinde yapılabiliyor. Bunun haricinde başka bir yerde atama yapmak mümkün değil.

readonly struct Point
{
    public int X { get; }
    public int Y { get; }

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

Şimdilik bu tema kapsamında bu yazıda bahsedeceklerimiz bu kadar. C# 7.2 ile beraber gelen ve yine tema kapsamında olan Span< T>'nin kullanımından ise başka bir yazıda bahsediyor olacağım. Bu yeniliklerin özellikle yüksek performanslı sistemlerin, oyunların optimizasyonu amacıyla yapıldığını hatırlatmakta fayda var. Biz belki kod yazarken sürekli structları veya bu yenilikleri kullanmıyor olabiliriz ancak kullandığımız frameworkler, toollar bu tipleri arka planda kullanıyorlar. Dolayısıyla bu yenilikler doğrudan olmasa bile dolaylı yoldan bizleri etkiliyor olacak. Mesela buradaki pull request içerisinde corefx içerisinde bu değişikliklerin adapte edildiğini görebilirsiniz.

Bir sonraki yazıda görüşmek üzere,