SDTR
Published in

SDTR

C# 9-10 Derinlemesine “record” Ve “record struct” Tipleri

Herkese yeniden merhaba,
Umarım keyifleriniz yerindedir. İzninizle bugün konuşacağımız konuya hızlıca giriş yapmak istiyorum. Hatırlarsanız C# 9 ile birlikte hayatımıza record’lar girmişti. record’ları fazla detaylı inceleme fırsatım olmadan C# 10 ile record struct’lar duyuruldu. Aslında C# 9 ile record’ların temel mantığını anlamış olmama rağmen bir şeyleri kaçırdığımı düşünüyordum. Bunu düşünmemin en önemli sebebi bazı görmediğim record kullanımlarına rastlamam ve kullanım alanlarını merak etmemdi. Bu düşünceler beni bu konuyla alakalı detaylı bir araştırma yapmaya itti. Yaptığım bu araştırmaları da yazıya dökerek sizinle paylaşmaya karar verdim. Bu yazımın ana konusu record ve record struct’lar olacaktır. record struct’ları anlatırken doğal olarak belli bir seviyede struct’lara da değineceğim. Hazırsanız başlayalım.

record

C# 9 ile birlikte record’lar, class ve struct’lara ek olarak verileri kapsülleyebileceğimiz bir tip olarak ortaya çıkmıştı. record’larda class’lar gibi bir referans tip olmak ile beraber asıl ortaya çıkış amaçları (değişmez)immutable nesneler oluşturmamızı ve nesnemizin değerleriyle ön plana çıkmasını sağlamaktır.

Değişmezlik ve değerleriyle ön plana çıkmasını açıklamadan önce yine C# 9 ile birlikte hayatımıza giren init-only özellliğinden bahsederek konuya giriş yapmak istiyorum. Hatırlarsanız init-only özelliği sayesinde değişmez(immutable) nesneler tanımlayabiliyorduk. Bu özellik ile sınıf nesnelerimiz değişmez olmakla birlikte ilk atamasını readonly’den farklı olarak object initializer kullanmamıza izin veriyordu.

Yukarıda gördüğünüz üzere init anahtar kelimesi ile belirttiğimiz property’lere sahip class olan ClassWithInitOnlyProp sınıfına ait nesneye object initiazier’ı kullanarak değerlerini atayabiliyoruz. Fakat readonly property’ler gibi nesne oluşturulduktan sonra değişmezliğini koruduğuna dikkat edelim.

Şimdi de bir record tanımlaması yapalım:

Evet örneği incelediğinizde aklınıza ilk gelen şeyin record’ların default olarak değişmez(immutable) değilde init ile birlikte değimez özellik kazandığı olabilir. Aslında cevap kısmen evet kısmen hayır. Bildiğiniz üzere C# bize positional ve cretional olmak üzere iki tarzda kod yazmamıza izin verir. Bunları kısaca açıklarken record’ların ne zaman default olarak değişmez(immutable) olduklarına değinelim:

Positional Creation

Nesnenin özelliklerini atamak için constructor’ı kullandığımız geleneksel yöntemdir.

Nominal Creation

Bu yöntemde bir class veya record’un constructor’ı parametre almaz veya ilk değerleri atanması gereken özellik sayısından daha az parametre alır. İlk değer atamaları object initializer kullanılarak gerçekleştirilir.

Örnek üzerinden gidelim:

Örneği incelediğimizde PositionalRecordExample’ın primary constructor’ı içinde tanımlanan Name ve Surname değerlerinin otomatik olarak init-only tanımlandığını görüyoruz. Yani record’ları Positional olarak oluşturduğumuzda otomatik init özelliği gelmektedir. Fakat dikkat ederseniz eğer PositionalRecordExample record’unda Age’in otomatik olarak init tanımlanmasını istemediğim için bir property olarak belirterek atamasını yaptım. Ayrıca Role özelliğinin ise otomatik olarak gelen init tanımını değiştirmeden protected olarak bildirdim. Bu sayede Positional tarzda yazdığımda otomatik olarak gelen tanımı da ezebildiğimizi görmüş olduk. record’ların ne zaman default olarak değişmez(immutable) tanımlandığını gördüğümüze göre diğer özelliklerine geçelim:

