C# 9.0 – Records İle Immutable Datalar

Merhaba,

Hani bazen gelen yenilik, var olan küçük bir gediği kapatmak için yapılan bir onarımdan yahut iyi ya da kötü bir değişiklikten ibaret olabilmektedir. Lakin bazıları vardır ki, hangi olgu üzerinde olursa olsun bir gelişimdir, ileriye dönüktür, faydalıdır ve mevcudiyete bir değer katandır. İşte… C# 9.0 ile gelen ve yenilikten öte, dile ayrı bir değer katan, gelişimsel olarak ileriye taşıyan Records özelliğiyle tanışmaktayız. Bu içeriğimizde bu özellik üzerine istişarelerimizi edecek ve tüm detaylarıyla konuyu irdeliyor olacağız.

Records Nedir?

C# 9.0’da gelen ve bir önceki Init-Only Properties ve Init Accessor başlıklı makalemde ele aldığımız konuda kısaca ‘init’ keyword’ünü ele almış ve nesne üretim esnasının dışında değişmez değerler oluşturulması için constructer ve auto property initializers yapısının yanında object initializer yapısınında nasıl kullanılabilir hale getirildiğini incelemiştik. İlgili makaledeki konuda sabitlik sadece tek bir property üzerinde amaç edinilmektedir. Eğer ki bir objeyi bütünsel olarak değişmez yapmak istiyorsak o zaman daha fazlasına ihtiyacımız olacaktır. İşte bu ihtiyaca istinaden Records geliştirilmiştir.

Record, bir objenin topyekün olarak sabit/değişmez olarak kalmasını sağlamakta ve bu durumu güvence altına almaktadır. Böylece bu obje, artık değeri değişmeyeceğinden dolayı esasında objeden ziyade bir değer gözüyle bakılan bir yapıya dönüşmektedir. Buradan yola çıkarak, record’ları içerisinde data barındıran lightweight(hafif) class’lar olarak değerlendirebiliriz. Record’lar, class’lara istinaden objeden ziyade içerisinde bulunan dataları sabitleyerek varlığına nazaran verilerini/datalarını öne çıkarmakta ve böylece biz yazılımcılar açısından bir nüans sağlamaktadır. Biliyorum… Buradaki anlatım zihninizde tam karşılık bulmamış olabilir. Bunun için aşağıdaki şemaları inceleyerek class ve record arasında ayrımı gözetmemiz anlamlandırmamız için oldukça faydalı olacaktır.

class record
C# 9.0 - Records C# 9.0 - Records
Class’lar da verisel olarak nesne ön plandadır ve her bir farklı referansa sahip olan nesne farklı değer olarak algılanmaktadır.
Dolayısıyla Equals(x, y) karşılaştırması yanlıştır.
Record’lar ise verisel olarak değeri ön planda tutmaktadır. Sadece nesnel olarak bu veriler bir objede tutulmakta lakin değiştirilememektedir.
Haliyle farklı objelerde de olsa, veriler(property değerleri) aynı olduğu sürece Equals(x, y) önermesi doğru olacaktır.
Her iki türlüde veriler bir objede tutulmakta, lakin record’lar class’lara nazaran, bir objeden ziyade topyekün veri imajında olacak şekilde spesifik bir davranış sergilemektedirler.

Record, compile çıktısı olarak IEquatable<T>’i implemente eden bir sınıfa dönüşmektedir. Böylece record üzerinden yaratılacak nesneler aralarında karşılaştırıldıklarında referans olarak değil, verisel olarak değerlendirilir.

Records Nasıl Oluşturulur?

Record’lar, tıpkı bir class, interface, struct tanımlamasında olduğu gibi benzer iskelette bir syntax ile oluşturulmaktadırlar.

Prototip olarak aşağıdaki gibidir;

record [Name]
{

}

Örnek vermemiz gerekirse eğer;

    public record Book
    {
        public string Name { get; init; }
        public string Author { get; init; }
    }

şeklinde tanımlanmaktadır.

Bu şekilde tanımlanan record’lara norminal records denmektedir. Ayriyetten içeriğimizin devamında positional records‘ların olduğunuda göreceğiz.

Records İle Class Arasında Fark Kritiği Yapalım!

Yukarıda Records Nedir? başlığı altında record ile class arasındaki farklardan bahsetmiş ve bunu mümkün mertebe görselleştirmiştik. Lakin şimdi record ile class ararsında pratiksel açıdan farklara değineceğiz.

