C# 7.0 Yenilikleri

24 Ağustos 2016 itibariyle Microsoft Visual Studio “15” Preview 4 içerisinde kullanabileceğimiz haliyle C# 7‘yi duyurdu. Hemen hemen artık duyurdukları özelliklerin çoğunun production versiyonunda olacağını varsayabiliriz. C# 5 ve C# 6 versiyonlarıyla karşılaştırdığımız zaman daha dolu bir versyion olmuş. Elbette diğer sürümlerde de önemli değişiklikler vardı örneğin C# 5’de ki async özellikleri development yaklaşımlarımızda önemli değişikliklere sebep oldu. Ama C# 7 yıllardır “Keşke şöyle olsa” dediğimiz bir sürü angaryadan bizi kurtarıyor.

Microsoft özellikle son senelerde çok güzel işler çıkartmaya başladı. Open source camiaya verdiği destek .Net Platformunu çok daha dinamik hale getirdi ve yeni özelliklerin, framework’lerin hayatımıza çok daha hızlı girmesini sağladı. Bu yazının kapsamında bakarsak buna verilebilecek en iyi örnek .Net’in Compiler Platform’u olan “Roslyn”. Roslyn 3 Nisan 2014’de Microsoft’un San Francisco’daki bir konferansında sahnede open-source hale getirilmişti. O zamana kadar compiler bizim için kapalı bir kutuydu ve kod analiz tool’ları ve ya kod odaklı uygulamaları geliştirmek daha çok deneme yanılma yöntemiyle yapılabiliyordu. Rosyln’le beraber aslında Compiler bir Platform haline geldi ve developer’lara bir api sağlanmış oldu. Bu sayede kod analiz, refactoring, meta-programming, code generation/transformation tool’larının yazılması çok daha kolaylaştı ve .Net Framework’un Windows harici işletim sistemlerinde de çalıştırılabilmesi çok daha kolay hale geldi. Bugün github üzerinde oldukça popüler olan ve çok fork’lanmış projelerden biri Roslyn. C# 7 ile ilgili geliştirmeler hala sürmekte gelişmeleri github’dan takip edebilirsiniz.

C# 7.0 Yenilikleri

Gelelim yazımızın konusu olan C# 7 versiyonuna. Microsoft bu versiyonla beraber kod okunurluğunu arttıracak sade ifadeler yazmamıza imkan sağlamış ve çeşitli performans geliştirmeleri yapmış. Ama bence en önemli gelişitrme C#’ın farklı programalama pradigmalarını da daha iyi destekler hale gelmiş olaması diyebiliriz. Zaten geçmiş versiyonlarda da Functional Programming paradigmasını destekleyen özellikler gelmişti ama bu versiyonla beraber bu özelliklerin sayısı arttırılmış. Bu stratejinin dili daha güçlü hale getirdiğini düşünüyorum. Her paradigmanın güçlü yanlarını kullanmamıza imkan sağlanmış.

Tuples

Bu özellikten başlamak istedim çünkü hem C# 7.0 içerisindeki en sevdiğim yenilik olmuş hem de kendisiyle ilgili bir anım var. Tuple 2010 yılında Microsoft .Net 4.0’ı yayınladığında hayatımıza girmişti. Tuple Matematik’de sonlu serilerilere verilen isim. N sayıda elemente sahip tuple, n-tuple olarak adlandırılır. .Net’de octuple veya 8-tuple’a kadar destekleniyor.

Şimdi Nereye Tuple’lıyoruz?

Tuple ilk çıktığında o zamanın tembel developer’ları olarak çok sevinmiştik ve ilk tepkimiz “Ooo süper database ve business modelleri tanımlamaya son!!! Her yere tuple’layalım.” şeklinde olmuştu. Bir projenin büyük bir kısmını Tuple kullanarak yapmıştık. Kısa süre sonra hangi metotdan ne dönüldüğünü ve dönen tuple’ın hangi elemenitinin neyi ifade ettiğini unuttuğumuzu fark ettik. Dolayısıyla projeyi refactor etmek durumunda kaldık. Sebebi aşağıdaki gibi metotların ortaya çıkmasıydı elbette.

