Tag Archives: struct

C#’ta Interface (Arayüz) Kavramı

C#’ta arayüzler, sınıf ve struct yapılarının ne yapması gerektiğini belirten yapılardır. Bir arayüz; method, özellik (properties), olay (event) ve indexleyici (indexers) yapılarını içerebilir. Bir arayüzün içindeki metodların gövdeleri yoktur. Yani bir arayüz hiçbir zaman hiç bir şekilde bir uygulama sağlamaz. Bir arayüzü birden fazla sınıf ya da struct kullanabildiği gibi, bir sınıf ya da struct yapısı da birden fazla arayüz uygulayabilirler.

Arayüzler çoklu kalıtımı (multiple inheritance) desteklerler. Örneğin; aşağıdaki ICombobox arayüzü hem ITextBox hem de IListBox arayüzlerinden kalıtım alır:

interface IControl 
{ 
    void Boyama(); 
} 
interface ITextBox : IControl 
{ 
    void YazıBelirler(string text); 
} 
interface IListBox : IControl 
{ 
    void OgeBelirle(string[] items); 
} 
interface IComboBox : ITextBox, IListBox { }

Sınıflar ve struct yapıları da çoklu kalıtımı desteklerler. Aşağıda, EditBox sınıfı hem IControl hem de IDataBound arayüzlerinden kalıtım alır:

interface IDataBound 
{ 
    void Bind(); 
} 
public class EditBox : IControl, IDataBound 
{ 
    public void Boyama() { } 
    public void Bind() {}

}

Bir sınıf ya da struct belirli bir arayüzü uyguladığında, o sınıf ya da struct örneği, dolaylı olarak arayüz tipine çevrilebilir. Örneğin;

EditBox editBox = new EditBox(); 
IControl control = editBox; 
IDataBound dataBound = editBox;

Arayüzler aşağıdaki nedenlerden dolayı özeldirler;

  • Bir sınıf birden fazla arayüz uygulayabilir. Buna karşılık, bir sınıf sadece bir sınıftan kalıtım alabilir.
  • Arayüz üyelerinin tümü dolaylı olarak soyuttur (abstract). Buna karşılık, bir sınıf hem soyut hem de somut üyelerin uygulanmasına olanak sağlar.
  • Struct yapıların arayüzleri uygulayabilir. Buna karşılık, bir struct yapısı bir sınıf yapısından kalıtım alamaz.

Bir arayüz tanımlanması tıpkı sınıf tanımlamasına benzer, fakat bir arayüz kendi üyelerinin hepsi dolaylı olarak soyut oldukları için bunlara herhangi bir uygulama sağlamaz. Bu üyeler, bu arayüzü uygulayan sınıf ya da struct yapıları tarafından kullanılırlar.

System.Collections namespace’inde bulunan IEnumerator arayüzünün tanımı şu şekildedir;

public interface IEnumerator 
    { 
        bool MoveNext(); 
        object Current { get; } 
        void Reset(); 
    }

Bir arayüz üyesi her zaman dolaylı olarak public özelliktedir ve herhangi bir erişim değiştiricisi tanımlanamaz. Bir arayüzü uygulamak onun tüm elemanlarını public olarak kullanılmasına olanak sağlamak anlamına gelir.

internal class GeriSayim : IEnumerable 
{ 
    int sayac = 10; 
    public bool MoveNext() { return sayac-- > 0; } 
    public object Current { get { return sayac; } } 
    public void Reset() { throw new NotSupportedException(); } 

}

Dolaylı olarak bir nesneyi herhangi bir arayüz tipine çevirebilirsiniz;

IEnumerator e = new Countdown();

Bir arayüz başka bir arayüzden türetilebilir. Bu sayede türetilen arayüz diğer arayüzün tüm üyelerini kalıtım alır. Örneğin;

public interface IGeriAl 
     { 
         void GeriAl(); 
     } 
     public interface IYenidenYap : IGeriAl 
     { 
         void YenidenYap(); 
     }

