İlkay İlknur

just a developer...

Object,Collection ve Dictionary Initializers

Merhaba Arkadaşlar,

Bu yazımızda sizlerle C# 3.0 sürümünden itibaren bizlerle olan ve C# tarafında object’lerin ve collection’ların yaratılması sırasında developerlara oldukça kolaylık sağlayan Object & Collection & Dictionary Initiliazers konusunu inceliyor olacağız.

İlk olarak Object Initializers tarafına bakarsak Object Initializer yapısı basit olarak biz developerların özellikle object yaratımı sırasında objectlerin propertylerine ilk değerleri atamamız konusunda syntax bakımından oldukça kolaylık sağlamakta. Bunun yanında Object Initiliazers özellikle LINQ sorgularında ve anonymous type yaratımında da hayati önem arzetmekte.

Şimdi öncelikle basit bir sınıf tanımlayalım ve daha sonra Object Initializer mekanizmasıyla yaratmış olduğumuz object’in propertylerini ilk değerlerine set edelim.

class MyClass {         public int Property1 { get; set; } public string Property2 { get; set; } public MySecondClass Property3 { get; set; }}

class MySecondClass{ public int MyProperty { get; set; }}

 

Şimdi object initializer kullanarak MyClass tipinden bir nesne örneği yaratalım.

MyClass myClass = new MyClass(){ Property1 = 1, Property2 = "Property2", Property3 = new MySecondClass() { MyProperty = 3 }};

Gördüğünüz gibi oldukça basit bir şekilde nesnemizi yarattık. Peki arka planda neler oluyor ? Öyleyse hemen arka plana geçelim ve bakalım neler oluyor.

Farklılıklar aslında yaratılan IL tarafında gerçekleşmekte.

.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 3 .locals init ( [0] class ConsoleApplication2.MyClass myClass, [1] class ConsoleApplication2.MyClass <>g__initLocal0, [2] class ConsoleApplication2.MySecondClass <>g__initLocal1) L_0000: nop L_0001: newobj instance void ConsoleApplication2.MyClass::.ctor()     L_0006: stloc.1 L_0007: ldloc.1 L_0008: ldc.i4.1 L_0009: callvirt instance void ConsoleApplication2.MyClass::set_Property1(int32) L_000e: nop L_000f: ldloc.1 L_0010: ldstr "Property2" L_0015: callvirt instance void ConsoleApplication2.MyClass::set_Property2(string)     L_001a: nop L_001b: ldloc.1 L_001c: newobj instance void ConsoleApplication2.MySecondClass::.ctor()     L_0021: stloc.2 L_0022: ldloc.2 L_0023: ldc.i4.3 L_0024: callvirt instance void ConsoleApplication2.MySecondClass::set_MyProperty(int32)     L_0029: nop L_002a: ldloc.2 L_002b: callvirt instance void ConsoleApplication2.MyClass::set_Property3(class ConsoleApplication2.MySecondClass)     L_0030: nop L_0031: ldloc.1 L_0032: stloc.0 L_0033: ret }

Akışa baktığımızda aslında bizim yaptığımız işlemleri compiler arka planda açarak, teker teker tüm işlemleri gerçekleştirmekte ve yaklaşık olarak aşağıdaki kod örneğine çevirmekte.

MyClass myClass = new MyClass();myClass.Property1 = 1;myClass.Property2 = "Property2";MySecondClass second = new MySecondClass();second.MyProperty = 3;myClass.Property3 = second;  

Şimdi yukarıdaki kod ile bir önceki object initializer kullanarak yazdığımız kodu karşılaştırdığımızda özellikle okunulabilirlik bakımından oldukça büyük farklar bulunmakta. İlk kullanımda kodu okuyan developer hemen hızlı bir şekilde set edilen değerleri okuyup geçerken diğer örnekte ise tek tek referansları kontrol etmek zorunda kalmakta ve daha çok zaman kaybetmekte. Bunun yanında kodu yazan developer da ikinci kullanımda birinci kullanıma göre daha çok zaman kaybetmekte.