public Tuple<int, string, string, string> GetUserById(int userId)
{
  // TODO : verilerin datareader veya datatable'da geldiğini ve sonrasında Tuple nesnesini oluşturduğumuzu düşünün. 
  // (Evet ORM kullanılmadığımız bir projeydi)
  var user = new Tuple<int, string, string, string>(1, "test", "test1", "test@test.com"); 
  return user;
}

// -------- Kullanımı
Tuple<int, string, string, string> user = GetUserById(1);
user.Item1
user.Item2;
user.Item3;
user.Item4;

Gördüğünüz üzere metodu tanımlayan için çok keyifli, kullanan için ızdırap. Hangi Item neyi ifade ediyor acaba? Bir de içine item olarak Tuple alan Tuple’ları hayal edin :). Elbette ki Tuple’ı böyle hunharca kullanmak bizim suçumuzdu.

C# 7.0 ve Tuple

Şimdi hepimizin bildiği üzere C#’da bir metotdan birden fazla değer dönmek için kullanabileceğimiz bir kaç yöntem var.

  • out kullanmak. Aşırı kullanımı metotların okunurluğunu düşürüyor ve async alt yapısıyla kullanılmasını engelliyor
  • Anonymous type’lar aracılığıyla geri dönüş tipi olarak dynamic kullanımı. Dönen tipin içerisinde hangi property’nin ne tipinde olduğunu bilememek gibi sıkıntıları var.
  • Her metodun geri dönüşü için ayrı tip tanımlamak. Her ne kadar ben burada bir yanlış görmesem de bazen projenin genelinde değil çok küçük kısmında geçici olarak kullanmaya yönelik nesneler tanımlamaya ihtiyacımız olabiliyor.
  • Ve yukarıda da anlattığımız System.Tuple. Item’lar da neyin ne olduğu anlaşılmıyor ve Tuple nesnesi için önceden yer allocate etmemiz gerekiyor

C# 7.0’la beraber hayatımıza Tuple Type ve Tuple Literal giriyor. Yukarıda verdiğimiz Tuple örneği üzerinden gidersek eğer.

public (int, string, string, string) GetUserById(int userId) // Tuple Type
{
  //  verilerin bir datasource'dan geldiğini düşünelim
  int id = 1;
  string firstName = "test";
  string lastName = "test";
  string email = "test@test.com";
  
  return (id, firstName, lastName, email); // Tuple Literal
}

Çok daha sade ve güzel gözüküyor değil mi? Şimdi bu şekilde kullanırsak yine yukarıdaki Tuple örneğindeki gibi isimler Item1, Item2 … şeklinde olucaktır. İstersek aşağıdaki gibi isimleri set ederekte kullanabiliriz.

public (int Id, string FirstName, string LastName, string Email) GetUserById(int userId) // Tuple Type
{
  //  verilerin bir datasource'dan geldiğini düşünelim
  int id = 1;
  string firstName = "test";
  string lastName = "test";
  string email = "test@test.com";
  
  return (id, firstName, lastName, email); // Tuple Literal
}

İsimleri Tuple Type’da verebileceğimiz gibi Literalde de verebiliyoruz.

return (Id : id, FirstName : firstName, LastName : lastName, Email : email); // Tuple Literal

Tuple Type’lar value type değişkenlerdir ve içerisindeki field’lar public ve Mutable’dır. Ayrıca value olarak iki tuple birbiriyle eşitlik olarak karşılaştırılabilir. Bu durum tuple’ların Dictionary’lerde key olarak kullanılmasına da imkan sağlıyor. C# 7.0 ile gelen değişiklikleri kendi development rutinlerimize sokmak biraz zaman alabilir ama sonucunda daha güzel ve kısa kodlar yazarak daha çok iş yapabileceğimizi düşünüyorum. Biraz bakış açımızı değiştirmemiz gerekiyor.

