21 Haziran 2023 Çarşamba

Bir Refactoring Hikayesi: Class vs Record vs Struct

     S.A. Arkadaşlar,

    Bugün, yazdığım bir parça kodun gelişim hikayesinden bahsetmek istiyorum. Hediye kodu tanımlama ile ilgili bir iş üzerinde çalışmam gerekti. Benzersiz hediye kodları üretip bunları veri tabanına yazmam gerekiyordu. Kodun ilk hali her oluşturulan kod için veri tabanına gidip kontrol edip eğer aynı kayıt yok ise bunu kaydediyordu. Bu belki de en yavaş çözüm olabilirdi. Biz bu kod parçasını geliştirmeye çalıştık. Hazırsak başlayalım.

    Birçoğumuzun aklına ilk gelen çözüm tabii ki bu işlemi toplu olarak kaydetmek gelir herhalde. Benim de aklıma gelen oydu. İlk öncelikle oluşturduğum kodları kontrol edip daha sonra kayıt işlemini tamamlıyordum. Bu yazı için odaklandığım kısım ise kodları oluşturduktan sonra listeye eklediğim kısım. Bu listeyi alıp dapper aracılığıyla belirli parçalara bölüp kaydediyorum. (Kodu ilk başta 2 metoddan yazmak yeterli olacak diye düşünürken yazı ilerledikçe kapsam genişledi. Bu yüzden kodun tamamını buraya taşıdım.)

var giftCodeList = new List<GiftCodeClass>();

for (var i = 0; i < giftCodes.Count; i++)
{
giftCodeList.Add(new GiftCodeClass
{
Code = giftCodes.ElementAt(i),
Amount = 50,
Desc = "Aciklama",
Partner = "Partner",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddYears(1),
CreatorId = 1
});
}

    Yukarıdaki kodda giftCodes (proje içerisinde bu değişken HashSet olarak tanımlandı, çünkü eklenen kodların benzersiz olmasını amaçlıyorduk) içerisinde 1 milyon kod olan bir liste olarak düşünebilirsiniz. Bir sınıf oluşturuyorum ve bunu sırasıyla koleksiyona ekliyorum. Daha sonra sınıf kullanımı yerine "record" yapısını kullanabileceğimi düşündüm. Kodu aynı bırakmak kaydıyla sınıfı record olarak değiştirdim. Fark ettiniz mi bilmiyorum ama kod hariç diğer değişkenler değişmiyor (gerçekte de öyleydi). Bu yüzden burada record ile birlikte kullanılan "with" anahtar kelimesini kullanabileceğimi düşündüm ve kodu aşağıdaki gibi değiştirdim. Yani ilk etapta elimizde ölçüme hazır 3 parça kod bulunuyordu.

var giftCode = new GiftCodeRecord
{
Amount = 50,
Desc = "Aciklama",
Partner = "Partner",
StartDate = DateTime.UtcNow,
EndDate = DateTime.UtcNow.AddYears(1),
CreatorId = 1
};

var giftCodeList = new List<GiftCodeRecord>();

for (var i = 0; i < giftCodes.Count; i++)
{
giftCodeList.Add(giftCode with { Code = giftCodes.ElementAt(i) });
} 

    Sizce de buradaki kod daha iyi değil mi? Hepimizin bildiği üzere yeni bir nesne oluşturmak oldukça maliyetli bir iş. Yukarıdaki örnekte listenin her elemanı için bir nesne oluşturup bütün değerlerini teker teker atıyorduk. Son örneğimizde ise bir tane nesne oluşturuyoruz. Daha sonra bunu her defa yapmak yerine "with" anahtar sözcüğü ile sadece değiştirmek istediğimiz alan olan kodu değiştiriyoruz ve bunu listeye ekliyoruz (shallow copy). Buraya kadar her şey güzeldi, fakat bu yazıyı yazmaya başladıktan sonra biraz araştırmalar neticesinde farklı seçenekler de karşımıza çıktı. C# 10 ile gelen "record struct" ve daha önceden beri var olan "struct" yapısını da eklemeye karar verdik. Ayrıca bunların "with" anahtar kelimesi ile birlikte kullanımlarını da ele aldık. Böyle 3 metod olan kodumuz 6 metoda çıkmış oldu. 

    Benchmark kütüphanesi ile aşağıdaki gibi bir çıktı aldık.



    Yukarıdaki tabloyu biraz inceleyecek olursak "record" ile "class" yapılarındaki değerlerin birbirine çok yakın olduğunu göreceksiniz. Hatta bazı metriklerde aynı olduğunu fark etmişsinizdir. C# kodu decompile olduğunda "record" aslında "class" yapısına dönüşmektedir. Burada belki belirtmemiz gereken nokta "record" yapılar immutable (değişmez) yapılar iken, "class"lar ise mutable (değişir) yapılardır. "Class"lar için benzer değişiklikler yapılarak değişilmez hale getirebileceğimizi de belirtelim.
Aşağıdaki ekran çıktısını inceleyebilirsiniz.