İşte bu noktadaki problemi çözmek için compiler devreye girmekte ve developerlara oldukça kolay bir tanımlama syntax’ı sağlarken arka planda da bu syntax’ın kullanıldığı kısımları yeniden yazarak aslında arka planda yine klasik kullanımı muhafaza etmekte ;)

Collection Initializers

C# 3.0 ile beraber gelen bir diğer syntax kolaylığı ise Collection Initializers’dı. Collection Initializers ile yarattığımız koleksiyonlar içerisine elemanları hızlı bir şekilde yerleştirebilmekteyiz.

Örneğin, List<int> list = new List<int>(){ 1,2,3,4,5,6,7};  

yukarıdaki gibi bir kullanımla bir List<T> tipinden biri koleksiyon örneği yarattıktan sonra içerisine 1’den 7’ye kadar elemanları ekleyebilmekteyiz. Bu kullanım aslında C# 3.0 öncesinde array’ler için geçerliydi. Ancak C# 3.0 ile beraber bu kullanım kolaylığı collection tipleri için de getirildi.

Peki collection initializers kullandığımızda arka planda neler oluyor ? :) .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 2 .locals init ( [0] class [mscorlib]System.Collections.Generic.List`1<int32> list, [1] class [mscorlib]System.Collections.Generic.List`1<int32> <>g__initLocal0) L_0000: nop L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()     L_0006: stloc.1 L_0007: ldloc.1 L_0008: ldc.i4.1 L_0009: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) L_000e: nop L_000f: ldloc.1 L_0010: ldc.i4.2 L_0011: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)     L_0016: nop L_0017: ldloc.1 L_0018: ldc.i4.3 L_0019: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) L_001e: nop L_001f: ldloc.1 L_0020: ldc.i4.4 L_0021: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) L_0026: nop L_0027: ldloc.1 L_0028: ldc.i4.5     L_0029: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) L_002e: nop L_002f: ldloc.1 L_0030: ldc.i4.6 L_0031: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) L_0036: nop L_0037: ldloc.1 L_0038: ldc.i4.7 L_0039: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)     L_003e: nop L_003f: ldloc.1 L_0040: stloc.0 L_0041: ret }

Burada da baktığımızda aslında yazdığımız kodun aksine compiler arka planda öncelikle bir List nesne örneği yaratmakta ve sonrasında da tüm elemanları List tipinin Add metodunu kullanarak eklemekte. Aslında bizim normalde de kullanmamız gereken bu metodu compiler arka planda kendisi kullanmış ve biz developerlara daha kısa bir syntax ile uygulama geliştirme imkanı sağlamış.

Collection Initializer mekanizmasını kullanmamız için sadece tek bir kısıt bulunmakta. O da tipimizin IEnumerable interface’ini implemente etmesi ve tip içerisinde Add metodunun bulunması. Burada aklımıza gelen bir soru bulunmakta. Bu da arka planda generate edilen IL kodlarında IEnumerable interface’inin kullanılmamasına rağmen collection initializers mekanizmasında neden zorunlu olarak IEnumerable interface’inin tip tarafından implemente edilmek zorunda olduğu. Bunun nedeni de aslında C# programlama dili tasarım ekibi tarafından alınmış bir karara dayanmakta. C# 3.0 ile ilgili araştırmalar yaparken collection Initializers yapısının destekleneceği noktalar ekip tarafından incelenmiş ve bu özellik collection tipleri ile sınırlı bırakılmış ve bu nedenle de tipin IEnumerable interface’ini implemente etmesi gerektiği zorunlu hale getirilmiştir.

Dictionary Initializers

Bu yazımızda son olarak bahsedeceğimiz konu da Dictionary Initializers. Collection Initializers ile collection tipleri içerise kolay bir şekilde eleman ekleyebilirken, aslında bu ihtiyaca benzer bir durum da dictionary tipleri için doğmakta. Bazı durumlarda yarattığımız Dictionary’ler içerisine elemanları teker teker eklememiz gerekebilmekte. Bu noktada da yine Collection Initializers’da olduğu gibi C# tarafından sağlanan kolay bir syntax ile dictionary içerisine ilk elemanları atabilmekteyiz.

