Umut Özel    About    Archive    Feed

C# 8 Yenilikleri

C#, 7.0 büyük versiyonun üstüne 7.1, 7.2 ve 7.3 ufak geliştirmeleriyle büyük beğenimizi kazanmıştı. Şimdi 8.0 ile Tony Hoare tarafından Milyar Dolarlık Hata olarak adlandırılan null referans konusunda yenilikler, fonksiyonel dillerden bildiğimiz Pattern Matching ve range ve yıllardır keşke olsa dediğimiz, sık sık major versiyon yükseltmelerine sebep olan arayüz uyumsuzluğuna bir çözüm getiren Default Interface Members gibi bir çok yeniliği içeriyor. Sırayla görelim bakalım.

Neler Geliyor

Readonly Members

async anahtar kelimesini hatırlarsınız. Sık sık Async all the way cümlesini görmüşsünüz hatta bu durumu yaşamışsınızdır. Bir metodu Task dönecek şekilde async hale getirdiğimizde bu metodu çağıran tüm zinciri de async yapmamız gerekirdi. Burada da benzer bir durum var. Bir üyeyi readonly yaptığımızda derleyicinin başımızın etini yememesi için sırayla tüm zincirin readonly olmasını sağlamamız gerekiyor. Örneğe bakalım:

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Distance => Math.Sqrt(X * X + Y * Y);

    public override string ToString() =>
        $"({X}, {Y}) is {Distance} from the origin";
}

Point sınıfının ToString metodu sınıf üzerinde bir değişiklik yapmaması gerekiyor (fonksiyonel dillerden pure fonksiyonları hatırlayın). Dolayısıyla bu metodu readonly olarak işaretleyebiliriz.

public readonly override string ToString() =>
    $"({X}, {Y}) is {Distance} from the origin";

Ancak bu durumda derleyici içeride kullanılan Distance property’sinin bir yan etkisi olmadığına emin olmak istiyor. Bu property’yi de readonly olarak işaretlememiz gerektiğine dair aşağıdaki uyarıyı alıyoruz.

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'

Gerçi biz Distance‘ın bir değişiklik yapmadığını biliyoruz. Hemen gerekli düzeltmeyi yaptığımızda uyarı ortadan kalkıyor.

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

Şimdi akıllara şu soru gelebilir: set içermeyen -yani sadece okuma yaptığımız- bir property için neden readonly eklememiz gerekti?

Çünkü C# property’leri Java’dan alşık oluan setXXX ve getXXX metodlarına benzer bir işi bize yazım kolaylığıyla sunuyorlar. Dolayısıyla get fonksiyonu içinde de sınıf state’ini değiştirebiliriz.

private int _nameReadCount = 0;

private string _name;
public string Name {
    get {
        // burada sınıf state'i değiştiriliyor
        _nameReadCount++;
        return _name;
    }
}

Niyetimizi derleyiciye açıkça belirterek bize tutarlılığı sağlamamızda yardımcı olmasını sağlıyoruz.

Nullable Reference Types

Biliyorsunuz C# dilinde tüm referans tipler null değeri alabilir. Ancak bazen bunların null almasını istemeyiz, bazen de null içermeyeceklerinden emin olabiliriz. Yine derleyicinin bize sağladığı yeni yaklaşım ile tutarlılığımızı kontrol etmesini, hata yaptığımız durumlarda kibarca bizi uyarmasını sağlayabiliyoruz.

Burada benim tahminim daha sakin bir geçiş için varsayılan tüm referans tipleri nullable kabul edip, bizim istisnaları işaretlememize izin verecekleri yönündeydi.

I was wrong

Tam tersini tercih ettiler, tüm referans tipler non-nullable, yani bir yerde null ataması yapacaksanız değişkeninizi nullable olarak işaretlemeniz gerekiyor. Jon Skeet‘in bu geçişi Noda Time ile yaparken yaşadıklarını anlattığı yazısını mutlaka okumanızı tavsiye ederim.

string s = null;
// Warning: Assignment of null to non-nullable reference type

Dolayısıyla kodlarınızda yukarıda gibi kodlar varsa derleme sırası çıkan uyarıları temizlemek için kolları sıvamanız gerekiyor.

string? s = null;
// sorun yok

Peki diğer etkileri nasıl?

void Test(string? s)
{
    // "s" null olabilir, dolayısıyla azarı yiyoruz
    // Warning: Possible null reference exception
    Console.WriteLine(s.Length);
    if (s != null)
    {
        // artık "s" değerinin null olamayacağını derleyici biliyor
        Console.WriteLine(s.Length);
    }
}