Record’lar değiştirilemez objeler oluşturmamızı sağlamaktadırlar demiştik. Peki bu değiştirilemez objeleri class’lar ile gerçekleştiremez miyiz? Gelin deneyelim.

    public class Employee
    {
        public string Name { get; init; }
        public string Surname { get; init; }
        public int Position { get; init; }
    }

Yukarıdaki class tanımlamasını incelerseniz eğer içerisindeki tüm propertyler ‘init’ olduğundan dolayı aşağıdaki gibi sadece üretim esnasında ilk değerlerini alabilmektedirler.

            Employee employee1 = new Employee
            {
                Name = "Gençay",
                Surname = "Yıldız",
                Position = 1
            };

Dolayısıyla burada pratiksel açıdan class’ların, record’lardan pekte bir farkının olmadığını düşünebilirsiniz. Peşin hükme varmadan, aceleye getirmeden sakince örneklendirmeye devam edelim. Düşünelim ki, bu nesnenin süreçte herhangi bir property değerini değiştirmek istediğimizde bunu gerçekleştirebilmek için aşağıdaki gibi yeni bir Employee nesnesi üretmemiz ve değişikliğin yapılacağı property dışında diğer property’leri eşleştirmemiz gerekmektedir.

            Employee employee1 = new Employee
            {
                Name = "Gençay",
                Surname = "Yıldız",
                Position = 1
            };

            Employee employee2 = new Employee
            {
                Name = employee1.Name,
                Surname = employee1.Surname,
                Position = 2
            };

Bu yöntemin, adil sayıdan fazla property’e sahip olunduğu durumlarda can sıkıcı hale geleceği aşikar olsa gerek. Ne kadar çok property o kadar zor ve maliyetli kod demektir. Elbette reflection veya serialization ile kopyalama mantıkları uygulanabilir yahut bir auto-mapping kütüphanesi kullanılabilir. Ancak bu tarz bir durumda C# 9.0 ile gelen Records yapılanmasıyla çalışılması en doğru yöntem olacaktır.

Eğer ki biz, Employee sınıfını aşağıdaki gibi record olarak tasarlarsak;

    public record Employee
    {
        public string Name { get; init; }
        public string Surname { get; init; }
        public int Position { get; init; }
    }

bu record’dan üretilen nesnelere değişmez bir veri misali davranılmasını istediğimizi bildirmiş olacağız. Dolayısıyla record nesnesinde bir değişiklik yapılmak istendiğinde;

            Employee employee1 = new Employee
            {
                Name = "Gençay",
                Surname = "Yıldız",
                Position = 1
            };

            Employee employee2 = new Employee
            {
                Name = employee1.Name,
                Surname = employee1.Surname,
                Position = 2
            };

şeklinde class’lar da olduğu gibi manuel bir yaklaşımla ilgili nesneyi kopyalamak ve sadece istenilen değeri farklı kılmak mümkündür. Lakin böyle bir ihtiyaca istinaden record yapılanması ‘with‘ komutu ile hızlıca ilgili nesneyi kopyalamamızı ve istenilen özelliğinin değiştirilebilmesini sağlamaktadır.

with Expressions

Immutable türlerde çalışırken nesne üzerinde değişiklik yapabilmek için ilgili nesneyi ya çoğaltmamız/klonlamamız(deep copy) ve üzerinde değişiklik yapmamız gerekmekte ya da yukarıda yaptığımız gibi yeni bir nesne üretip mevcut nesnedeki değerleri, değişikliği yansıtılacak şekilde aktarmamız gerekmektedir.

Immutable tiplerde nesne üzerinde yapılacak değişiklik için yeni bir nesne üretilmesinin gerekliliğini, string üzerinde yapılan değişikliğin yeni bir string yaratılmasını gerektirmesiyle örneklendirebiliriz…

Burada ‘With’ metotları oluşturup probleme kısmi bir kolaylıkta çözüm getirebiliriz.

    public class Employee
    {
        public string Name { get; init; }
        public string Surname { get; init; }
        public int? Position { get; init; }

        public Employee With(int position)
        {
            return new Employee
            {
                Name = this.Name,
                Surname = this.Surname,
                Position = position
            };
        }
    }