Dictionary Initializers’ın kullanımı ise şu şekilde olmakta.

Dictionary<int, string> a = new Dictionary<int, string>(){ {1,"Bir"}, {2,"İki"}, {3,"Üç"}};

Peki compiler arka planda ne yapmakta ?

Dictionary<int, string> <>g__initLocal0 = new Dictionary<int, string>();

<>g__initLocal0.Add(1, "Bir");

<>g__initLocal0.Add(2, "İki");

<>g__initLocal0.Add(3, "Uc");

Dictionary<int, string> a = <>g__initLocal0;

 

Compiler kimi zaman bu yazımızda da gördüğümüz gibi bir takım kolay syntax yapılarını developerlara sunarken arka planda aslında bu syntax yapısını klasik kullanımlara geri çevirerek developerların yazılım geliştirme sürelerini kısaltmayı amaçlamakta. Kimi zaman da keywordler veya modifierlar sunarak arka planda kurduğu generic yapılarla çok daha farklı işleri biz developerlara hissettirmeden gerçekleştirmekte. ;)

Hoşçakalın



C# 5.0 Async & Await Arka Planda Neler Oluyor ?

Merhaba Arkadaşlar,

Sizlerle şu ana kadar C# 5.0 ile beraber gelecek olan Asenkron Programlama yeniliklerini hangi noktalarda kullanabileceğimizi inceledik. Ancak bildiğiniz gibi her programlama dili yeniliği aslında arka planda yeni bir sihir yaratır. Daha önce kodumuzu neredeyse tamamen değiştirerek asenkron hale getirirken artık sadece 1-2 ufak değişiklikle kodumuzu asenkron bir şekilde çalışacak hale getirebiliyoruz. Peki gerçekten arka planda neler oluyor ? Compiler neler yapıyor da biz sadece 1-2 kod değişikliği ile kodumuzu asenkron hale getirebiliyoruz.

İlk olarak aşağıdaki gibi bir asenkron metodumuz olduğunu düşünelim.

static async Task<string> DownloadStringAsync(string Url)
{
 WebClient client = new WebClient();
 return await client.DownloadStringTaskAsync(Url);
}

Şimdi yazmış olduğumuz kodu derleyelim ve sonrasında Reflector yardımıyla açalım ve bakalım derleme sonucunda asenkron metodumuz compiler tarafından nasıl yeniden yazılmış ?

Dikkat : Yazının bu kısmından sonrası aşırı miktarda compiler tarafından üretilen kod içermektedir. Eğer compiler tarafından üretilen kodlara karşı alerjiniz varsa bu satırlar sizin için son çıkıştır. Ancak yazıyı buraya kadar okuduysanız sizinde aslında arka planda benim gibi neler olduğunu merak ediyor olduğunuzu düşünüyorum :)

private static Task<string> DownloadStringAsync(string Url) { <DownloadStringAsync>d__0 d__; d__ = new <DownloadStringAsync>d__0(0) { Url = Url, <>t__MoveNextDelegate = new Action(d__.MoveNext), $builder = AsyncTaskMethodBuilder<string>.Create() }; d__.MoveNext(); return d__.$builder.Task; }

Gördüğünüz gibi compiler, bizim yazmış olduğumuz DownloadStringAsync isimli metodu arka planda bir takım tipleri kullanarak yeniden yazdı.

Önceki yazılarımızdan da hatırlayacağımız üzere asenkron metotların dönüş tipi Task, Task<T> veya void olmak zorunda. Ancak bu dönüş tipini her zaman bizim yaratıp döndürmemiz gerekmemekte. Nitekim await kullanarak WebClient tipi içerisindeki DownloadStringTaskAsync metodunu çağırdığımızda bu metodun dönüş tipinin string olduğunu kolayca görebilmekteyiz. Ancak gördüğünüz gibi biz aslında asenkron metodumuzun dönüş tipini Task<string> olarak belirttik ve baktığımız zaman return ifadesinde de string döndürdük. Baktığımız zaman bu kullanım aslında tamamen legal. Ancak tabi ki bu işlem arka planda bir takım dönüşümler gerektirmekte. Bu dönüşümlerden ilki de Task<string> tipinin arka planda yaratılması ve metottan geri döndürülerek yazdığımız kodun arka planda da legalleştirilmesi. İşte compiler tarafından yazılan kodda görmüş olduğunuz AsyncTaskMethodBuilder tipi tam da bu işi gerçekleştirmekte.