VS 15 Preview 4’ü indirip yukarıdaki kodu çalıştırmak isterseniz öncelelikle Nuget’den include prerelease’i işaretleyip System.ValueTuple paketini indirmeniz gerekiyor.
İsimleri Tuple Type’da verebileceğimiz gibi Literalde de verebiliyoruz.

Tuple Deconstruction

Tuple Deconstruction özelliği oluşturduğumuz Tuple tiplerini elementlerine parçalamamıza imkan sağlıyor. Yani yukarıdaki örneği ele alırsak şu şekilde kullanabiliyoruz :

(int id, string firstName, string lastName, string email) = GetUserById(1); // var kullanılabilir.Console.WriteLine($"{id} - {firstName} - {lastName} - {email}");

id, firstName, lastName ve email local değişken olarak kullanabilir. Ayrıca değişkenler tanımlanırken var’da kullabiliyoruz. var’ın literal dışında kullanımına örnek olarak :

var (id, firstName, lastName, email) = GetUserById(1);

Veya varolan değişkenlere de atama yapabilirsiniz :

(id, firstName, lastName, email) = GetUserById(1);

Genel olarak yeni Tuple yapısının ve Tuple Deconstruction’ın çok yararlı bir özelliklik olduğunu düşünüyorum. Özellikle DTO tanımlamanın çok gerekli olmadığı durumlarda ve bir metotdan birden fazla değer dönmek istediğimiz durumlarda kullanabiliriz. Tabi bu durum model tanımlamayı bırakıp artık Tuple kullanacağımız anlamına gelmiyor :).

Out Variables

Bence out parametrelerinin kullanımı C#’ın kanayan yarasıydı. out parametresi kullanan metotları kullanmadan önce değişkenimizi tanımlamamız gerekiyordu. Özellikle Try metotlarını (int.TryParse, TryDequeue, Header.TryGetValues vs..) kullanmamız gerektiğinde ortaya çok çirkin kod blokları çıkıyordu. Yeni versiyonda out parametresi olarak kullanacağımız değişkenleri kullanıldığı yerde tanımlamamıza imkan sağlamışlar ayrıca artık tip belirmek yerine var’da kullanabiliyoruz.

Örnek vermek gerekirse eski kullanımda :

public void UpdateUserBalance(int userId, string balance) // harici bir sistemden gelen string data
{
    decimal userBalance;

    if(decimal.TryParse(balance, out userBalance))
    {
        Console.WriteLine($"{userId} id'li kullanıcının bakiyesi {userBalance}");
    }
    else
    {
        Console.WriteLine("Geçerisiz bir değer girildi");
    }       
}

Yeni gelen özellikle beraber yukarıdaki metodu aşağıdaki gibi tanımlayabiliyoruz :

public void UpdateUserBalance(int userId, string balance) // harici bir sistemden gelen string data
{
    if (decimal.TryParse(balance, out decimal userBalance)) // decimal yerine var'da kullanabiliriz
    {
        Console.WriteLine($"{userId} id'li kullanıcının bakiyesi {userBalance}");
    }
    else
    {
        Console.WriteLine("Geçerisiz bir değer girildi");
    }
}

Gördüğünüz üzer decimal tipindeki değişkeni TryParse metoduna parametre geçerken tanımlayabiliyoruz.

VS 15 Preview 4’ü indirip yukarıdaki kodu çalıştırırsanız çalışıcaktır fakat userBalance değişkenini if scope’u dışında kullanmaya çalışırsanız çalışmayacaktır. Bu VS 15 Preview 4 ile alakalı bir problem, out variables’ların scope’larını şuan beklediğimiz gibi handle edemiyor ama release olduğu zaman değişkenimiz UpdateUserBalance scope’u içerisinde istediğimiz yerde kullanılabilecek.

Pattern Matching

