C# 4.0 - Invariance, Covariance, Contravariance ???

Merhaba Arkadaşlar,

Bundan yıllar önce(aslında 2005 yılında...Çok eski bir tarih gibi görünmese de yazılım dünyası için çok çok uzun zaman önce anlamına gelmekte.) daha genç bir makale yazarıyken C# 2.0 delegate tiplerinde co-variance, contra-variance durumlarını incelemeye çalışmıştım. Kişisel görüşüme göre, anlaşılmasından ziyade iyi bir şekilde analiz edilerek anlatılması çok zor olan bir konu Co-Variance, Contra-Variance. Üstelik bu kavramların çıkış noktasında yer alan Variant, Invariant tip kavramları düşünüldüğünde konuyu anlamak için epey bir çaba sarf etmemiz gerekebiliyor. 

Hatta anlayamadığımız durumlarda neredeyse bulunduğumuz duruma isyan eder bir hale gelebiliyoruz. Bu tip zor konularda benim öğrenmek üzerine uyguladığım strateji aslında pek çoğumuzun da uyguladığı bir yöntem. Önce sorunu örnekler ile anlamaya çalışmak, getirilen çözümü görmek ve en son olarak tanımlamaları yapmak. Bu önce kavram tanımlaması, sonra örnek uygulamanın yapılmasından ziyade daha etkili bir öğrenme şeklidir diye düşünüyorum. Öyleyse vakit kaybetmeden analizimize başlayalım.

.Net' in başından beri...

Aslında .Net' in ilk duyurulduğu ve C#, Vb.Net gibi nesne yönelimli yönetimli dillerin(Managed Languages) dünyaya geldiği anlardan bu yana kalıtımsal ilişkide olan tipler arasında bazı referans geçişlerinin yapıldığını bilmekteyiz. Burada tiplerin polimorfik özellikte olabilmelerinin de payı büyük. Bu sebepten .Net Framework' ün tüm sürümlerinde aşağıdaki gibi bir kod parçası olası.

using System;

namespace Before
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteString(35);
            WriteString("Burak Selim Şenyurt");
            WriteString(true);
            WriteString(new Album { AlbumID = 1, Title = "Chikenfoot" });
        }

        static void WriteString(object obj)
        {
            Console.WriteLine(obj.ToString());
        }        
    }
    class Album
    {
        public int AlbumID { get; set; }
        public string Title { get; set; }
    }    
}

Bu kod parçasında yer alan WriteString metodu tüm .Net tiplerinin atası olan object tipinden parametre almaktadır. Bu ata tip-alt tip ilişkisi nedeniyle metoda herhangibir .Net tipinin atanması söz konusudur. Üstelik herkes bir Object tipi olduğundan ToString metodunu uygulamakta veya uygulamasa bile object tipinin varsayılan ToString metodu çalıştırılabilmektedir. Nitekim Album tipi içerisinde ToString metodu ezilmemiş olmasına rağmen Object sınıfındaki varsayılan ToString metodunun çalıştırılması sağlanmıştır. (Bildiğiniz üzere ToString metodu object tipi içerisinde virtual olarak tanımlanmıştır ve alt tiplerde ezildiği takdirde objcet nesne referansı üzerinden ezilen versiyonunun çalıştırılması söz konusudur.)

Yolumuza devam edelim ve bu sefer aşağıdaki kod parçasını göz önüne alalım.

static void Main(string[] args)
{
   object albm = CreateAlbum(2, "Is There a Love in space[Joe Satriani]");
}
static Album CreateAlbum(int albumId,string title)
{
   return new Album{AlbumID=albumId,Title=title};
}

Bu kod parçasında yer alan CreateAlbum metodu Album tipinden bir değer döndürmektedir. Main metodu içerisinde ise CreateAlbum çağrısı sonucunun object tipine atanması söz konusudur. Her iki kod parçasınında çalışma zamanında veya derleme zamanında bir hata üretmesini beklemeyiz. Şimdi koltuklarınıza yaslanın ve takip eden paragrafı okuyun...

İlk kod parçasının çalışması doğaldır nitekim .Net' in tüm sürümlerinde parametreler Covariant tiptedir. Diğer yandan ikinci kod parçasının da çalışması doğaldır çünkü dönüş tipleride Contravariant' tır.