public struct AsyncTaskMethodBuilder<TResult> { public Task<TResult> Task { get; } public void SetResult(TResult result); public void SetException(Exception exception); }

AsyncTaskMethodBuilder tipi gördüğünüz gibi içerisinde metot tarafından döndürülecek olan Task tipini ve metot işletilmesinden sonra da bu Task tipi içerisine sonucu yazmakta kullanılan SetResult isimli metodu içermekte. Bir de tabi eğer metot çalışması sırasında bir exception oluşursa bu exception’ı yine Task tipi içerisine yerleştirmek için bir de SetException metodunu bulundurmakta.

Compiler tarafından yeniden yazılan DownloadStringAsync isimli metodumuz içerisinde bir de <DownloadStringAsync>d__0 isimli bir tipin kullanıldığını görüyoruz. Bu tip te compiler tarafından yazılmış olan ve asenkron akışı tamamen içerisinde barındıran (enkapsüle eden) bir tip. Compiler tarafından yaratılmış olan bu tipin kodu ise şu şekilde.

[CompilerGenerated] private sealed class <DownloadStringAsync>d__0 { // Fields private bool $__disposing; public AsyncTaskMethodBuilder<string> $builder; private int <>1__state; public string <>3__Url; public Action <>t__MoveNextDelegate; private TaskAwaiter<string> <a1>t__$await3; public WebClient <client>5__1; public string Url; // Methods [DebuggerHidden] public <DownloadStringAsync>d__0(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] public void Dispose() { this.$__disposing = true; this.MoveNext(); this.<>1__state = -1; } public void MoveNext() { string <>t__result; try { string <1>t__$await2; bool $__doFinallyBodies = true; if (this.<>1__state != 1) { if (this.<>1__state != -1) { this.<client>5__1 = new WebClient(); this.<a1>t__$await3 = this.<client>5__1.DownloadStringTaskAsync(this.Url).GetAwaiter<string>(); if (this.<a1>t__$await3.IsCompleted) { goto Label_0089; } this.<>1__state = 1; $__doFinallyBodies = false; this.<a1>t__$await3.OnCompleted(this.<>t__MoveNextDelegate); } return; } this.<>1__state = 0; Label_0089: <1>t__$await2 = this.<a1>t__$await3.GetResult(); this.<a1>t__$await3 = new TaskAwaiter<string>(); <>t__result = <1>t__$await2; } catch (Exception <>t__ex) { this.<>1__state = -1; this.$builder.SetException(<>t__ex); return; } this.<>1__state = -1; this.$builder.SetResult(<>t__result); } }

Compiler tarafından yaratılan tipin constructor metoduna state isimli bir int değerin parametre olarak alındığını görüyoruz. Bu state değişkeni aslında bizim asenkron metot akışımızda önemli bir yere sahip. Bu yüzden <>1__state alanını aklımızın bir köşesinde tutalım ;)

Aslında sizin de göreceğiniz üzere <DownloadStringAsync>d__0 tipi içerisinde asenkron akışı yöneten metot MoveNext metodu. Bu metot içerisinde peki neler oluyor ?

Bu metodu incelemeden önce DownloadStringAsync isimli metodumuzun içerisine geri dönelim ve yapılan değer atamalarına bir bakalım.