Benim en sevdiğim özelliklerden biri bu oldu. is ve switch-case ifadelerinde büyük yenilikler yapan bir özellik. İlerleyen versiyonlarda çok daha kapsamlı bir şekilde geliştirileceğe benziyor. Aslında yapılan bir değerin belli bir şablonla test edilmesi ve eğer sonuç olumluysa değerin extract edilmesi. Bu pattern’leri şimdilik üç kategoride toplamışlar.

  • Constant patterns : Verilen değerin constant bir ifadeyle test edilmesi.
  • Type patterns : T x şeklinde. T test edilmek istenilen değerin tipi, x değişkenin adı. Değerin T tipiyle match edip etmediğinin test edilmesi ve sonuç olumluysa değerin extract edilip x’e atanması.
  • var patterns : var x şeklinde. bu pattern’in Type Pattern’den farkı aslında ortada test edilecek bir durumun olmaması, değerin extract edilip x’e atanması durumu

Pattern Matching özelliğinin arkasının geleceğini ve daha çok fazla geliştirileceğini düşünüyorum. Zaten Microsoft’un yaptığı açıklamada bu yönde.

Is ifadelerinde şu şekilde kullanılıyor :

public static void ShowUserBalance(object balance)
{
    if (balance is null) // constant pattern "null"
    {
        return;
    }
    
    if (balance is decimal userBalance) // type pattern "decimal userBalance"
    {
        Console.WriteLine($"Kullanıcının bakiyesi {userBalance}");
    }
}

Bildiğiniz üzere “is” ifadelerini eskiden sadece type’lerla beraber kullanabiliyorduk :

if (value is object)
{
Console.WriteLine("is object");
}
if (value is string)
{
Console.WriteLine("is string");
}
if (value is UserModel)
{
Console.WriteLine("is UserModel");
}

Eğer ilk örneği incelersek “balance is null” ifadesinde bir constant pattern kullanıldığını görmekteyiz. Yani aslında burada balance’ı constant bir value ile test ediyoruz. Bu aşamada null yerine bir decimal veya herhangi bir değerde kullanabilirdik. “balance is decimal userBalance” kısmında ise bir type pattern görmekteyiz. Aslında burada eski kullanımdan farklı olarak tanımladığımız decimal userBalance’a pattern’in match etmesi halinde atama da yapıyoruz.

Out Variables’da olduğu gibi VS 15 Preview 4 eğer userBalance’ı if scope’u dışında kullansaydık hata verecekti.

Out Variables ve Pattern Matching özelliklerini bir arada kullanılmasıyla ilgili bir örnek vermek gerekirse

public static void ShowUserBalance(object balance)
{
    if (balance is decimal userBalance || (balance is string s && decimal.TryParse(s, out userBalance)))
    {
        Console.WriteLine($"Kullanıcının bakiyesi {userBalance}");
    }
}

Benim fikrim bu tür özelliklerin aşırı kullanılması ve her şeyin tek satırda yazılması kod okunurluğunu biraz düşürür yönünde ama yukarıdaki kodu eski yöntemle yapmaya kalkışsaydık ortaya çirkin kod blokları çıkacaktı. O yüzden yeni gelen özelliklerle beraber kod standartlarımızı ve bakış açımızı biraz değişmesinde yarar görüyorum. Bir developer için olabilecek en kötü şey kafasında dogmalar oluşturması. Değişimlere açık olmak gerekiyor.

Switch-Case’de ise çok güzel yenilikler var.

  • Artık bütün tipler switch-case’de kullanılabiliyor
  • case’lerde yukarıda anlattığımız pattern’ler kullanılabiliyor
  • case’lere çeşitli condition’lar verilebiliyor

Şöyle güzel bir örnek var :

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Örnekte Circle ve Rectangle tiplerinin bir base class’dan türediğini farz edebiliriz. Circle c, Rectangle s ve Rectangle r ifadelerinde ilgili case’e gelindiğinde değerin c,s ve ya r değişkenlerine atandığını görüyoruz burada type pattern kullanıldığını gözlemleyebiliriz. Ayrıca ikinci case ifadesinde when kullanırak hangi condition sağlandığı zaman o case’in çalışması gerektiği de belirtilmiş. Son case’de constant pattern kullanılmış ve null olması durumunda exception fırlatılması sağlanmış.