Bu özellikte, bir önceki gibi bizim bir taahhütte bulunmamız ve derleyicinin bunun gerekliliklerini yerine getirip getirmediğimizi kontrol etmesi ve gerektiğinde bizi kibarca uyarması üzerine kurulu :)

Default Interface Members

Soyut Abstraction

Artık neredeyse hepimiz kütüphanelerimizi birer NuGet paketi olarak yayınlıyoruz. En can sıkıcı konulardan birisinin de versiyonlandırma olduğunu biliyoruz. Bir şeyleri soyutlamak için yazdığımız bir interface’imize yeni bir metod ya da özellik eklediğimizde mecburen major versiyon geçişi yapmamız gerekiyor (Breaking Change). Peki bu metod ve özellik için varsayılan bir implementasyon verebilsek?

public interface ILogger
{
    void Log(LogLevel level, string message);
}

Yukarıdaki interface paketimizin 1.0.0 versiyonu ile yayınlandı. Şimdi de hataları loglayan yeni bir metod eklemek istiyoruz.

void Log(Exception ex);

Bu metodu interface’e eklediğimiz anda breaking change yapmış oluruz ve 2.0.0 versiyonu yayınlamamız ve paketimizi kullanan herkesin kodlarında güncelleme yapması gerekir.

C# 8 sayesinde, interface’imizi aşağıdaki değiştirir ve 1.0.1 versiyonumuzu yayınlarsak, paketimizi kullanan kişilerin kodlarında değişiklik yapmaları gerekmeden bu yeni metoda sahip olmalarını sağlayabiliriz.

public interface ILogger
{
    void Log(LogLevel level, string message);
    // varsayılan davranış ile yeni metod
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString());
}

class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { ... }
    // Log(Exception) varsayılan kod çalışacak
}

Daha detaylı bilgi için https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/default-interface-members-versions adresine bakmanızı tavsiye ederim.

Async Streams

JavaScript ve TypeScript ile haşır neşir olanlar AsyncIterator kavramını duymuşlardır. Aynısının adı farklı olanı :)

Benim dikkatimi çeken yield nedense bir türlü C# geliştiricilerinden gerekli ilgiyi ve takdiri görmüyor. Async Stream konusunu anlamak için C# 5.0 ile gelen async/await ve fi tarihinden beri dilde yer alan yield kavramını bilmek gerekiyor.

Diyelim ki durmadan bir odanın sıcaklığını ölçen IoT cihazınız var. Sürekli akan bu ölçüm verileri bir veri akışı (data stream) aslında.

IEnumerable‘ı hatırlarsınız, verilerimiz senkron bir okuma işi için hazırdır, ve biz her işimiz biten elemandan sonra yenisini isteriz. Ancak verilerimizin okuma işi asenkron ise, yani bir network çağrısı, disk operasyonu gibi gereksinim içeriyorsa şimdiye kadar yapabileceğimiz bir şey yoktu. Mecburen veriyi isteyen thread’i bekletiyorduk.

Artık verileri aşağıdaki gibi gezebiliyoruz:

// sonuçlar içinde sadece 20'den büyük olanları alıyoruz
async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result;
    }
}

Daha daha Patterns

Elixir, Haskell, Elm gibi Fonksiyonel dillerden bildiğimiz Pattern Matching desteği, C# 7.0 ile tanıtılmıştı. 8.0 ile artık daha fazla yerde kullanabiliyoruz.

Switch expressions

Sağolsun C# ekibi bıyık parantez kullanımımızı bayağı azalttı. Exception expressionlar ile if kullanımımız da azalmıştı. Benim kişisel en gıcık olduğum ifadelerden switch de sonunda bu gelişmelerden nasibini aldı. case ve break her yazdığımda şuna bir el atın düşüncesi aklımdan geçmiştir. Artık aşağıdaki gibi yazabileceğiz:

public static RGBColor FromRainbow(Rainbow colorBand) =>
    // değişken adının switch'den önce geldiğine dikkat edin
    colorBand switch
    {
        Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
        _              => throw new ArgumentException("Invalid value", nameof(colorBand)),
    };

Görünürde case ve break yok, default yerine _ tam 6 karakter daha az :)

Thats What I'm Talking About

Property patterns

Property Patterns ile bir objenin özelliğine eşleşme durumuna göre iş yapabiliyoruz. Kafa karıştırıcı cümleyi uzatmayalım, örnek aşağıda:

public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
    location switch
    {
        { Plate: 34 } => salePrice * 0.75M,
        { Plate: 17 } => salePrice * 0.06M,
        { Plate: 10 } => salePrice * 0.05M,
        _ => 0M
    };

Metodumuza gelmiş location parametresi üzerindeki Plate property’sinin değerine göre satış fiyatını vergiyle çarpıyoruz.

Tuple patterns