private static Task<string> DownloadStringAsync(string Url) { <DownloadStringAsync>d__0 d__; d__ = new <DownloadStringAsync>d__0(0) { Url = Url, <>t__MoveNextDelegate = new Action(d__.MoveNext), $builder = AsyncTaskMethodBuilder<string>.Create() }; d__.MoveNext(); return d__.$builder.Task; }
  • İlk olarak constructor metoda 0 değeri parametre olarak geçilmiş. Bu değer de hatırlayacağımız üzere <DownloadStringAsync>d__0 tipi içerisindeki state değişkenine atanmakta.
  • <>t__MoveNextDelegate isimli delegate tipine de yine <DownloadStringAsync>d__0 tipi içerisindeki MoveNext metodu atanmakta.
  • Daha sonra ise yukarıda incelemiş olduğumuz AsyncTaskMethodBuilder tipinin Create isimli Factory metodu çağrılarak ilgili tipten bir nesne örneği $builder değişkenine atanmakta.
  • Son olarak ise nesne örneği yarattıktan sonra bu örnek üzerinden MoveNext metodu çağrılmakta.

Evet şimdi MoveNext metoduna geri dönüyoruz.

MoveNext metodu içerisinde ilk çağrım sırasında state değişkenimizin değeri 0. Bu nedenle metot içerisindeki 2 if ifadesinden de geçerek aşağıdaki kod bloğu işletilmekte.

if (this.<>1__state != 1) { if (this.<>1__state != -1) { this.<client>5__1 = new WebClient(); this.<a1>t__$await3 = this.<client>5__1.DownloadStringTaskAsync(this.Url).GetAwaiter<string>(); if (this.<a1>t__$await3.IsCompleted) { goto Label_0089; } this.<>1__state = 1; $__doFinallyBodies = false; this.<a1>t__$await3.OnCompleted(this.<>t__MoveNextDelegate); } return;

}

Çalışan koda baktığımızda bizim orjinal kodumuz içerisinde yazmış olduğumuz kodların bu kısımda olduğunu görmekteyiz. İlk olarak WebClient tipinden bir nesne örneği yaratılmakta ve akabinde de bu tip üzerinden DownloadStringTaskAsync isimli metot çağrılmakta.

Önceki yazılarımızda hatırlarsanız async & await kullanmamız için “awaitable pattern”’ı uygulamamız gerektiğinden sıkça bahsetmiştik. İşte awaitable pattern tam da kodumuzun bu noktasında devreye girmekte. Bir işlemin awaitable olması için aslında içerisinde GetAwaiter metodu ile aşağıdaki tipi döndürmesi gerekmekte.

public struct TaskAwaiter<TResult> { public bool IsCompleted { get; } public void OnCompleted(Action continuation); public TResult GetResult(); }

Bu tip aslında compiler tarafında oldukça önem arz etmekte. Bunun nedeni ise compiler arka planda yani yukarıdaki kodlarda bu tip üzerinden asenkron işlemin tamamlanıp tamamlanmadığını kontrol etmekte. Eğer işlem tamamlanmadıysa OnCompleted metodunu kullanarak bir callback ataması yapmakta. Tabi birde işlemin sonucunu elde edebilmek için GetResult metodunu çağırmakta.

TaskAwaiter tipi ile ilgili açıklamamızdan sonra tekrar MoveNext isimli metoda geri dönersek artık sanırım neden GetAwaiter metodunun çağrıldığını daha fazla açıklamamıza gerek yok. :)

GetAwaiter metodunun çağrılması sonrasında elde edilen TaskAwaiter tipi üzerinden ise bir takım kontroller yapılmakta. Bu kontrollerden ilki yapılan asenkron çağrımın sonlanıp sonlanmadığı ile ilgili. Eğer işlem sonlandıysa kod akışı doğrudan Label_0089 isimli alana goto ifadesi ile yönlenmekte. Eğer sonlamadıysa öncelikle state değişkeni 1’e çekilmekte daha sonra ise TaskAwaiter tipi içerisindeki OnCompleted metodu çağrılmakta ve callback olarak ta <DownloadStringAsync>d__0 tipi içerisindeki <>t__MoveNextDelegate alanı verilmekte. Yazımızın başlarında hatırlarsanız bu alana da MoveNext metodu atanmıştı !!! . Bu da demek oluyor ki asenkron işlem tamamlandıktan sonra yine MoveNext metodu çağırılıyor olacak. Sonrasında ise return ifadesi işletilerek MoveNext metodu sonlanmakta ve kontrol çağıran koda geri dönmekte yani bizim kodumuza. Bu şekilde de aslında bizim kullanıcı arayüzümüz cevap verebilir durumda olmakta.