Dikkat edilmesi gereken bir kaç nokta var :

  • Exception catch’lerindeki gibi artık case’lerin sırasının da önemi var. Ve belli tutarsız durumlarda compiler bizi uyarabiliyor. Örneğin birden fazla interface’den türemiş bir nesneyi case’e sokarsak ve interface’leri yukarıdaki gibi case’lerde kullanırsak compiler bu case daha önceki case’de zaten handle edilmiş gibisinden bir hata veriyor.
  • default her zaman en son kontrol ediliyor. Yukarıdaki örnekte hemen altında case null ifadesini görüyoruz fakat bu durumu etkilemiyor. Yine de default ifadelerini okunurluk açısından en sona koymakta fayda var.

Local functions

C# 7.0’la beraber artık metot içinde metotlar tanımlayabiliyoruz. Bunu eskiden anonymous function’lar, delegate’ler veya expression kullanarakta yapabiliyorduk. Şu haliyle daha düzenli olmuş kanaatindeyim. Bazen sadece bir metot için anlamlı olabilecek helper metotlar yazmamız gerekebiliyordu ve yazdığımız private metotlar class’ımız çok kalabalıklaştırabiliyordu. Bu şekilde class’larımızı daha sade tanımlamamıza imkan sağlanmış. Konuyla ilgili güzel bir Fibonacci örneği var :

public int Fibonacci(int x)
{
  if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
  {
    return Fib(x).current;
  }
  
  (int current, int previous) Fib(int i)
  {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
  }
}

Literal Düzenlemleri

Ufak bir değişiklik olarak sayı tipindeki değişkenleri tanımlarken okunurluğu arttırmak için digitlerin ayrılmasına imkan sağlanmış. Örneğin 1.000.000 rakımını aşağıdaki gibi ifade edebiliyoruz :

int d = 1_000_000;

Ayrıca artık binary literal’ler tanımlamamıza da imkan sağlanmış :

var b = 0b1010_1011_1100_1101_1110_1111;

Ref returns

Value (değer) tipli değişkenlerin, referansını kullanacağımız metodlara geçmek için ref ve out parametrelerini önceden de kullanıyorduk. Şimdi artık referansını dönüş tipi olarak verip bir değişkende tutabiliyoruz. Konuyla ilgili verilmiş güzel ve açıklayıcı bir örnek :

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // burada dönülen aslında i. elemanın değeri değil onun array içerisindeki referansı
        }
    }
    
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // array'in 4.elemanı 7'yi işaret ediyor
place = 9; // 7'yi 9'la değiştiriyoruz
WriteLine(array[4]); // array'in 4.eleman artık 9'u işaret ediyor

Yukaridaki örneği incelediğimiz zaman metod geri dönüş tipi belirtilirken başın ref eklendiğni görüyoruz. Yine return edilirken ref’le beraber return kullanılıyor. return ref numbers[i] satırında dönülen array’in i.elemanının değeri değil array içerisindeki referansı. Aynı zamanda atama yapılırken yine atama yapılacak değişkenin başına ref koyuyoruz ref int place = ref Find(7, array);. place değişkeni referans tutmakta yani place’e yapacağımız her atama ilgili array içerisinde referans verilen elemanda değişikliğe sebep olacaktır.

Burada dikkat edilmesi gereken nokta şu : Bu metot normal bir şekilde kullanılsaydı dönüş değeri olarak array’in i.elemanının değeri ayrı bir değişkene kopyalanıp o şekilde dönülecekti. C# value type’lı değişkenlere bu şekilde davranıyor. Aynı şekilde herhangi bir metoda geçtiğimiz value type değişken aslında metot scope’unda geçerli olmak üzere başka bir değişkene kopyalanır. ref kullanımlarında değer kopyalanmıyor ve memory’deki adresi geçiliyor ve dolayısla yaptığımız değişiklikler orjinal değişken üzerinde yapılıyor.