İlgili ‘With’ metodu yeni bir nesne üretiminden sorumlu olmakta ve sadece değişiklik olacak property’e odaklanmaktadır. Kullanımı ise aşağıdaki gibidir;

            Employee employee1 = new Employee
            {
                Name = "Gençay",
                Surname = "Yıldız",
                Position = 1
            };

            Employee employee2 = employee1.With(2);

Evet, immutable olan bir türde nesneyi türeterek kısmi değişiklik yaptık yapmasına ama ‘With’ metodunu yazmak her zaman bu kadar kolay olmayacaktır. Bunun için C# 9.0’da record’lar için gelen with expressions’lar, ‘With’ metodu yazmaktan bizleri kurtarmakta ve daha spesifik çözüm getirmemizi sağlamaktadır.

    public record Employee
    {
        public string Name { get; init; }
        public string Surname { get; init; }
        public int? Position { get; init; }
    }
            Employee employee1 = new Employee
            {
                Name = "Gençay",
                Surname = "Yıldız",
                Position = 1
            };

            Employee employee2 = employee1 with { Position = 2 };
            Employee employee3 = employee1 with { Name = "Hilmi", Position = 2 };
            Employee employee4 = employee2 with { Name = "Rıfkı", Position = 5 };

with komutu daha etkili bir şekilde yeni nesne oluşturmak için olanak sağlar. Bu kod, kendisinden önceki kodlarla aynı sonucu getiriyor olsa da işlevsel açıdan pratik ve kod maliyeti açısından oldukça düşüktür.

Positional Records

Norminal record’lar object initializer’lar ile initialize edilebildiği gibi positional record’lar da ise constructor ve deconstructor kullanımına izin verilmektedir.

Normalde record’lar yapısal olarak class’lar gibi constructor ve deconstructor alabilmektedirler. Lakin Positional Records bu manuel tanımlamalardan ziyade daha fazlasını ifade etmektedir. Şimdi gelin önce bildiğimiz yöntemle constructor ve deconstructor tanımlayalım.
Eski Yöntem;

    public record Employee
    {
        //Constructor
        public Employee(string name, string surname)
            => (Name, Surname) = (name, surname);

        //Deconstructor
        public void Deconstruct(out string name, out string surname)
            => (name, surname) = (Name, Surname);

        public string Name { get; set; }
        public string Surname { get; set; }
        public int Position { get; set; }
    }
            Employee employee = new Employee("Gençay", "Yıldız")
            {
                Position = 1
            };

            var (Name, Surname) = employee;

Yeni Yöntem;
Yeni yöntem bu konuda oldukça pratik bir çözüm getirmiştir.

    public record Employee(string Name, string Surname)
    {
        public int Position { get; set; }
    }

Yukarıda görüldüğü üzere direkt olarak record tanımlamasının yanında parantezler ile constructor misali obje içerisindeki ilgili property’ler tanımlanmaktadır. Bu property’ler default ‘init’ olacak şekilde oluşturulmakta ve nesne üretimi esnasında constructor’dan verilecek değerlerden sonra readonly olarak kullanılmaktadır.
C# 9.0 - Records

Ayrıca aşağıdaki varyasyonda da bir positional record tanımlanabilmektedir.

    public record Employee(string name, string surname)
    {
        public string Name => name;
        public string Surname => surname;
        public int Position { get; set; }
    }

Bu varyasonu değerlendirdiğimizde şahsen ben ilk örneklendirmedekini tercih etmekteyim 🙂

Records ve Inheritance?

Yeni gelen record’lar da inheritance desteğide bulunmaktadır. Hemen örneklendirmemiz gerekirse eğer;

    public record Car
    {
        public string Name { get; init; }
        public string Model { get; init; }
    }
    public record SportCar : Car
    {
        public int MaxPower { get; init; }
    }

Yukarıda görüldüğü üzere record’lar da klasik : operatörü ile kalıtım alınabilmektedir. Eğer ki kalıtım veren record türü positional ise;

    public record Car(string Name, string Model)
    {

    }
    public record SportCar : Car
    {
        public SportCar() : base("X", "Y")
        {

        }
        public int MaxPower { get; init; }
    }

base record’a derived record’dan ilgili dataları göndermek kaydıyla kalıtım gerçekleştirilebilir. Lakin tam tersi olarak norminal record, positional record’a kalıtım verirse herhangi bir ayara gerek kalmaksızın bu işlem aşağıdaki gibi gerçekleştirilebilmektedir.

    public record Car
    {
        public string Name { get; init; }
        public string Model { get; init; }
    }
    public record SportCar(int MaxPower) : Car
    {

    }