Bir şey anladınız mı? Açıkçası ben halen daha durumu tam olarak netleştiremediğimizi düşünüyorum. Öyleyse...

Generic mimariden önceki koleksiyonlarda durum...

İşin içerisine object tipinden değerler ile çalışan generic mimari öncesi koleksiyonlar girdiğinde, Covariance veya Contravariance olmanın artık güvenli olup olmadığından söz edilmeye başlanmaktadır. Güvenlik tip bazındadır. Şimdi bu durumu anlamaya çalışarak devam edelim.

ArrayList albumList = new ArrayList();
albumList.Add(new Album { AlbumID = 1, Title = "Chikenfoot" });
albumList.Add(new Album { AlbumID = 2, Title = "Is There a Love in space[Joe Satriani]" });
albumList.Add(new Album { AlbumID = 3, Title = "Big Blue Ball [Peter Gabriel]" });

ArrayList gibi koleksiyonların object tipi ile çalışmalarının sebebi herhangibir tip için koleksiyon bazlı özelliklerin kullanılabilmesini sağlamaktır (Tabi bu, tip güvensiz-unsafe bir durumu oluşturmuş ve sonrasında generic mimari getirilmiştir). Diğer yandan object ile çalışmaları nedeniyle, herhangibir .Net tipini bünyesinde barındırabilirler. Buna göre ilk örneğimizi ve bu son kod parçasını göz önüne alırsak koleksiyonların Covariance özelliğini sağladığını(yani Covariant tipte olduklarını) düşünebiliriz. Ancak buda tam olarak doğru değildir. Şimdi aşağıdaki kod parçasını göz önüne alalım.

using System;
using System.Collections;

namespace Before
{
    class Program
    {
        static void Main(string[] args)
        {
            ArrayList albumList = new ArrayList();
            albumList.Add(new Album { AlbumID = 1, Title = "Chikenfoot" });
            albumList.Add(new Album { AlbumID = 2, Title = "Is There a Love in space[Joe Satriani]" });
            albumList.Add(new Album { AlbumID = 3, Title = "Big Blue Ball [Peter Gabriel]" });
            albumList.Add("Reality Killed The Video Star [Robbie Willams]");

            WriteAlbumList(albumList);
        }

        static void WriteAlbumList(ArrayList albums)
        {
            foreach (Album albm in albums)
            {
                Console.WriteLine(albm.ToString());
            }
        }
    }
    class Album
    {
        public int AlbumID { get; set; }
        public string Title { get; set; }

        public override string ToString()
        {
            return String.Format("{0} {1}", AlbumID.ToString(), Title);
        }
    }    
} 

Çok doğal olarak foreach döngüsü içerisinde albums koleksiyonu üzerinde dolaşılırken sadece Album tipleri ele alınmak istenmiştir. Ancak birisi kazayla albumList isimli koleksiyona string tipte bir değişken göndermiş ve yukarıdaki çalışma zamanı hatasının alınmasına neden olmuştur. Aman tanrımmmm!!! Sealed Şimdi koleksiyonlar için bahsedilen Covariant tipte oldukları gerçeği Un-Safe Covariant olarak düzeltilmelidir. Nitekim tip güvenliğinin garanti altına alınması mümkün olmamıştır. Hatta pek çok kaynak object tipi ile çalışan koleksiyonların aslında tamamen Invariant olduklarını ifade etmektedir. Kafamız gittikçe karışıyor değilmi. Öyleyse...

Peki ya diziler(Arrays)...

Aşağıdaki ekran görüntüsünde yer alan kod parçasını göz önüne alalım.

WriteAll metodu object tipinden bir dizi ile çalışmaktadır. Buna göre string tipinden bir dizinin bu metoda parametre olarak aktarılması mümkündür. Burada Covariance olma durumu söz konusudur. Elbette güvensiz olan versiyonu. Çünkü WriteAll metodu içerisinde bir tip güvenliği yoktur. Diğer yandan ilginç olan durum int tipinden bir diziyi göndermek istediğimizde ortaya çıkmaktadır. Böyle bir durumda derleme zamanı hatası alınacaktır. Int tipi değer tipi olduğundan object gibi bir referans türüne atanması mümkün değildir. İşte bu noktada object tipinden dizinin Invariance özellik gösterdiğini söyleyebiliriz.