Not: C# 9'da record anahtar kelimesi ile kullandığımız bu referans tip, C# 10 ile birlikte record class takma ismiyle de kullanılabilmektedir. (record = record class)

  • With Anahtar Kelimesi

record’ların asıl amacı değişmezlik olduğundan nesne özelliklerini değiştirmek istediğimizde immutable nesneyi değiştiremeyeceğimizden yenisini oluşturmamız gerekmektedir. record’lar da bu işlemi yapmak için with keyword’ü tanımlanmıştır.

Örneğe bakacak olursak eğer with keyword’ü ile Car record’unun kopyasını alarak yeni bir obje oluşturdu. Buraya kadar bir problem yok. Fakat biraz daha dikkatli incelersek eğer with ile yapılan kopyalama işleminin deep copy değil bir shallow copy işlemi olduğunu anlarız. Eğer record tanımlamamız içerisinde complex type değişkenimiz varsa bunun kopyası alınmayarak heap’teki aynı complex type değişkeni gösterecektir.

  • Değer Eşitliği

Yazımızın en başında record’lardan bahsederken nesnemizin değerleriyle ön plana çıktığından bahsetmiştik. record’ların referans tip olmasına karşı eşitlik söz konusu olduğunda referans tipler gibi davranış sergilemezler.

Öncelikle şunu kesinleştirelim. record nesneleride class nesneleri gibi heap’te oluşturulur. Aeroplane sınıfının objelerinin eşitlik karşılaştırmalarını incelediğimizde False sonucunun döndüğünü görmekteyiz. Çünkü aeroplane_obj1 ve aeroplane_obj2 nesneleri karşılaştırılırken heap’te aynı bölgeyi gösterip gösterilmediğine bakılmaktadır. Bu yüzden False sonucu dönmektedir. Ama aynı durum car_obj1 ve car_obj2 nesneleri için geçerli değildir. Çünkü record nesneleri karşılaştırılırken değerler ön planda tutulur. Yani iki record nesnesinin aynı olması için nesne içindeki özelliklerin birebir aynı olması şarttır. record’lar bunu yapabilmek için otomatik olarak IEquatable interface’ini uygulamaktadır. Örnek olarak Aeroplane sınıfının eşitliklerde bir record gibi davranmasını isteseydik sınıfı aşağıdaki şekilde düzenlememiz gerekirdi:

  • Kalıtım

record’lar aynı zamanda kalıtıma’da izin vermektedir.

Aşağıdaki şekilde de kullanılabilir:

Ayrıca record’lar abstract olarak tanımlanabilmektedir.

abstract sınıflarla aynı davrınışı sergilemektedir.

  • toString

record’larda toString() class’lardan farklı override edilmiştir.

  • Deconstruct

record’lar C# 7 ile tanıtılan deconstruct özelliğini destekler. Hatırlarsanız eğer bu özellik sayesinde bir tuple’ı parçalara ayırıp değerlerini yeni değişkenlere atayabiliyorduk.

Örneği dikkatli şekilde incelerseniz eğer class’lar için oluşturduğumuz Deconstruct(string,int) metodunu record için tanımlamamıza gerek kalmadan deconstruct özelliğini kullanabildik. 4 ve 5 numaralı satırlarda record’lar için iki farklı kullanımı gösterilmiştir.

  • Generic

record’lar ile generic işlemler yapabilmemizin yanı sıra generic class’lar ile de record’ları kullanarak işlem yapabiliriz. Yine örnek üzerinden gidelim:

Görüldüğü üzere Process sınıfına constrait olarak Car türündeki record’u verdik.(6.satır / where T:Car) Ve Car türünden türüyen Opel record’unu kullanarak Process sınıfından bir nesne oluşturduk.(2. satır) Son olarak Process sıfının Show() metodu içinde bilgileri ekrana yazabilmek için generic record olan Writer record’unun Concat() metodunu kullandık.(9. satır)