Şimdi asenkron çağrım sonrasında işlemin sonlandığını ve TaskAwaiter tarafından OnCompleted metoduna parametre olarak geçirilen MoveNext metodunu işlemin sonlanmış olduğunu düşünerek tekrar işletelim. Bir üstteki paragrafta state değişkenine 1 değeri atanmıştı. Bu nedenle MoveNext metodu içerisindeki if kontrolleri girmeden doğrudan devam edecek ve ilk olarak state değişkenine 0 değeri atanacak ve sonrasında da Label_0089  ismiyle etiketlenmiş kod bloğu çalışacak. Bu kod bloğu içerisinde de TaskAwaiter tipi içerisindeki GetResult isimli metot kullanılarak asenkron metodun sonucu alınmakta.  Son olarak ise catch ifadesinin altında bulunan kısım işletilmekte ve state değişkenine –1 değeri atanmakta ve son olarakta AsyncTaskMethodBuilder tipi içerisindeki SetResult metodu kullanarak işlem sonucu AsyncTaskMethodBuilder tipi içerisindeki Task’ın içerisine yazılmakta.

MoveNext metoduna dikkat ettiyseniz bir de try-catch bloğu bulunmakta. Bu blok içerisinde de asenkron işlem sırasında oluşabilecek olan Exceptionlar yakalanmakta ve bir exception yakalanması durumunda state değişkeni –1’e çekilmekte ve oluşan exception da AsyncTaskMethodBuilder tipi içerisindeki SetException metodu çağrılarak AsyncTaskMethodBuilder tipinde bulunan Task’ın içerisindeki Exception alanına yazılmakta.

Genel yapıyı incelediğimizde aslında aklımıza C# dili içerisinde uzun süreden beri bulunan yield keywordünün alt yapısı gelmekte. Yield keywordü hatırlayacağınız üzere arka planda bir state machine yaratmaktaydı. C# 5.0 ile beraber gelen asenkron programlama yeniliklerinde de compiler aslında yine benzer bir yapıyı kullanarak bir state machine yaratmakta ve bu state machine içerisinde de callback atamalarını gerçekleştirmekte. Peki compiler neden bir state machine yaratmakta ? Aslında bunun durumu oldukça basit. Asenkron işlemin yönetimini kolay yapabilmek ve mevcutta bulunulan durumları saklayabilmek.

Özet olarak C# 5.0 ile gelen asenkron programlama yeniliklerinin arka planında callback tabanlı bir state machine yapısı yatmakta diyebiliriz.

Şu ana kadar Task tipinden bir nesne örneği döndüren asenkron metodun arka planını inceledik. Peki void dönüş tipine sahip olan asenkron metotlar nasıl yeniden yazılmakta ? Aslında çokta fazla bir değişiklik bulunmamakta. Bu yüzden de uzun uzadıya bir örnek yapıp incelememize gerek yok diye düşünüyorum. Aradaki tek fark AsyncTaskMethodBuilder<T> tipi yerine adından da anlayabileceğiniz üzere AsyncVoidMethodBuilder tipinin kullanımı olmakta. Compiler dönüş değeri void olduğu için herhangi bir Task tipi üretme işine girmemekte. :)

Compilerların arka olanda gerçekleştirdikleri işlemleri bilmek bazılarımıza gereksiz geliyor olsa da aslında bence oldukça önemli. Çünkü bir takım kullanımlar aslında baktığımızda bize legal olarak gözükmese de compiler arka planda bu programlama diline has özellikleri legal bir şekle sokmakta ve bu detayları da biliyor olmamız aslında bizim en doğru kullanımları yapmamızı sağlamakta. ;)

Umarım sizler için faydalı bir makale olmuştur.

Hoşçakalın,