Tuple patterns ile Property Patterns’e çok benzer işi Tuple’lar ile yapabiliyoruz.

public static string RockPaperScissors(string first, string second)
    => (first, second) switch
    {
        ("taş", "kağıt") => "Kağıt taşı kapladı. Kağıt kazandı.",
        ("taş", "makas") => "Taş makası kırdı. Taş kazandı.",
        ("kağıt", "taş") => "Kağıt taşı kapladı. Kağıt kazandı.",
        ("kağıt", "makas") => "Makas kağıdı kesti. Makas kazandı.",
        ("makas", "taş") => "Taş makası kırdı. Taş kazandı.",
        ("makas", "kağıt") => "Makas kağıdı kesti. Makas kazandı.",
        (_, _) => "berabere"
    };

Positional patterns

Yine JavaScript dünyasını yakından takip edenlerin bileceği Destructuring işini objecler için C# ile de yapabiliyoruz, ancak adı Deconstructing. Bu özellik C# 7.0 ile geldiği için burada değinmeyeceğim.

Diyelim ki bir Point nesnesinin koordinat sisteminde hangi bölgeye geldiğini bilmek istiyoruz. Bunu artık aşağıdaki gibi yapabiliyoruz (Deconstructing spoiler içerir).

public class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) =>
        (x, y) = (X, Y);
}

public enum Quadrant
{
    Bilinmiyor,
    Merkez,
    SagUst,
    SagAlt,
    SolAlt,
    SolUst,
    Cizgide
}

static Quadrant GetQuadrant(Point point)
    // burada point x ve y dönen deconstruct işleminden geçiyor
    // ve bu tuple üzerinden karşılaştırma yapılıyor
    => point switch
    {
        (0, 0) => Quadrant.Merkez,
        var (x, y) when x > 0 && y > 0 => Quadrant.SagUst,
        var (x, y) when x < 0 && y > 0 => Quadrant.SolUst,
        var (x, y) when x < 0 && y < 0 => Quadrant.SolAlt,
        var (x, y) when x > 0 && y < 0 => Quadrant.SagAlt,
        var (_, _) => Quadrant.Cizgide,
        _ => Quadrant.Bilinmiyor
    };

Recursive Patterns

is kontrolünü yaptıktan sonra otomatik değişken atamasını aşağıdaki gibi yapabiliyorduk:

// eğer p bir Student ise student isimli bir değişkene atanacak
if (p is Student student)

Bunu artık bir adım öteye taşıyabiliyoruz:

if (p is Student { Graduated: false, Name: string name })

Yukarıda p eğer Student ise ve henüz mezun olmamışsa (Graduated: false), Name değerini yeni bir name değişkenine atıyoruz.

Ranges/Indicies - Aralık ve indeksler

Go ve Ruby gibi dillerde yer alan dizi yeteneklerinin benzerleri de sonunda C#’a geldi.

Burada dizinin belirli bir elemanına işaret edebilen Index kullanımını görebilirsiniz. Baştan bir sıra belirtmek için bir int kullanabiliyoruz, sondan sıra için ise değerin başına ^ koymamız yeterli.

Index i1 = 3;  // baştan 3.
Index i2 = ^4; // sondan 4.
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6"

Bu da dizide bir aralığı temsil eden Range kullanımı. Range de özel bir syntax ile geliyor, başlangıç..son.

var slice = a[i1..i2]; // { 3, 4, 5 }

Using Declarations

IDisposable uygulamış tiplerimizi using ile oluşturmak ve scope dışı kaldığında otomatik dispose edilmelerini sağlamak çok alışkın olduğumuz bir kullanım. Şunun gibi kodları defalarca yazdık:

public void Update() {
    using(var command = connection.CreateCommand()) {
        //...
    }
}

Yukarıdaki metodda using ile bir scope açtık ancak düşününce metod scope’u ile aynı. Yani using bittiğinde kavramıyla metod bittiğinde kavramı arasında fark yok. Using Declarations bizim bu durumdan faydalanmamızı sağlıyor.

public void Update() {
    using var command = connection.CreateCommand();
    //...
    // burada artık command otomatik olarak dispose edilecek
}

Hedef tip yazmadan new

Yeni bir dizi, liste oluştururken sizce de gereğinden fazla tuşa basmıyor muyuz? Sonunda bu konuda da güzel bir gelişme:

Point[] ps = { new (1, 4), new (3,-2), new (9, 5) }; // Hepsi Point

Burada dizimiz Point tipinde olduğundan tip yazmadığımız her bir new işleminde Point tipi varsayılıyor.


Tüm yeni özellikler için https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8 başta olmak üzere bir çok kaynak bulabilirsiniz.

Mutlu kodlamalar.