Burada IYenidenYap, IGeriAl arayüzünün tüm üyelerini kalıtım alır.

Açık Arayüz Uygulaması (Explicit Interface Implementation)

      Birden fazla arayüz uygulaması bazen üye imzalarının çarpışması sonuçları doğurabilir. Bu durumu arayüz üyesine uygulanan

açıkça uygulama(explicitly implementing)

    şeklinde çözebilirsiniz. Şu aşağıdaki koda göz atalım;
class Test 
{ 
    interface I1 
    { 
        void Foo(); 
    } 
    interface I2 
    { 
        int Foo(); 
    } 
    public class Arac : I1, I2 
    { 
        public void Foo() 
        { 
            Console.WriteLine("I1.Foo uygulamasi"); 
        }

        int I2.Foo() 
        { 
            Console.WriteLine("I2.Foo uygulaması"); 
            return 23; 
        } 

    } 
}

Görüldüğü üzere, hem I1 hem de I2 arayüzlerinin çelişkili Foo imzaları var. Arac sınıfı, açık bir şekilde I2 arayüzünün Foo methodunu uygular. Bu durum bu iki methodun aynı sınıf içerisinde bir arada olmasına olanak verir. Bu üyelerin çağrımını ancak arayüz çevrimleri ile sağlayabiliriz:

Arac a = new Arac(); 
a.Foo();       // I1.Foo uygulanır. 
(I1)a.Foo(); // I1.Foo uygulanır. 
(I2)a.Foo(); // I2.Foo uygulanır.

Arayüz Üyelerini Sanal Uygulama (Implementing Interface Members Virtually)

Açıkça uygulanmış arayüz üyeleri default olarak sealed tanımlıdır. Temel sınıfta bu üyeler virtual ya da abstract olarak tanımlanmak zorundadır. Örneğin;

using System; 
using System.Collections; 
class Test 
{ 
    public interface IGeriAl 
    { 
        void GeriAl(); 
    } 
    public class Sınıf1 : IGeriAl 
    { 
        public virtual void GeriAl() 
        { 
            Console.WriteLine("Sınıf1.GeriAl()"); 
        } 
    } 
    public class Sınıf2 : Sınıf1 
    { 
        public override void GeriAl() 
        { 
            Console.WriteLine("Sınıf2.GeriAl()"); 
        } 
    } 
    public static void Main() 
    { 
        Sınıf2 s = new Sınıf2(); 
        s.GeriAl();                  // Sınıf2.GeriAl() 
        ((IGeriAl)s).GeriAl();  // Sınıf2.GeriAl() 
        ((Sınıf1)s).GeriAl();    // Sınıf2.GeriAl() 
    } 
}

Bir arayüz elemanını temel sınıftan doğrudan çağırmak ya da alt sınıftan arayüz çağrısı ile çağırmak bize aynı sonucu verir.

Altsınıflarda Arayüzü Tekrar Uygulama

Bir altsınıf, temel sınıf tarafından uygulanmış arayüz üyelerini tekrar tekrar uygulayabilir. Tekrar uygulama, üye uygulamasını kaçırır ve temel sınıftaki üyeler virtual olsun ya da olmasın çalışır. Aşağıdaki örnek ile daha iyi anlatmaya çalışalım;

Bu örnekte; TextBox sınıfı IGeriAl.GeriAl() methodunu açıkça uygular. Bu yüzden virtual olarak işaretlenemezler. Override etmek için, RichTextBox sınıfı IGeriAl.GeriAl() methodunu yeniden uygulamak zorundadır.

public interface IGeriAl 
    { 
        void GeriAl(); 
    } 
    public class TextBox : IGeriAl 
    { 
        void IGeriAl.GeriAl() 
        { 
            Console.WriteLine("TextBox.GeriAl"); 
        } 
    } 
    public class RichTextBox : TextBox, IGeriAl 
    { 
        public new void GeriAl() 
        { 
            Console.WriteLine("RichTextBox.GeriAl"); 
        } 

    }