Buna göre; Diziler bazı durumlarda Invariant bazı durumlarda ise Covariant tip olarak görülebilirler. Ancak Covariant olsalar dahi tip güvensiz olacaklardır(Unsafe). Diğer yandan değer türlü(Value Type) diziler her zaman için Invariantce özellik gösterir.

Generic koleksiyonlar...

Gelelim tip güvenli(Type Safe) olan generic koleksiyonlara. Generic mimari .Net içerisinde bir devrim yaratmış ve major versiyonlamaya gidilmesine neden olmuştur. Generic mimari sayesinde örneğin koleksiyonların sadece söylenen tiple çalışabileceği daha kodlama zamanındayken belirtilebilmekte ve böylece çalışma zamanında tip güvenliğinin aşılması engellenmektedir. Peki ya aşağıdaki kod parçasını göz önüne aldığımızda;

using System;
using System.Collections;
using System.Collections.Generic;

namespace Before
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteAll(new List<Album>{
                new Album { AlbumID = 1, Title = "Chikenfoot" },
                new Album { AlbumID = 2, Title = "Is There a Love in space[Joe Satriani]" }
            }
            );

        }
        static void WriteAll(IEnumerable<object> parameters)
        {
            foreach (object parameter in parameters)
            {
                Console.WriteLine(parameter.ToString());
            }
        }
    }
    class Album
    {
        public int AlbumID { get; set; }
        public string Title { get; set; }

        public override string ToString()
        {
            return String.Format("{0} {1}", AlbumID.ToString(), Title);
        }
    }    
}

Burada WriteAll metodu IEnumerable<object> tipinden bir parametre almaktadır. Buna göre tüm T generic tiplerinin object tipinden türeyeceği düşünüldüğünde herhangibir sorun olmayacağı sonucuna varılabilir (En azından kafamızdaki compiler bu kodu sorunsuz olarak derleyecektir. İlk etapta...Wink) Yani Covariance olma durumu söz konusudur diyebiliriz...Mi acaba? İşte derleme zamanının bize vereceği cevap...

Görüldüğü üzere Album tipi ile çalışan koleksiyonun object tipine dönüştürülemeyeceğini belirten bir hata mesajı ile karşı karşıyayız. Buna göre generic koleksiyonların aslında Invariant tipte olduklarını ifade edebiliyoruz. Bu nedenle generic koleksiyonlar ne Covariance nede Contravariance özellik göstermektedir. Aslında generic koleksiyonlarda özel bir durum olarak T tipleri için türetmenin söz konusu olmadığını söyleyebiliriz. Bu sebepten IEnumerable<Album> ün IEnumberable<object> tarafından taşınması söz konusu olmamaktadır. Birde aşağıdaki kod parçasını göz önüne alalım;

using System;
using System.Collections;
using System.Collections.Generic;

namespace Before
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<object> albums = GetAlbums();
        }

        static IEnumerable<Album> GetAlbums()
        {
            return new List<Album>
            {
                new Album { AlbumID = 1, Title = "Chikenfoot" },
                new Album { AlbumID = 2, Title = "Is There a Love in space[Joe Satriani]" }
            };
        }
    }
    class Album
    {
        public int AlbumID { get; set; }
        public string Title { get; set; }

        public override string ToString()
        {
            return String.Format("{0} {1}", AlbumID.ToString(), Title);
        }
    }    
}

GetAlbums metodu IEnumerable<Album> tipinden bir referans döndürmektedir. Album, Object' in alt tipidir. Buna göre GetAlbums metodunun sonucunun IEnumerable<object> tipinden bir arayüze atanabiliyor olması düşünülebilir. Oysaki derleme zamanında aşağıdaki hata mesajı alınacaktır.

Görüldüğü üzere bir dönüştürme hatası alınmaktadır.