Roslyn Arka Planda Ne Yapıyor?

Şimdi ise aşağıdaki gibi positional olarak bir person record’u oluşturalım:

Gördüğünüz üzere with anahtar kelimesi ile bir klonloma da yaptık. Şimdi arka planda derleyici tarafından nasıl lowering yapıldığına bakalım:

Not: Roslyn branches main(9 Aug 2022)

Arka planda yapılan lowering’e göz atmaya başlarsak eğer record’umuzun arka planda IEquatable interface’ini uygulayan bir class olarak tanımlandığını görebiliriz.(Evet boşuna record class denilmiyor.😉) Ardından sırayla name,surname ve age property’lerinin init-only olarak tanımladığını görüyoruz. Böyle olmasının sebebini record’u positional olarak oluşturmamız olduğunu söylemiştik. Ardından constructor ile property’lere atamalar yapılmaktadır. 81.’i satırda toString() fonksiyonunun override edilip 95. satırdaki PrintMembers() fonksiyonu ile beraber kullanılarak örneğimizde gösterdiğimiz gibi nesnelerin property’lerinin ekrana nasıl yazıldığı açıkca görülmektedir. record’larda değer kıyaslaması yapabilmemiz için 143. satırda Equals fonksyionun ve 118 ve 112. satırlarda (== / !=) operatorlerinin override edilerek nasıl bir değer kıyaslaması yapıldığını daha net anlayabiliyoruz. 131. satırda GetHashCode() metodunun override edildiğini görmekteyiz. 157. satırda Clone() fonksiyonunun tanımlandığını ve 162. satıda ise protected constructor’un tanımlandığını görmekteyiz. Clone() metodunun nasıl kullanıldığını örneğimizdeki

var person2 = person with { age = 25 } ;

ifadesine bakarak açıklayabiliriz:

main metotu içinde aşağıdaki ifadeye bakarak her şeyi daha net anlayabiliriz:

new Person(“Mehmed”, “Emre”, 24).<Clone>$().age = 25 ;

ifadesine bakarak görmekteyiz ki with keyword’ü ile yeni bir Person nesnesi klonlanarak sadece with keyword’ü içerisinde değişikliğini yaptığımız age property’sinin değiştirildiğini görmekteyiz. Son olarak 169. satırda ise otomatik olarak Deconstruct() fonksiyonunu oluşturulduğundan record’un property’lerini deconstruct yapabiliyoruz.

“record /record class” Ne Zaman Kullanabiliriz?

Farklı uygulamalar veya tek bir uygulama içindeki farklı katmanlar arasında veri aktarımı için kullandığımız DTO class’ları, record olarak tanımlanabilir. Veya örnek olarak Domain Driven Design içerinde bulunan domain katmanındaki ValueObject ‘te class yerine record tanımlanarak ValueObject içerisinde iki objenin eşit olup olmadığını kontrol ettiğimiz fonksiyonları otomatikleştirebiliriz. Fakat ValueObject kullanırken oluşturulan class yapısının record’lardan daha üstün olduğunu gösteren bazı makalelerde mevcuttur. Ek olarak farklı nesnelerin özelliklerini karşılaştırmamız gereken bir iş mantığımız varsa kullanmak faydalı olacaktır. Son olarak bir veri yapısını kopyalamak istediğimizde de record’lar bize tek satırda kopyalama sağlar.

Ne Zaman Kullanmamalıyım…

Evet record’ları o kadar anlattık. Gerçekte olduğu gibi yazılımda da her şeyi yerinde kullanmak bize pozitif sonuçlar doğuracaktır. Aksi olduğunda bize en iyi ihtimalle gereksiz bir kullanım olarak geri dönüt verecektir. Aşağıda record kullanmanın gereksiz olduğu bir senaryo üzerinde duralım:

  • REST Endpoint Ve record class

Aşağıdaki gibi bir endpoint’imiz olsun:

CreateProductCommand parametresinin record olarak tanımlandığını düşünelim. Sizce bu mantıklı mı? Veya bize bir faydası olacak mı? REST protokolü mimarisi gereği stateless(durumsuz)’dır. Yani her bir request’tin bir önceki request’i hatırlaması gerekmez. Request’ler birbirinden bağımsız ele alınır. API’nin gelen nesne ile alakalı herhangi bir durum bilgisine sahip olmaması gerekir. Peki durum böyleyken nesnenin durumunu kontrol edecek gibi bir istek parametresi tanımlamanın anlamı var mıdır? record’un birincil amaçları IEquatable interface’ini uygulamak ve değişmezlik iken istekle alakalı bir durum kontrolünün yapılmayacağını bildiğimiz halde kullanmak gereksiz olacaktır. Kaldı ki bir API’nin amaçlarından biride nesnenin durumunu değiştirme yeteneği sağlamaktır.

Bu ve bunun gibi daha bir çok durum bulunabilir. Bu yüzden bir bilgiyi bilmekten öte nerede kullanacağımızı bilmek bize daha çok avantaj sağlayacaktır. Tabi bilgiyi öğrenmeden de kullanma aşamasına hiç bir zaman geçemeyiz. 😊

record struct

Yazımın başında record’lardan ve referans tipli olduklarından bahsetmiştik. C# 10 ile birlikte gelen record struct sayesinde record’ları değer türlü olarak oluşturabiliriz . record’lar nasıl arka planda class olarak tanımlanıyorsa record struct’larda arka planda struct olarak tanımlanmaktadır. Bildiğimiz üzere struct’lar value type(değer türlü) veri tipleridir. Sıklıkla kalıtım gerektirmeyen küçük veri türlerini tutmak için kullanırlar. Değer türlü olduklarından belleğin Heap değil Stack kısmında tutulmaktadırlar. Heap’e erişmenin maliyeti fazla olduğundan bu tarz verilerin Stack’de tutulması performansa önemli ölçüde etki etmektedir. Peki record struct ile neler oluyor?

  • With Anahtar Kelimesi

record struct’lar ve struct’larda with keyword’ünü desteklemektedir:

Yine record’larda olduğu gibi shallow copy yapıldığında record struct içinde complex type değişkeni heap’te aynı yeri gösterecektir.

  • Değişmezlik

record struct’ların property’lerinin değişmez olmasını istiyorsak eğer property’lerinde init anahtar kelimesini kullanmak zorundayız. record’ları positional olarak oluşturduğumuzda otomatik init-only özelliği gelmekteydi. Fakat record struct’ları positional oluştursak bile otomatik olarak init-only özelliği ile beraber gelmeyecektir.

Positional record struct’ları değişmez olarak tanımlayabilmek için readonly anahtar kelimesini kullanmamız gerekir.

  • Eşitlik

struct’larda aynı değerlere sahip iki struct’ı Equals ile karşılaştırdığımızda true değeri dönecektir. record struct’ta bir struct olduğundan aynı durum record struct içinde geçerlidir. struct, class ve record class aksine değer tipli olduğundan == ve != operatörlerini override edemez, bu nedenle iki struct’ı bu operatörlerle karşılaştırmak imkansızdır . Ancak, bir record struct üzerinde bu operatörlerle karşılaştırma yapılabilir.

  • toString

record struct’larda toString() metodu override edilmiştir.

  • Deconstruct

Deconstruct olayına tekrar dönecek olursak record’lar ile aynı davranışı sergilediğini söylemek mümkün:

  • ref struct

record struct ile ref record struct kullanamayız. Bunun nedeni record struct IEquatable<T> interface’ini uygular fakat ref struct’lar ise IDisposable dışında herhangi bir interface uygulayamaz.