Altsınıfın uygulamasında arayüz çağrısı olarak tekrar uygulanmış üyeleri çağırmak için;

RichTextBox r = new RichTextBox(); 
r.GeriAl(); //RichTextBox.GeriAl 
((IGeriAl)r).GeriAl();  //RichTextBox.GeriAl

Peki ya TextBox sınıfımızı aşağıdaki şekilde değiştirirsek?

public class TextBox : IGeriAl 
    { 
        public void GeriAl() 
        { 
            Console.WriteLine("TextBox.GeriAl"); 
        }

    }

Bu özellik bize GeriAl() methodunu farklı bir şekilde çağırma olanağı sağlar.

RichTextBox r = new RichTextBox(); 
r.GeriAl();                   // RichTextBox.GeriAl 
((IGeriAl)r).GeriAl();    // RichTextBox.GeriAl 
((TextBox)r).GeriAl();  // TextBox.GeriAl

Bu üçüncü durum bize gösteriyor ki, bu durumda bir uygulama yapılması bir üyenin base sınıftan değil de direkt arayüz tarafından çağrılması durumlarında etkin hale gelir. Bu genellikle istenilmeyen bir durumdur.

Arayüzler ve Boxing

Bir struct yapısını bir arayüz yapısına çevirmek boxing işlemidir. Dolaylı olarak uygulanmış struct yapısını çağırmak boxing işlemi değildir.

         interface I 
        { 
            void Metod(); 
        }

        struct S : I 
        { 
            public void Metod() { } 
        }

        S str = new S(); 
        str.Metod(); //Boxing değil

        I i = S; 
        i.Metod(); //Arayüz yapısına çevirdiğimizde boxing gerçekleşir.

Sınıf vs. Arayüz Yazılması

Temel olarak;

  • Uygulama aşamasında doğal olarak paylaşıllan tipler için sınıf ya da alt sınıflar kullanın.
  • Bağımsız uygulamaya sahip tipler için arayüzler kullanın.

Aşağıdaki sınıflar göz önüne alalım;

    abstract class Hayvan { } 
    abstract class Kus : Hayvan { } 
    abstract class Bocek : Hayvan { } 
    abstract class Kusgiller : Hayvan { } 
    abstract class Etobur : Hayvan { }

    //Somut Sınıflar

    class DeveKusu : Kus { } 
    class Kartal : Kus, Kusgiller, Etobur { } //Geçersiz 
    class Arı : Bocek, Kusgiller { } //Geçersiz 
    class Pire : Bocek, Etobur { } //Geçersiz

Kartal, Arı ve Pire sınıfları derlenmezler çünkü birden fazla sınıftan kalıtım almaları yasaktır. Bunu çözmek için, bazı tipleri arayüz tipine çevirmek zorundayız. Tabi burada soru şudur, hangi tipleri? Genel kuralımızı takip ederek, Bocek ve Kus sınıfları uygulamalarını paylaşırlar. Bu yüzden bunlar sınıf olarak kalmak zorundadır. Buna karşılık, Kusgiller uçmak için bağımsız bir mekanizmaya sahiptir ve Etobur et yiyenler için bağımsız stratejilere sahiptir. Bu yüzden bu sınıfları arayüz tipine çevirmemiz gerekir.

interface IKusgiller { } 
interface IEtobur { }

C#’ta Struct Yapısı

Sınıflar gibi, struct yapıları veri ve fonksiyon üyeleri içeren veri yapılarıdır. Sınıflardan farklı olarak, struct yapısı değer tipidir ve heap bölge tahsisi gerektirmez. Bir struct değişkeni, direkt olarak struct’ın verisini tutar. Oysa sınıf tipinde bir değişken, dinamik olarak ayrılmış nesneye referans tutar. Struct yapısı, kullanıcı tanımlı kalıtımı desteklemez ve tüm struct yapıları dolaylı olarak object tipinden kalıtım alır.