Tüm bunların dışında positional record, bir başka positional record’a kalıtım verememektedir.
C# 9.0 - Records
Görüldüğü üzere ‘SportCar’ record’ı, base record olan ‘Car’a ilgili constructor değerlerini gönderebilmek için kendisi yeni bir constructor oluşturmak zorunda kalmakta ve bir tutarsızlık ortaya çıkmaktadır. Madem biz constructor oluşturacaktık o halde ‘MaxPower’ parametresini direkt constructor’da tanımlardık! Öyle değil mi?…

Velhasıl, örneklerde olduğu şekilde aralarında kalıtımsal ilişki olan record nesnelerini with expression ile yeniden klonlamak istiyorsak eğer bunu aşağıdaki gibi rahatlıkla gerçekleştirebiliriz.
C# 9.0 - Records
Biz her ne kadar ‘Car’ referansı üzerinden bu objeyi with ile türetsekte compiler arkaplanda ‘Clone’ fonksiyonunu kullandığı için yeni bir ‘SportCar’ üretmekte ve tip bilgisini kaybetmeksizin var olan verilerle değişiklikleri yakalayabilmektedir. Dolayısıyla ‘MaxPower’ gelmektedir.

Record Objects Comparison Equals/ReferenceEquals

Record’ların, farklı referanslarla işaretlenmiş nesneleri olsa dahi taşıdıkları veriler aynı olduğu sürece Equals(x, y) karşılaştırmasını doğrulayacağından bahsetmiştik.

Şimdi gelin bunu == operatörü eşliğinde hem ‘Equals’ hemde ‘ReferenceEquals’ metotlarıyla test edelim;

            Car car1 = new SportCar()
            {
                Name = "X",
                Model = "Y",
                MaxPower = 1000
            };

            Car car2 = car1 with { };
            Car car3 = car1 with { Model = "Z" };

            Console.WriteLine($" car1 == car2 {car1 == car2}");
            Console.WriteLine($" car1 == car3 {car1 == car3}");
            Console.WriteLine($" car2 == car3 {car2 == car3}");

            Console.WriteLine($" Equals(car1, car2) {Equals(car1, car2)}");
            Console.WriteLine($" Equals(car1, car3) {Equals(car1, car3)}");
            Console.WriteLine($" Equals(car2, car3) {Equals(car2, car3)}");

            Console.WriteLine($" ReferenceEquals(car1, car2) {ReferenceEquals(car1, car2)}");
            Console.WriteLine($" ReferenceEquals(car1, car3) {ReferenceEquals(car1, car3)}");
            Console.WriteLine($" ReferenceEquals(car2, car3) {ReferenceEquals(car2, car3)}");

C# 9.0 - Records

Görüldüğü üzere record’lar ‘Equals’ karşılaştırmasında veriyi ön planda tutmakta, aksi taktirde ‘ReferenceEquals’ nesneler farklı referanslarda olduğu için false değerini döndürmektedir.

Generic Yapılanmalarda Record

Record’lar her ne kadar farklı bir olguymuş gibi gözükse de esasında bir class olduklarından dolayı generic yapılanmalarda bir base class constraint olarak bildirilememektedirler.
C# 9.0 - Records
Lakin ihtiyaca binaen aşağıdaki gibi direkt class yerine kullanılabilirler.
C# 9.0 - Records

Sonuç
Bu güzel ve performans açısından yüksek dereceli getirisi olan record özelliği hakkında nihai olarak denebilecek pek bir şey olduğunu düşünmemekteyim. Sadece bu ve bunun gibi yenilikler sayesinde, her geçen gün milyonlarca insanın kullandığı Türkçe’mizin bile bozulmaya ve yozlaşmaya yüz tuttuğu şu dünyada bir programlama dilinin bu derece nazik atılımlar yapması ve daha da önemlisi kendisini geliştirmek ve alanında her şeyi tarif edebilir hale gelebilmek için çabalaması ve ona göre tasarlanması, geleceğin bu dilleri bilenlerin dünyası olduğunun bir yansıması olduğunu göstermekte, bizlerinde buna şahit olduğu düşüncesindeyim.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*