23 Mayıs 2020 Cumartesi

C# 9.0 ile Gelen Yenilikler


    S.A. Arkadaşlar,
    Bugün C#'in en son yayımlanan sürümü olan 9.0 sürümünü ele alacağız. Yapılan yenilikler nelerdir, neler değişmiş onları anlatmaya çalışacağız. Daha önceki sürümlerde olduğu gibi Microsoft yine çok güzel özellikler geliştirmiş, fakat bu sefer veri şekillerini kısaltmak(okunurluk) ve veri şekillerinin değişmezliğine(performans) odaklanmışlar. Bunun yanı sıra fonksiyonel programlamayı anımsatan değişiklikler de var. Tüm bunları elimizden geldiğince incelemeye çalıştık.
   
     Init-only properties
     Objelerin başlangıçta tanımlamasının ne kadar önemli olduğunu anlatmaya gerek yoktur herhalde. Özellikle de obje içinde obje tanımlamalarında daha da önemli hale gelmektedir. Kodun okunurluğunu oldukça etkilemektedir. Buraya basit bir kod örneği bırakalım. public class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}
var person1 = new Person();
person1.FirstName = "Malik";
person1.LastName = "Masis";

var person = new Person
{
    FirstName = "Malik",
LastName = "Masis"
};
      Burada dikkat edilmesi konu ilk tanımladığımız şekilde objeyi init kullanımı ile set etmemize izin vermezken objeyi ilk defa oluştururken değer ataması yapabileceğiz. Bu da bize objenin o değerini değişmediğini garanti edecektir.

    Aşağıdaki tanımlamamıza dikkat edecek olursanız init keyword'ü geldiğini fark edeceksiniz. Eski sürümlerde bunu derlemek istediğinizde get veya set değeri beklenmektedir gibi bir hata alacaksınız. C# 9.0 ile birlikte bu yeni keyword ne anlama gelmektedir. Değerimizin set keyword'ünü kullandığımızda herkes tarafından değiştirilebilmektedir, set değerini kaldırdığımızda ise objeye değer atayamıyoruz. (Aslında atayabiliyoruz ama okunurluk oldukça düşüyordu, aşağıdaki örneği inceleyebilirsiniz). init keyword'ü sayesinde sadece ilk atamalara izin verilmektedir, fakat daha sonradan her hangi bir değeri set edemiyoruz sadece okuyabiliyoruz. 
public string FirstName { get; init; }
private string lastName;
public string LastName
{
    get { return lastName; }
    set { lastName = "Masis"; }
}
     Yukarıda görmüş olduğunuz kod parçası C#9.0 ile gelen ve kod okunurluğunu daha kolay hale getirmektedir. Aşağıda ise full property dediğimiz eski sürümlerden beri süre gelen kod ise daha karmaşık görünmektedir.
    
    Init accessors and readonly fields
    Yukarıda da bahsettiğimiz gibi init keyword'ü sadece başlangıçta değer atamayı kabul ettiğinden readonly ifadesi gibi de düşünebiliriz ve bu sayede yapılandırıcı metod (constructor) içinde değer ataması yapabiliyorduk. (readonly için daha fazla bilgi burada) Burada da yine benzer bir yaklaşım mevcuttur. Aşağıdaki kod inceleyelim.
public class Person
{
    private readonly string firstName;
    private readonly string lastName;

    public string FirstName
    {
        get => firstName;
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
    }
    public string LastName
    {
        get => lastName;
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}
   Records   
   init keyword'ü değişmez değerler oluşturmak için mükemmel çözümler sağlamaktadır. Fakat bütün objeyi değişmez yapmak istiyorsanız o zaman daha fazlasına ihtiyacınız vardır. O da record'tur. O zaman da data keyword'ünü kullanmanız gerekecektir. Objenin elemanlarını tek tek değiştiremiyorduk fakat bütün objeyi değiştirme tehlikesini de bu şekilde ortadan kaldırmış olduk. Artık objenin tamamen değişmez olduğunu garanti ettik. Aşağıdaki kod yapısı inceleyelim.
public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}
   data keyword'ü ile objeyi değişmez kılıyoruz. Artık objeden ziyade bunu değer olarak görmemiz daha doğru olacaktır. 

   With-expressions
    Değişmez verilerle çalışırken eski objeye yeni değerler atamak genel bir yaklaşımdır. Örneğin kişi(Person) objemizde soyad(LastName) kısmını değiştirmek istiyoruz. Bunu yapmak için beklediğimiz soyad ile yeni objeyi kopyalamak gerekirdi. Bu işlemi yapmak ise büyük bir maliyettir. Bilindiği üzere new keyword'ünü kullanmak en maliyetli işlerdendir. Bu sebeple with keyword'ü kullanarak bu maliyeti azalmaya yoluna gittiler, yeni objeyi istediğimiz yeni değeri ile oluşturabileceğiz.