Önemli: Struct yapılarının heap bölge tahisisi gerektirmemesi, bu yapıların hiçbir zaman bunu yapmadıkları anlamına gelmez.

Bu yapılar özellikle küçük veri yapıları kullanımında kullanışlı olurlar. Karmaşık sayılar ve koordinat sisteminde noktalar struct yapılarına iyi örneklerdir. Bir yapıda sınıf yerine struct kullanımı, bellek tahsis etmede ve programınızın performansında büyük farklılıklar yaratabilir.

Örneğin; aşağıdaki program 100 nokta oluşturan bir programdır. Sınıf yapısı ile kullandığımızda 101 adet farklı nesne – 1 adet dizi için, 100 adet te dizi üyeleri için – oluşturulur:

using System;


class Nokta

{

    public int x, y;


    public Nokta(int x, int y)

    {

        this.x = x;

        this.y = y;

    }

}

class Test

{

    static void Main()

    {

        Nokta[] noktalar = new Nokta[100];

        for (int i = 0; i < 100; i++)

            noktalar[i] = new Nokta(i, i);

    }

}

struct-1

Alternatif olarak Nokta yapısını struct yapabiliriz.

struct Nokta

{

    public int x, y;


    public Nokta(int x, int y)

    {

        this.x = x;

        this.y = y;

    }

}

Burada sadece 1 nesne yaratılır – 1 adet dizi için – ve Nokta örnekleri dizi içerisinde saklanırlar.

struct-2

Önemli: Struct yapısının performans açısından sağladığı faydayı “Her zaman struct kullanın” şeklinde algılamak yanlış olur. Tabi ki bazı senaryolarda struct yapısına bellek ayırmak ve bellekten almak daha az zaman alır fakat her struct ataması bilindiği gibi değer kopyalamasıdır (value copy). Bu her zaman referans kopyalamasından daha fazla zaman alır.

Struct yapıcıları new operatörü ile çağrılırlar fakat bu demek değildir ki belirli bir bellek ayrılmıştır. Nesnenin veya referansının dinamik olarak bellek ayrımı yerine, bir struct yapıcısı basitçe struct değerinin kendisini döndürür (genellikle yığının geçici bölgesinde) ve bu değer gereli olduğunda kopyalanır.

Sınıflarda, iki farklı değişken aynı nesneye referans gösterebilir ve bu şekilde bir değer üzerindeki işlemler aynı nesneyi referans eden diğer değişkeni etkileyebilir. Struct yapılarında, her değişken kendi verisinin kopyasını tutar ve bir değişkenin diğerini etkilemesi imkansızdır. Örneğin aşağıdaki kodu inceleyelim;

Nokta a = new Nokta(10, 10);

Nokta b = a;

a.x = 20;

Console.WriteLine(b.x);

Eğer buradaki Nokta örneği bir sınıf (class) ise, sonucumuz 20 olur çünkü a ve b aynı nesneye referans gösterirler. Eğer Nokta örneği bir struct ise, sonucumuz 10 olur çünkü ikinci satırdaki atama işlemi değer olarak a’daki değerleri b’ye kopyalar ve bu kopyalama 3. satırdaki a.x atamasını etkilemez.

Bu örnekte struct kullanımının 2 noktasının farkına vardır. Birincisi, tüm struct yapısını kopyalamak bir nesne referansını kopyalamaktan daha az verimli. Böylece atama yapma ve değer parametresi geçirme struct yapılarında referans tiplerine göre daha maliyetli olabilir. İkincisi, ref ve out parametreleri dışında, struct yapısına referans oluşturma imkansızdır.

Bill Wagner: Eğer tüm durumlarda değer anlamı (value semantics) istemiyorsanız, sınıf kullanmalısınız. Sınıflar bazı durumlarda değer anlamı uygulayabilirler, (string gibi) ama default olarak referans anlamına uyarlar. Bu fark sizin tasarımınızda yığın vs hesap bellek ayrımına göre daha çok farklılık gösterir.