Not: Bir ref struct veya readonly ref struct, public erişim belirteçli bir void Dispose() metodunu kullanırsa, bu IDisposable interface’inin uygulanmasına eşdeğer olacaktır. Bu da , garbage collector’ın, söz konusu örneğin kaynaklarını serbest bırakırken Dispose() metodunu çağıracağı anlamına gelir. Bu yüzden IDisposable arabirimini uyguladığınızı belirtmenize bile gerek yoktur. Hatta eğer belirtirseniz derlemeden önce hata alacaksınız.

ref struct nedir? Hatırlamak isteyen aşağıya bakabilir. Bakmanızı tavsiye ederim:

ref struct bonus

Hatırlarsak eğer Microsoft C# 7.2 ile beraber ref struct’ı tanıtmıştı. Şimdi kısaca ref struct’ı hatırlayalım:

Evet şimdi istisna bir durumdan bahsedelim. Bu yazıda da söylediğimiz ve sizin de defalarca duyduğunuz şöyle bir bilgi var:

class’lar referans tipli değişken olduklarından heap üzerinde yaşarlar. struct’lar değer türlü değişken olduklarından stack’de yaşarlar.

Aslında bu tam olarak böyle değil. Nedenini örnek üzerinden açıklayalım:

person adında bir struct tanımlayalım:

Bellekte ki görüntüsü aşağıdaki gibi olacaktır:

Boxing sonucunda bellekte ki yeni görüntüsü aşağıdaki gibi olacaktır:

Sizin de gördüğünüz üzere boxing sonucunda heap’e değer türünde bir değişken konulmuştur. Şimdi diğer örneğimize geçelim:

Bu örneğimizde ise gördüğünüz üzere PersonClass’ı içine PersonStruct referansı konulmuştur. Şimdi bellekte ki görüntüsüne bakalım:

PersonClass sınıfından bir nesne örneği oluşturduğumuzda bu nesne örneği bir PersonStruct değişkeni içereceğinden PersonStruct değişkeni heap üzerinde oluşturulacaktır. O zaman bu örneklerden struct’ların heap üzerinde tanımlanabileceği sonucuna varabiliriz. İşte burada tam olarak ref struct devreye giriyor. ref struct sayesinde struct’ın stack dışında oluşturulmasını engelleyebiliriz. Yukarıdaki örneklerde var olan struct tanımlarını ref struct olarak değiştirdiğimizde derleyici ilk örnek için boxing işlemine izin vermeyecektir. İkinci örnek içinde sınıf içerisinde ref struct örneğini tanımlamaya izin vermeyecektir.

Roslyn Arka Planda Ne Yapıyor?

Şimdi ise aşağıdaki gibi positional olarak bir person readonly record struct’u oluşturalım:

Not: Roslyn branches main(9 Aug 2022)

Evet record struct’un IEquatable interface’ini uygulayan bir struct olduğunu görmekle beraber, readonly anahtar kelimesi sayesinde tüm protperty’lerimiz init-only olarak oluşturulduğunu fark etmişsinizdir. Diğer fonksiyonların davranışlarının record’ın lowering’i yapılmış hali ile aynı olduğu için tek tek hepsine değinmeyeceğim. Son olarak performans açısından, struct’ları kullanmanın faydalı olacağından da bahsetmiştik. Fakat struct yerine record struct kullanmak, karşılaştırmalara göre 20 kat daha hızlıdır. Bakınız: https://nietras.com/2021/06/14/csharp-10-record-struct/

record class ve record struct’lar C# 9–10 ile gelen güzel özellikler olmasının yanı sıra doğru yerde kullanıldıklarında performans ve kolaylık sağlamaktadırlar. Benim bu konu hakkında söyleyeceklerim ve araştırmalarım bu şekildeydi . Yeniden bir yazının daha sonuna gelmiş bulunuyorum. Eğer gözümden kaçan yazım hatası veya anlatım bozukluğu olduysa şimdiden kusuruma bakmayın. Yazım umarım faydalı olmuştur. Okuduğunuz için teşekkürler. Bir sonraki yazıda görüşmek üzere!

Yararlandığım Bazı Kaynaklar:

--

--

Yetenekli ve bilgili geliştiricilerden oluşan bir topluluk!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store