var otherPerson = person with { LastName = "Kaya" };
   Bu sayede objeyi oluştururken kolayca istediğimiz property'leri değiştirebiliriz. Birden fazla değiştirme yapmak istiyorsak virgül ile ayırmamız yeterli olacaktır.

    Burada bir not geçmek istiyorum, obje değişiklerini tek tek karşılaştırmak büyük maliyet olduğundan bahsetmiştik, bu yüzden bu yapıyı oluşturdular, buna ek olarak sonraki sürümlerde belki Observable bir yapı ile bunu daha da geliştirebilirler.

   Value-based equality
   C#'ta bilindiği üzere her şey bir objedir ve her şey obje sınıfından türer. Equls(object) de yine bu sınıftan türemiş sanal bir metoddur. Değerler null olmadığında statik metod olan Equals(object,object) kullanımına dayanır.  Struct'lar bu yapıyı ezerek (override) tüm değerleri tek tek karşılaştırır. Record da aynı yapıyı kullanır. Yani bunun anlamı şudur, 2 nesne birbirinin aynısı olmadan da eşit(belki denk kelimesi daha doğru bir ifadedir) olabilir. Çünkü baz aldıkları obje değil, değerleridir.
var originalPerson = otherPerson with { LastName = "Kaya" };
bool equals = Object.Equals(originalPerson , otherPerson );
bool referenceEquals = Object.ReferenceEquals(originalPerson
, otherPerson);
    Burada ilk değerimiz true olacaktır. Çünkü değerler aynıdır. (Siz bunu with ile değil de eski yöntemlerle denerseniz değeriniz false olduğunu göreceksiniz.) 2'nci çıktı ise false olacaktır. Çünkü burada objeler aynı değildir. Bu işlemi beğenmediğiniz takdirde tabi ki metodu ezerek istediğiniz şekle çevirebilirsiniz.

     Data members
     Record değişmezlik üzerine kurulmuş bir yapıdır. Bu yüzden public olan property'lerde basitçe bu işi artık tanımlayabiliriz. Sınıf ve struct tanımlamalarında varsayılan(default) olarak init olarak kabul edilir. 
public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}
public data class Person { string FirstName; string LastName; }
private string FirstName;
    Yukarıda kod parçalarını tanımlarsak bu özellik sayesinde 1.bölümdeki uzun kod yerine 2'ncisini basitçe tanımlayabiliyoruz. İkisi aynı işlemi yapmaktadır. Burada dikkat edilmesi gereken şey ise property'ler varsayılan olarak public'tir. Bunu private olarak tanımlamak için başına sadece private yazmak yeterli olacaktır.

     Positional records
     Bazen de değişkenleri yapılandırıcı metod(constructor) içinde argüman olarak kullanmak daha kullanışlıdır. Bu tarz durumlarda aşağıdaki kodu yazmanız gerekecektir.
public data class Person 
{ 
    string FirstName; 
    string LastName; 

    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);

    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}
   Fakat aynı işi yapan çok daha kısa bir tanımlama mevcuttur.
public data class Person(string FirstName, string LastName);
   Bu tanımla ile artık public init constructor ve deconstructor kullanımını şu şekilde yapabilirsiniz.
var person = new Person("Malik", "Masis"); // positional construction
var (f, l) = person;                        // positional deconstruction
    
    Records and mutation
    Dictionary gibi yapılarda her hangi bir değişkenin değişmesi ona bağlı diğer değişkenleri de değiştireceğinden aradığınız değeri bulamayabilirsiniz. Bunun gibi durumlarda obje sınıfının metodlarını ezerek elle ayarlar yapmak zorunda kalabilirsiniz.

    With-expressions and inheritance
    Herkesin bildiği üzere değer tabanlı eşitlikler kalıtım ile değişir. Şimdi Person sınıfımızdan bir adet Student sınıfı üretelim. Daha sonra da Student sınıfından bir kayıt üretelim fakat bunu Person objesi olarak saklayalım.
