C# 9.0 İle Gelen Yeni Özellikler
Selamlar,
Bu makalede C# 9.0 ile gelen yeni özellikleri beraberce madde madde inceleyeceğiz. Son 15 yıldır, özellikle büyük kırılımların olduğu farklı bir çok versiyona şahit oldum. Örneğin “Linq” veya “Genericsler” gibi. Bence işte bu büyük kırılmalardan birinin olduğu bir versiyon ile karşı karşıyayız.
Ön Hazırlık:
Ben kendi adıma, Preview versiyonu ile çalışır iken var olan sistemin bozulmaması için, Virtual Machine olarak harici bir makina kurup, aşağıdaki uygulamaları indirdim.
- Öncelikle .Net 5.0 buradaki linkden indirilebilir. Bu makale itibari ile, SDK 5.0.100-rc.1versiyonu indirilmiştir.
- Ayrıca Visual Studio 2019 Preview buradaki linkden indirilebilir. Bu makale itibari ile, v16.8 versiyonu indirilmiştir.
“Değişim, ancak içeriden açılabilen bir kapıdır.” —Terry Neil
Hadi şimdi gelin C# 9.0’un bize kattığı yenilikleri, hep beraber inceleyelim.
1-)Top-level statements:
Özellikle Microservis uygulamalarda bolca kullandığım, Console Applicationlardaki “class” ve bir “Main()” methoda(yani bir başlangıç noktasına) gerek duyulmadan, sadece amaçlanan kod artık yazılabilmektedir. Peki başlangıç noktasını nasıl belirlenmektedir ? Tabi ki dosya’nın ismi ile (Program.cs)
Öncelikle gelin, yeni bir .Net 5.0 Console Application oluşturalım. Ve otomatik oluşan kodları aşağıdaki yeni kod ile değiştirelim.
Console Uygulamasının çalıştırılacağı Target framework seçimi:
Eskisi:
1 2 3 4 5 6 7 8 9 10 11 12 |
using System; namespace Test { class Program { static void Main(string[] args) { Console.WriteLine("Selam Millet!"); } } } |
Yeni: C# 9.0
İşte bu kadar kısa :)
1 2 3 |
using System; Console.WriteLine("Selam Millet"); |
2-)Init-only properties:
Nesnenin sadece oluşturulması esnasında atanabildiği özellikleridir. Init tanımlı property, sonrasında sadece readonly olarak çalışılabilir. Yani değiştirilemez. Bir propertyde hem “set“, hem “Init” aynı anda kullanılamaz. Sadece biri seçilmelidir.
Aşağıda, Motor sınıfının yaratılma esnasında atanmış olduğu “init Model” property, sonradan değiştirilmeye çalışıldığında alınan hata gösterilmiştir.
“Geçmişin arabalarıyla hiçbir yere gidemezsiniz.” —Maksim Gorki
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Program { static void Main(string[] args) { var honda = new Motor { Model = "CBR650R", Color = "red", Year = 2020, Price = 90000 }; honda.Model = "CBR1000RRR"; //HATA honda.Color = "Dark Black"; //DOĞRU } } public class Motor { public string Model { get; init; } public string Color { get; set; } public int Year { get; init; } public decimal Price { get; init; } } |
3-) Target-typed new expressions:
Tipi öncesinden belli bir nesne oluşturulurken, constructor çağrılması sırasında ayrıca tekrardan tipinin belirtilmesine gerek yoktur. Şahsen bu özellikle, yeni bir “List” oluştururken, her seferinde ayrıca tipini belirtmekten kurtulduğum için çok seviniyorum :)
Eski:
1 |
List<Motor> motorList = new List<Motor>(); |
Yeni:
1 |
List<Motor> motorList = new(); |
Örnek Kullanım: Aşağıdaki örnekte, Motor sınıfı tanımlanırken motorList propertsi “new()” Expression ile tanımlanmıştır. Ayrıca Main() methodunda, “Motor honda” gene aynı şekilde tipi bir daha yazılmaya gerek duyulmadan oluşturulmuştur. Ve son olarak “honda.motorList“‘e, yeni Model gene new() Expression ile eklenmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void Main(string[] args) { Motor honda = new() { Model = "CBR650R", Color = "red", Year = 2020, Price = 90000 }; honda.motorList.Add(new() { Model = "CBR650R", Color = "black", Year = 2019, Price = 57000 }); } public class Motor { public string Model { get; init; } public string Color { get; set; } public int Year { get; init; } public decimal Price { get; init; } public List<Motor> motorList = new(); } |
4-) Records: Benim adıma C# 9.0 ile gelen en önemli yenilik:
Sınıflara çok benzeyen ama sınıflara göre ayrıca bir çok artı özelliği olan yapılardır. Inheritance, yalnızca kendi tipleri arasında olabilmektedir. Yani bir “class” => “record“‘dan, ya da “record” => “class“‘dan türüyemez. Recordların esas çıkış noktası, içerisindeki datanın yönetimidir. Class’a nazaran record tanımlaması, nesneyi immutable ve value type’a çevirir.
1 2 3 4 5 6 7 |
record Motor { public string Model { get; init; } public string Color { get; set; } public int Year { get; init; } public decimal Price { get; init; } } |
5-) *Record with Expressions:
C# 9.0’dan var olan bir nesnenin sadece bir kaç özelliğini değiştirip, diğer özelliklerini aynen klonlayarak yaratılmasını sağlayan benim favori özelliğimdir. Bu design patternlerdan, Prototype‘ın, keyword halidir :)
Aşağıda görüldüğü gibi “blackHonda”, “honda”‘nın sadece “Color” özelliğini override edip, diğer özelliklerini Deep Copy yaparak, yani farklı bir referance tipinde aynen almıştır. Yukarıda görüldüğü gibi ekrana yazdırıldığında, color haricinde tüm özelliklerini honda’dan almış, sadece color özelliğini ezerek “Color = “‘Dark Black'” yapmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Motor honda = new() { Model = "CBR650R", Color = "red", Year = 2020, Price = 90000 }; var blackHonda = honda with { Color = "Dark Black" }; WriteMotor(blackHonda); static void WriteMotor(Motor motor) { WriteLine($"1-){motor.Model}\n2-){motor.Color}\n3-){motor.Year}\n4-){motor.Price}"); } |
6-)Record Objects comparison Equals – ReferenceEquals :
Şu an için classlar’da kullanılamayan ama ilerde recordlarda bolca kullanacağımız iki nesnenin referans ve değer olarak karşılaştırılmasını inceleyeceğiz.
- ReferenceEquals() methodu ile iki nesnenin bellekte tutulduğu referance adresleri karşılaştırılır. Aşağıdaki örnekte “ReferanceEquals(honda,blackHonda)” methodu sonucu, blackHonda’nın honda record’undan “with” keyword’ü ile türetilmesinden dolayı “false“‘dur. Çünkü “with” keyword’ü, yeni bir nesne oluştururken referance’ı farklı, ve var olan özellikleri override edilmedikçe deep copy şeklinde kopyalayarak oluşturur.
- Equals() methodu ile iki nesnenin value olarak eşit olup olmadığına bakılır. Aşağıdaki örnekte colorHonda, honda’dan aynı renk kullanılarak oluşturulmuştur. Equals(colorHonda,Honda) ==> İki nesnenin de değerleri birbiri ile aynı oluğu için, sonuç True‘dur.
- Equals(honda,blackHonda): blackHonda, honda record’undan “Color” özelliği değiştirilerek oluşturulmuştur. Bu nedenle iki nesnenin değerleri aynı değildir. Ve sonuç False‘dur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Motor honda = new() { Model = "CBR650R", Color = "red", Year = 2020, Price = 90000 }; var blackHonda = honda with { Color = "Black" }; var colorHonda = honda with { Color = "red" }; WriteLine($"Referance Honda&Black Equals:{ReferenceEquals(honda, blackHonda)}"); WriteLine($"Equals Color&Honda :{Equals(colorHonda, honda)}"); WriteLine($"Equals Black&Honda:{Equals(honda, blackHonda)}"); |
7-) Positional records:
Positional Recordlarda, constructor ve deconstructor kullanımına hem izin verilmekte hem de çok pratik bir yolu bulunmaktadır.
İlk oluşturma sırasında bir record’u doğrudan constructor’ı ile birlikte propertylerini parametre vererek, yaratılmasını sağlıyabilirsiniz. Hem de bunu ilgili record’da, constructor tanımlamadan yapabilirsiniz :)
Eski Yöntem: Klasik bir Car record, Name ve Year init propertyleri ve son olarak Car() Constructor ve Deconstructor tanımlaması aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 |
public record Car { public string Name { get; init; } public int Year { get; init; } public Car(string name, int year) => (Name, Year) = (name, year); public void Deconstruct(out string name, out int year) => (name, year) = (Name, Year); } |
” ‘Değişim ne zaman gerekli?’ sorusuna verilecek en iyi yanıt, gerekli hale gelmedendir.” —Claus Moller
Yeni Yöntem: Aşağıda görüldüğü gibi student record’u oluşturulurken, sanki bir methodmuş gibi () => constructor ve parametreleri hemen yanında tanımlanmıştır. “record Student(string name, string surname, int no)“. Bu tanımlama şekli sadece recordlara özeldir. Bu şekilde tanımlanan propertyler, “Init” olarak tanımlanmakda ve ilk yaratılma anındaki atanmadan sonra, readonly olarak kullanılmaktadırlar.
Yaratılma anında tanımlanan init propertyler, bir daha değiştirilemezler.
- “public string Name => name” : Tüm propertyler, lambda expression kullanılarak atanmıştır.
- “var bora = new Student(“Bora”, “Kasmer”, 1923)”: Çağrılma şekli her zamanki gibidir.
- “var (name, surname, no) = bora” : Bora record’una ait tüm propertyler, local değişkenlere atılıp aşağıdaki gibi ekrana basılmıştı.
Sonuç Ekranı:
1 2 3 4 5 6 7 8 9 10 |
var bora = new Student("Bora", "Kasmer", 1923); var (name, surname, no) = bora; WriteLine($"Student:\n1-)Name:{name}\n2-)Surname:{surname}\n3-)No:{no}"); public record Student(string name, string surname, int no) { public string Name => name; public string Surname => surname; public int No => no; } |
Bunu daha iyi bir hale getirmek ister isek, aşağıda görüldüğü gibi direkt property değerlerini büyük harfle atayabiliriz.
1 2 |
Student person = new("Bora", "Kasmer", 34); public record Student(string Name, string Surname, int No); |
Ama bu sefer de atanmış olan bir property, değiştirilmek istendiğinde “Init-only” olarak atandığı bize hatırlatılır.
8-)Positional Recordlarda Inheritance:
recordlar, aynı classlarda olduğu gibi Inheritance özelliğine sahiptir. Klasik yöntemin zaten bilinmesinden dolayı, Positional Recordların kalıtımına örnek vermek istedim. Aşağıdaki örnekte görüldüğü gibi, Kawasaki hem Honda hem de Motor recordlarından inherit alınmıştır. Yaratılma anında, base record’un constructor propertyleri de, her record oluşturulmasında tanımlanır.
Not: Ayrca aşağıdaki örnekte, Motor record’una dikkat edilir ise, Positional Record olarak tanımlanan Color propertysi default olarak Init şeklinde tanımlandığı için, sonradan değiştirilebilmesi için tekrardan getter setter olarak tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 |
using System; Kawasaki motor = new Kawasaki("ZX6R", "green", 2020, 161000, 280, 0.60); public record Motor(string Model, string Color, int Year, decimal Price) { public string Color { get; set; } } public record Honda(string Model, string Color, int Year, decimal Price,int maxSpeed) : Motor(Model,Color, Year, Price); public record Kawasaki(string Model, string Color, int Year, decimal Price, int maxSpeed, double perLiterCost) : Honda(Model, Color, Year, Price, maxSpeed); |
9-)Improved pattern matching:
Aşağıda görüldüğü gibi, motor record tipinde alınan araca göre switch pahalılık derecesi string olarak dönülmektedir.
- “Honda h when h.Price < 30000 =>”: Eğer gelen motor Tipi Honda ise ve fiyatı 30000₺’den küçük ise, “Ucuz Honda” dönülür.
- “Honda h when h.Price > 30000 & h.Price < 100000 =>” : Gene tipi Honda ve fiyat aralığı 30000₺ ile 100000₺ arasında ise, “Pahalı Honda” değeri dönülür.
- “Honda or Kawasaki =>” Artık “
Honda or Kawasaki _ =>” şeklinde bir tanımlama yapılmasına gerek kalmamaktadır. Eğer yukarıdaki koşullar sağlanmaz ise ve gelen motor tipi Honda veya Kawasaki ise “Honda veya Kawasaki çok pahalı” değeri dönülür. - “_ => throw new ArgumentException()”: Eğer belirlenen motor tipi tanımsız ise hata fırlatılır.
1 2 3 4 5 6 7 8 9 10 |
public static string GetPriceState(Motor motor) => motor switch { Honda h when h.Price < 30000 => "Ucuz Honda", Honda h when h.Price > 30000 & h.Price < 100000 => "Pahalı Honda", Kawasaki k when k.Price > 100000 => "Kawasaki hep Pahalı", Honda or Kawasaki => "Honda veya Kawasaki çok pahalı", _ => throw new ArgumentException("Bilinen bir motor değil", nameof(motor)) }; |
Soru 1-)
Yukarıdaki method, aşağıdaki örnekler ile çağrıldığında alttaki gibi bir sonuç alınır. İlk motor Honda 3000<x<10000 arasında olduğu için, “Pahalı Honda” sonucu yazmıştır.” İkinci motor >100000 olduğu için, “Kawasaki hep Pahalı” sonucu yazmıştır :)
1 2 3 4 5 6 7 8 9 10 11 12 |
static void Main(string[] args) { Honda motor = new Honda("cbr650r", "red", 2019, 90000, 250); Kawasaki motor2 = new Kawasaki("ZX6R", "green", 2020, 161000, 280,0.6); string priceType = GetPriceState(motor); WriteLine($"Gerçekten {priceType}"); string priceType2 = GetPriceState(motor2); WriteLine($"Gerçekten {priceType2}"); } |
Soru 2-) : Peki aşağıdaki örneğin sonucu ne olurdu? Koşulların hiçbirini sağlamayıp sadece Honda veya Kawasaki koşuluna uyduğu için, aşağıdaki sonuca ulaşılmıştır.
1 2 3 |
Honda motor = new Honda("cbr650r", "red", 2019, 180000, 250); string priceType = GetPriceState(motor); WriteLine($"Gerçekten {priceType}"); |
Soru 3-) : Son olarak aşağıdaki örnekten nasıl bir sonuç dönerdi? Bunun cevabı biraz kafa karıştırıcı ama nedeni basit. Girilen motor tipi Kawasaki iken, Honda motor sonucu nasıl karşımıza çıkmaktadır? Çünkü Kawasaki Honda’dan miras alınarak yaratılmıştır. Yani Honda olma koşuluna uymaktadır. Ayrıca fiyat aralığı da 3000<x<10000 arasında olduğu için, “Pahalı Honda” sonucu yazılmıştır.
1 2 3 |
Kawasaki motor = new Kawasaki("ZX6R", "green", 2020, 85000, 280, 0.6); string priceType = GetPriceState(motor); WriteLine($"Gerçekten {priceType}"); |
10-)Relational patterns:
Aşağıda görüldüğü gibi motor yılına göre vergi oranı hesaplanmıştır. Doğrudan “<” ve “>=” koşulu yazılabilmektedir. Ide gayet akıllı olduğu için en son koşulda “_ => motor.Price” hata vermektedir. Bundan dolayı yorumlanmıştır. Nedeni sondaki “< 2018” koşulu, geri kalan tüm koşulları sağlamaktadır. Bundan dolayı bir sonraki koşula gerek yoktur.
1 2 3 4 5 6 7 8 |
public static decimal GetPriceWithTax(Motor motor) => motor.Year switch { > 2019 => motor.Price * Convert.ToDecimal(1.2), >= 2018 and < 2020 => motor.Price * Convert.ToDecimal(1.1), < 2018 => motor.Price * Convert.ToDecimal(1.3), //_ => motor.Price }; |
Yukarıdaki method, aşağıdaki örnek ile çağrıldığında, alttaki gibi bir sonuç ile karşılaşılır. “motor.year > 2019” olduğu için ilk koşul sağlanır. Yani “85000 * 1.2” ile çarpılır.
1 2 3 |
Kawasaki motor = new Kawasaki("ZX6R", "green", 2020, 85000, 280, 0.6); decimal price = GetPriceWithTax(motor); WriteLine($"Motor Son Fiyatı {price}"); |
Convert if else statement to switch expression (Quick Actions):
Diyelim ki aşağıdaki gibi istenmeyen bir “if-else” yapınız olsun. “if”‘in üzerine gelinip, solunda çıkan ampul resmini yani “Quick Actions“‘ tıklanırsa, karşımıza yukarıdaki gibi bir menu çıkar. Bunlardan Convert to ‘switch’ expression‘ı seçilirse, yukarıdaki gibi Refactor Sanat Eserini ile karşılaşırız :) Ama bunun da bir kusuru var. O da, dikkat ederseniz son satıra “_ => motor.Price” eklemesidir. Ama az önce de anlatıldığı gibi, bu koşula hiçbir zaman gelinemeyecek ve derleme anında hata alınacaktır. İlgili koşulun kaldırılması ile uygulama derlenir. Burdan şu sonuç çıkıyor, Convert to ‘switch’ refactor algoritması Visual Studio 2019 IDE’si kadar, kodları analiz edebilecek düzeyde değildir.
1 2 3 4 5 6 7 8 9 10 11 |
public static decimal GetIfPriceWithTax(Motor motor) { if (motor.Year > 2019) return motor.Price * Convert.ToDecimal(1.2); else if (motor.Year >= 2018 && motor.Year < 2020) return motor.Price * Convert.ToDecimal(1.1); else if (motor.Year < 2018) return motor.Price * Convert.ToDecimal(1.3); else return motor.Price; } |
11-) Covariant returns:
Aşağıda görüldüğü gibi, bu özellik ile override edilen metodun dönüş tipinin de, derived sınıf tipinde olmasına olanak sağlanmıştır. Özellikle Factory pattern’de çok işimize yarayacaktır. Peki nasıl yarayacaktır ? Tüm methodları aynı ama mesela bir methodu farklı tip(kalıtım alan sınıf tipinde) dönen bir sınıfı, inherit edemiyor yenisini yaratmak zorunda kalıyorduk. İşte tam bu noktada, dönüş tipini de override edip, bu dertten kurtulabileceğiz :)
1 2 3 4 5 6 7 8 9 |
public abstract class Person { public abstract Person GetJob(); } public class Developer : Person { public override Developer GetJob() { return new Developer(); } } |
12-) Target typed ??
and ?:
Aşağıdaki ilk örnekte C# 9.0 ile aynı nesneden türemiş yapılar arasında, compiler tarafından convert işlemi otomatik olarak yapılmakta, ve uygulama hatasız derlenmektedir. Honda ve Kawasaki recordları, ortak Motor recordundan türemiştir. Aşağıdaki örnekte honda null olduğu için, kawasaki console’a yazılmıştır.
1 2 3 4 5 |
Honda honda=null; Kawasaki kawasaki = new Kawasaki("ZX6R", "green", 2020, 85000, 280, 0.6); Motor vehcile = honda ? kawasaki; WriteLine("Vehcile: "+ vehcile); |
Aşağıda görülen ikinci örnekte, kawasaki motorunun maxSpeed’i, null olarak girilmiştir.
- “Motor vehicle = honda ? kawasaki” : Tanımlanan motorun Kawasaki olmasından dolayı honda kontrolü null değer alır.
- “vehcile is Kawasaki?” : Doğrulamasında honda null olduğu için, kawasaki alınmış ve ilgili koşul sağlanmıştır.
- “((Kawasaki)vehcile).maxSpeed ?? 0” : Kawasaki aracının maxSpeed propertysi null girildiği için, “??” null kontrolü sağlanarak “0” değeri atanmıştır.
1 2 3 4 |
Kawasaki kawasaki = new Kawasaki("ZX6R", "green", 2020, 85000, null, 0.6); Motor vehcile = honda ? kawasaki; int? maxSpeed = vehcile is Kawasaki? ((Kawasaki)vehcile).maxSpeed ?? 0 : null; // nullable value WriteLine("Vehcile Max Speed: " + maxSpeed); |
Sonuç :
Kabaca C# 9.0 ile gelen yeniliklere göz attık. Bu sene C# 9.0 ile gelen yenilikler, diğer senelere oranla çok daha heyecan verici ve büyük gözüküyor. Bazı template tiplerinin kaldırılması, örneğin “ASP.Net Web Form”‘un kaldırılması üzse de, artık tek bir .Net tipinin olması yani .Net 5.0 ile .Net Core ve .Net Framework’ün birleştirilmesi, benim adıma gayet heyecan verici gelişmelerden sadece bir kaçı. Ayrıca Entity Framework Core 5.0 ile gelen yenilikler, örneğin Database collations’ın gelmesi ya da Flexible query/update mapping yapısı ile, query’nin => view ile çekilmesi ve update’in de=> table’a yapılarak, işem tipine göre farklılaştırmanın sağlanması, ilk etapta konuşulabilecek değişikliklerdendir.
“Artık değişmeyecek hale geldiğin zaman, bitmiş sayılırsın.” —Bruce Barton
1 2 3 4 |
modelBuilder .Entity<Blog>() .ToTable("Blogs") .ToView("BlogsView"); |
Bizi, daha birçok yeniliğin beklediğine emin olabilirsiniz. Yukarıda, .Net Framework’ün ilerleyen yıllara göre çıkması planlanan versiyonları gösterilmektedir.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Emeginize saglik
Teşekkür ederim..