https://josipmisko.com/posts/c-sharp-class-vs-record

    Şimdi başka bir nokta ise "record" ile "with" anahtar kelimesinin birlikte kullanılması sonucu ortaya çıkan farklılık. "with" anahtar kelimesi "shallow copy" yani sığ kopyalama yapıyor, yani nesne olmayan değişkenleri (primative) alarak yeni bir nesne oluşturarak kayda değer bir performans farkı oluşturuyor. (Karmaşık yani nesne içeren işlemlerde bunu kullanamazsanız, bunu yapmak isterseniz "deep copy" yapmanız gerekecektir. Aradaki fark için hızlıca şuraya göz atabilirsiniz.)

    Şimdi ise yine C# 10 ile gelen başka bir özellik olan "record struct" yapısı ile daha önceden beri gelen ama yüzüne çok da bakılmayan "struct" yapılarına bir göz atalım. Bu iki yapının bazı metriklerinin "class" ve "record"lardaki gibi çok benzer veya aynı olduğunu görüyorsunuz. Burada da benzer bir decompile işlem olabileceğini düşünüyorum. "record", "struct" ve "class" arasında doğru kullanımda "struct" yapısının nasıl fark oluşturduğunu rahatlıkla görebiliyoruz. O yüzden ezberlerimizden kurtulup nerede ne kullanmamız gerektiğine dikkat edersek çok daha performanslı yapılar kurgulayabiliriz. "struct" yapısının diğerlerinden daha hızlı çalışmasının sebebi ise bellekte saklandığı yer. Bildiğiniz gibi "value type" olan yapılar "stack"da saklanır ve bu vesileyle yok edilmek için GC'a da ihtiyaç duymazlar.

    Son olarak ise "record struct" ve "struct" yapılarının gücünü "with" anahtar sözcüğü ile birleştirelim. (bu anahtar kelime "struct" ile de kullanıldığını hatırlatalım). Burada "with" anahtar kelimesi başlı başına öne çıkmayı başarmıştı ve burada birlikte kullanılması ile performansını daha da öne çekmektedir. 

    Bu arada benim karşılaştırdığım durumlar yeni bir nesne oluşturmasıyla ilgili metrikler. Farklı metriklerde farklı sonuçlar alabilirsiniz. Bu araştırmaları yaparken farklı durumları karşılaştıran şu yazıya da göz atmak isteyebilirsiniz.

    Yazıyı bitirmeden bazı notlar paylaşmak istiyorum.  Özetle

  •  "record" yapısı varsayılan olarak değişilmez değildir. Onu değişilmez yapmak isterseniz ya "init" anahtar kelimesini kullanmanız gerekir veya başlangıçta constructor ile birlikte kullanmalısınız.
  •  "record" yapısını C# 10 ile birlikte "class" ve "struct" için ayrı ayrı düşünebilirsiniz. Hatta C# 10 "record class" kullanımına da müsade ediyor. Varsayılan olarak bahsedilen ise "class" olan yapıdır. 
  • "record" ve "class" referans bazlı olmasına rağmen, "struct" değer bazlıdır. Referans bazlı olanlar "Heap"te, değer bazlı olanlar ise "Stack" saklanır.
  • "record" ve "class" sanki sürekli aynıymış gibi bahsetmemize rağmen "record" ve "struct" yapılarında karşılaştırma yaparken nesnenin değerine bakarken, "class"larda ise nesnenin referansının aynı olup olmadığına bakılır.
  • Değişmez, değişir kavramından çok bahsettik. Değişmez neslerin bazı avantajları: "thread safe" olmaları, yan etkilerinin az olması ve optimize edilebilirler olmaları iken dezavantajları ise öğrenme eğrileri ve bellek sorunu olarak belirtilebilir. (dahası)
  • Nerede hangisini kullanacağınıza kabaca bakacak olursak, değer bazlı bir yapı var ise "struct", değişmez olmasını istediğiniz yapılar var ise "record" (DTO, Even vs) ve diğer kalan durumlar için "class" tercih edebilirsiniz. (dahası)


 Konu dışı
    Bazen yazı yazmanın size ne gibi faydası oluyor diye soran arkadaşlar oluyor. Bu yazı öğrenmenin en somut göstergelerinden biriydi benim için. Yazıyı yazmadan önce aklımda olan şeyler ile yazıyı yazmaya başladıktan sonra araştırdığım nokta arasında büyük bir fark var. Yazıya başlamadan önce 2-3 saatte aklımdakileri yazar, bitiririm derken bu yazıyı yaklaşık 1 haftada (belirli aralıklarla) birçok farklı yazı, metric ve kodu denemeleri sonucu ele alabiliyorum.

 

         Yazıyı, konu dışında da bahsettiğimiz gibi, bildiklerini aktarmanın öneminden bahseden bir hadisi şerif ile bitirelim. 

Sevgili Peygamberimiz(s.a.v), “Sadakanın en faziletlisi bir Müslüman’ın, öğrendiklerini Müslüman kardeşine öğretmesidir” buyurur.  İbn Mâce, Sünnet, 20.

 


Hiç yorum yok:

Yorum Gönder