Aslında C++ pointer’larının unsafe olmadan kullanılabilecek hali gibi olmuş. Böyle bir kullanımın avantajı elbette ki performans. Çünkü herhangi bir kopyalama işlemi gerçekleştirmemiş oluyoruz. Günlük kullanımlarımız açısından bize çok bir getirisi yok gibi gözüküyor ama yüksek memory kullanımı gerektiren algoritmalar yazılırken performansa önemli bir katkısı olabilir.

Expression Bodied Üyelerde Düzenlemeler

C# 6.0’la beraber hayatımıza Expression Bodied Metot ve Property’ler dahil olmuştu. Bu şekilde bazı tek satırlık ifadeleri daha okunaklı ve kısa bir şekilde yazabiliyorduk.

public class Square
{
    public Square(int length)
    {
        Length = length;
    }
    
    ~Square() // Örnek amaçlı yazıldı
    {
        Length = 0;
    }

    public int Length { get; set; }
    public int Area => Length * Length;
    public int Perimeter => 4 * Length;
    public override string ToString() => $"Square: Area={Area}, Perimeter={Perimeter}";
}

Area ve Perimeter property’leri ve ToString metodu buna örnek verilebilir. Artık aynı şeyi Constructor’lar ve Destructor’lar için de yapabiliyoruz.

public class Square
{
    public Square(int length) => Length = length;
    ~Square() => Length = 0; // Örnek amaçlı yazıldı
    public int Length { get; set; }
    public int Area => Length * Length;
    public int Perimeter => 4 * Length;
    public override string ToString() => $"Square: Area={Area}, Perimeter={Perimeter}";
}

VS 15 Preview 4 versiyonunda bu özelliği deneyemiyoruz.

Throw expressions

Bizi bir sürü gereksiz if’den kurtaran bir yenilik daha ?

Özellikle çok sık kendi exception tiplerini yazıp fırlatan biri olarak ben bu özelliğe çok sevindim. Artık null-coalescing (? ?), ternary (? ? ve expression body’ler de exception’larımızı tak diye fırlatabileceğiz. Kanımca bu özellik yıllar önce gelmesi gereken bir özellikti. Konuyla ilgili güzel bir örnek :

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ? ? throw new ArgumentNullException(name);
    
    public string GetFirstName()
    {
        var parts = Name.Split(" ");
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }
    
    public string GetLastName() => throw new NotImplementedException();
}

Mesela normalde ternary operatorü kullanılırken ? ‘den sonra verilen tipin aynısının : ‘dan sonrada verilmesi gerekirdi. Bu bizim ternary operatörünü bazı durumlarda if gibi kullanmamızı kısıtlıyordu. Aynı şey null-coalescing operatorü için de geçirliydi. Küçük ama beni çok mutlu eden bir özellik olmuş. Teşekkürler Microsoft ?

VS 15 Preview 4 versiyonunda bu özelliği deneyemiyoruz.

Şimdilik C# 7.0 için duyrulan özellikler bu şekilde. Şuan için bile VS 15 Preview 4 bütün bu özelliklerin hepsini denememize izin vermiyor. Ayrıca bu özelliklerin bir kısmının ilerleyen günlerde değişme ihtimalide var. Yapılan değişiklikleri C# son 3 versiyonuyla karşılaştırırsak bence C# 7.0 öğrenmek gibi bir kavramın ortaya çıktığını görüyoruz. Tam anlamıyla bambaşka bir dil olmuş diyemem ama bazı özelliklere kendimizi adapte edilmemiz için düşünce yapımızı değiştirmemiz gerektiğini düşünüyorum. Özellikle Functional Programming prensiplerine bir göz gezdirmekte hatta mümkünse bir Functional dil öğrenmekte yarar var. C# istikrarlı bir şekilde 3.0 versiyonundan itibaren giderek Object Oriented bir dilden Multi-Paradigm bir dile evriliyor ve dilin avantajlarını tam anlamıyla kullanabilmemiz için diğer pradigmalara da hakim olmak gerektiğini düşünüyorum.

Paylaştıkça Artan Tat...