public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }
Person person = new Student { FirstName = "Malik", LastName = "Masis", ID = GetNewId() };
otherPerson = person with { LastName = "Kaya" };
    Son satır kodda derleyici doğal olarak Person objesinin Student objesinden üretildiğinden haberdar değildir. Bununla beraber, eğer Person objesi Student sınıfından üretilmemiş olsaydı bu doğru bir kopyalama işlemi olmayacaktı.

    Bu işin gerisini C# bizim için hallediyor. Nasıl mı? Record gizli sanal bir metoda sahip ve bu vesileyle bütün objeyi kopyalıyor(clone). Her türetilmiş record bu metodu ezerek üst(base) sınıftaki kopya olan yapılandırıcı metodu çağırır. With-expression bunu basitçe gizli olan klonlanmış metodu çağırıyor ve objeyi oluşturur.

   Value-based eqaulity and inheritance
   With-expression ile benzer olarak çalışmaktadır. Ancak burada farklı olan bir şey var. Şöyle ki, eğer gerçekten 2 tane obje karşılaştıracak olursak burada bir karmaşa ortaya çıkacaktır. Kodu inceledikten sonra devam edelim.
Person person1 = new Person { FirstName = "Malik", LastName = "Masis" };
Person person2 = new Student { FirstName = "Malik", LastName = "Masis", ID = GetNewId() };
    Bu iki obje sizce birbirine eşit midir? Sanki hayır ne alakası var der gibisiniz, ancak biraz yukarıda bahsettiğimiz ve üst sınıfın yapılandırıcı metodunu kopyalama işlemini hatırlarsanız aslında bu iki objenin de eşit olacağı sonucu ortaya çıkmaktadır. Ancak, C# yine bu işi bizim için kendi çözer. Nasıl mı? Record sanal protected EqualityContract adında bir property bulundurur. Her türetilmiş record bunu ezerek objelerin karşılaştırma yapabilmesi için aynı sözleşmeye sahip olmaları gerekmektedir.

    Top-level programs
     Bildiğiniz üzere basit bir C# kodu yazmak için basma kalıp bir kaç şey kullanmak zorundaydık, ama artık değiliz. 
using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}
using System;

Console.WriteLine("Hello World!");
   Bu kodu bu şekilde yazabiliyor olmanız her yerde yazabileceğiniz anlamına gelmez tabi ki. Bunu sadece bir dosyada yapabiliyoruz. Çünkü bir tane main olacağı mantığıyla oluşturulmuştur. 

    Improved pattern matching
    Bir kaç yeni tasarım şablonu eklendi. Burada bazılarından bahsedeceğiz. Açıkçasını söylemek gerekirse benim en burun kıvırdığım özellikler genelde switch-case yapısı ile ilgili olanlar oluyordu (daha önceki sürümlerde de yine bu konuda sık geliştirmeler oldu). Bunun sebebi nedir diye konuşurken if'e kıyasla performans bakımından daha iyi olduğundan kullanımı kolaylaştırarak kullanıcılara burayı daha sempatik göstermek istemiş olabilirler diye düşündük. 
public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
       ...
       
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };
Simple type patterns
DeliveryTruck => 10.00m,
Relational patterns
DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},
Logical patterns
DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},
not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
if (!(e is Person)) { ... } // type checking yazıma buradan ulaşabilirsiniz
if (e is not Person) { ... }
// bunun gelmesi gayet güzel olmuş. Çünkü ! okunurluğu düşürüyor tartışması oluyordu.
  
     Improved target typing
     Context içinden tipini almak için kullandığımız ifadedir. null ve lambda ifadeleri her zaman buna örnektir. Buna yeni ifadeler eklendi.
Target-typed new expressions
Point p = new (3, 5);
//new keywordü yanına obje sınıfını tekrardan koymaya gerek yok
var p = new Point(3,5); // sizce hangisi :)
//var ve dynamic kullanımı ile ilgili blog yazım
Target-typed ?? and ?: //konuyla alakalı blog yazım
//Aynı olmayan tipler arasında karşılaştırma yapılmıyordu.
//Ortak noktaları olmak kaydıyla artık yapılabilinecek
Person person = student ?? customer; // Shared base type
int? result = b ? 0 : null; // nullable value type
     
     Convariant Returns
     Bazen türetilmiş sınıftaki metodu ezip dönüş tipini farklı vermek işe yarayabilir. Örneğin hayvanlar sınıfından türeyen 2 sınıfımız var. Bunlar etle beslenenler, otla beslenen veya karışık beslenenler olabilir. Bu tarz durumlarda direkt geçiş yapamaz, objeyi cast etmemiz gerekirdi. Bu işlemi yaptıktan sonra intellisense özelliği ile artık onu görebiliyorduk, ama bu sayede bu tarz işlemleri yapmak zorunda kalmayacağız.
abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}
     Teknolojileri takip eden olmaktan ziyade, onlara yön vermeye geçmek dileğiyle...
     
     Not: Yazıyı asıl kaynağından okumak isteyenler buradan okuyabilir.

Hiç yorum yok:

Yorum Gönder