Şimdi buraya kadar anlattıklarımızı gözden geçirecek olursak belki şu cümleleri sarf edebiliriz.

  • Invariant tiplerin kullanıldığı yerlerde, belirtilen tipin birebir aynısının ele alınması gerekmektedir(.Net 4.0 öncesi generic koleksiyonlar)
  • Covaraint tiplerin kullanılabildiği yerlerde, alt tipten olan değişkenlerin parametre olarak aktarılması mümkündür.
  • Covariant' lık için güvensiz olma(Unsafe) durumlarıda söz konusudur.(Object tipli koleksiyonlar ve diziler)
  • Contravariant' lığa göre ise dönüş tipinin metoddan dönen tipin üst tipiden bir nesne örneğine atanması mümkündür.
  • Covariant tipler için geçerli olan güvensiz tip sendromu doğal olarak Contravariant tipler içinde söz konusudur.

Peki .Net 4.0 sonrasında...

.Net 4.0 versiyonunda Generic koleksiyonların katı olan tip kavramı bozularak Covariant ve Contravariant olmalarına izin verildiğini söyleyebiliriz. Ancak burada önemli bir fark olduğundan bahsetmemiz gerekiyor. Generic koleksiyonların Covariant ve Contravariant olarak çalışabilmeleri sağlanırken bunun güvenli olaraktan(Type Safe) gerçekleştirilebilmesi sağlanmış. Bu noktada örneğin object tipinden olan koleksiyonların covariance ve contravariance davranışlarından ayrıldığını ifade edebiliriz. Peki bu yeni kabiliyetler nasıl aktarılmış?

İşte IEnumerable<T> arayüzünün .Net 4.0' da tanımlanış şekli.

out T tanımlaması mutlaka dikkatiniz çekmiştir. Bu durum output safe olarak adlandırılmaktadır. Şimdi başka bir tipi göz önüne alalım.

Bu sefer in T kullanımı dikkati çekmektedir. Bu durum ise Input Safe olarak adlandırılmaktadır. Her iki durumda alt tarafta öyle ele alınmaktadırki generic koleksiyonlarda Covariant ve Contravariant' lığın tip güvenli olarak ele alınması mümkün olmaktadır. Sonuç olarak kafamız karışsada biraz .Net 4.0 ile birlikte generic koleksiyonların tip güvenli olaraktan Covariant ve Contravariant özellik gösterebilmelerinin mümkün hale geldiğini söyleyebiliriz. Bununla birlikte out T ve in T kullanımlarının şu an için sadece generic koleksiyonlar ve temsilcilerde(delegate) söz konusu olduğunu belirtelim. Dolayısıyla generic temsilcilerinde tip güvenli Covariant veya Contravariant olarak ele alınmaları mümkündür. Buna göre out T ve in T için şu ifadeleri kullanabiliriz.

  • out T Covariant tip kullanımını sağlamaktadır. Buna göre örneğin IEnumerable<object> tipte parametre alan bir metoda, IEnumerable<Product> gibi bir referansın atanabilmesi mümkündür.
  • in T Contravariant tip kullanımı sağlamaktadır. Buna göre örneğin IEnumberable<string> dönen bir metodun sonucunun IEnumerable<Object> tipine atanması mümkündür.

Dolayısıyla aşağıdaki örnek kod parçası sorunsuz olarak derlenecek ve çalışacaktır.

using System.Collections.Generic;

namespace NowInNet4
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Product> products = new List<Product>
            {
                new Product{ProductId=1,Name="Americano Coffee",ListPrice=10},
                new Product{ProductId=2,Name="English Royal Tea",ListPrice=12}                
            };

            Process(products);

            IEnumerable<object> allProducts = GetProducts();
        }

        static IEnumerable<Product> GetProducts()
        {
            return new List<Product>
            {
                new Product{ProductId=1,Name="Americano Coffee",ListPrice=10},
                new Product{ProductId=2,Name="English Royal Tea",ListPrice=12}                
            };
        }

        static void Process(IEnumerable<object> parameters)
        {
            // Bir takım işlemler
        }
    }
    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public decimal ListPrice { get; set; }
    }
}

Ben en azından biraz olsun anlamış durumdayım. Umarım sizlerede en iyi şekilde aktarabilmişimdir. Tekrardan görüşünceye dekhepinize mutlu günler dilerim.

CoContraVariance.rar (38,87 kb)

Yorumlar (1) -

Yorum ekle

Loading