Tag Archives: csharp

Codecademy “Learn C#” Online Programlama Eğitimi – [4. Kısım]

Codecademy üzerinden “Learn C#” adındaki programlama eğitiminin 4. kısmını bu Twitch yayını ile tamamladık. Bu yayında konuştuğumuz konu başlıkları şu şekilde;

➡️ Expression-bodied definitions (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members)

➡️ Lambda expressions (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions)

➡️ Dizi veri yapısı

➡️ Dizi tanımlama, elemanlarına erişim ve elemanlarını değiştirme, eleman sayısını bulma

➡️ Dizilerde “index” kavramı

➡️ System.Array sınıfına ait bazı hazır metod kullanımları (Sort(), Find(), IndexOf(), Clear())

🔥 1. Kısım: https://www.youtube.com/watch?v=0X7bEBSxygg

🔥 2. Kısım: https://www.youtube.com/watch?v=ziJPCgzvQjE

🔥 3. Kısım: https://www.youtube.com/watch?v=KGaN0O9Nf2U

👍 İlk ziyaretiniz mi? O zaman sizi şöyle alalım: https://bit.ly/sonergonul-youtube-subs

✅ Codecademy: “Learn C#” online programlama kursu: https://www.codecademy.com/learn/learn-c-sharp

C#’ta Fonksiyon Üyeleri

C#’ta çalıştırılabilir kod içeren üyeler genellikle sınıfın fonksiyon üyeleri olarak adlandırılırlar. Bir önceki yazıda belirttiğim konu olan methodlar konusu da aslında bir çeşit fonksiyon üyesidir. Aşağıda tablo Liste<T> olarak tanımlanmış bir generic sınıfın bir çok fonksiyon üyesi örneğini gösterir:

public class Liste<T> {
const int varsayılanKapasite = 4; Sabit
T[ ] elemanlar;
int sayaç;
Alanlar
public Liste (int kapasite = varsayılanKapasite ) {
elemanlar = new T[kapasite];
}
Yapıcı
public int Sayma {
get { return sayma; }
}
public int Kapasite {
get {
return elemanlar.Length;
}
set {
if (value < sayma) value = sayma;
if (value != elemanlar.Length) {
T[] yenielemanlar= new T[value];
Array.Copy(elemanlar, 0, yenielemanlar, 0, sayma);
elemanlar = yenielemanlar;
}
}
}
Özellikler
public T this[int index] {
get {
return elemanlar[index];
}
set {
elemanlar[index] = value;
OnChanged();
}
}
İndexleyiciler
public void Add(T item) {
if (sayma == kapasite) kapasite = sayma * 2;
elemanlar[sayma] = item;
sayma++;
OnChanged();
}
Methodlar
public event EventHandler Changed; Olaylar (Events)
public static bool operator ==(Liste<T> a, Liste<T> b) {
return Equals(a, b);
}
public static bool operator !=(Liset<T> a, Liste<T> b) {
return !Equals(a, b);
}
Operatörler
}
  • Yapıcılar (Constructors)

C# hem örnek hem de static yapıcıları destekler. Örnek yapıcılar, sınıfın ilk kullanımına hazırlamak için gerekli olan uygulama üyeleridir. Static yapıcılar, bir eylem gereken örnek sınıfın ilk yüklendiği zaman gerekli olan uygulama üyeleridir.

Bir yapıcı aynı bir method gibi tanımlanır fakat geri dönüş tipi olmamalı ve içerdiği sınıf ile aynı isme sahip olmalıdır. Eğer bir yapıcı, static anahtar kelimesi ile tanımlanmışsa o static yapıcıdır, aksi taktirde örnek yapıcı olarak adlandırılırlar.

Örnek yapıcılar aşırı yüklenebilir. Örneğin, Liste<T> adında bir sınıfın iki yapıcısı tanımlansın; birisini hiçbir parametre almayan, diğeri de bir adet int parametre alan. Örnek yapıcılar new operatörü kullanılarak çağırılırlar.


Liste<string> liste1 = new Liste<string>();
Liste<string> liste2 = new Liste<string>(10);

Diğer üyelerin aksine, örnek methodlar miras almazlar. Yani bir sınıfın kendi içinde tanıtılmış örnek yapıcısından başka bir örnek yapıcısı olamaz. Eğer bir sınıfın örnek yapıcısı yoksa, parametresiz boş bir örnek yapıcı otomatik olarak yaratılır.

Önemli: Yapıcılar tembel olmalıdırlar. En iyi pratik olarak yapıcılar içinde minimum iş yapılamıdır ki bu da basit bir şekilde argümanların daha sonra kullanımı için basitleştirmelidir. Örneğin; bir dosyanın ismini ya da o dosyanın yolunu veritabanı için tutmak istiyorsunuz ama dış kaynakları gerekli olmadığı sürece açmak istemiyorsunuz. Bu pratik, gerekli en az kaynağa erişimede emin olmak için yardımcı olur.

  • Özellikler (Properties)

C#’ta özellikler, alanların (fields) doğal uzantılarıdır. İkisi de ilişkilendirildikleri tiplerile birlikteki üyelerin isimleriyle, ilişkilendirildikleri alanların sözdizimi ile ve özelliklerle isimlendirilirler. Fakat, alanlardan farklı olarak, özellikler depolanmış bellek göstermezler. Bunun yerine, özelliklerin erişimcileri vardır ki onların değerlerini okuma ya da yazmada kullanılan ifadeleri belirtmede kullanılırlar.

Önemli: İyi bir uygulama tekniği olarak alanlara (özelliklerin verilerine) her zaman Getter ve Setter ile ulaşmak önerilir. Eğer Getter veya Setter’larınıza fonksiyonel işlevsellikler eklemek isterseniz, kodunuzu herhangi bir şekeilde kırmadan bunu kolaylıkla yapabilirsiniz.

Yukarıdaki anlatıma bir örnek verelim: Hiçbir zaman alanlarınızı (fields) public olarak tanımlamayın;


class İnek
{
public int Süt; // Kötü
}

Eğer belleğin yanında herhangi bir katman istemiyorsanız, bırakın derleyici sizin yerinize uygulasın;


class İnek
{
public int Süt {get; set;} // İyi
}

Bu sayede, istemci özelliğe Getter ve Setter’ı bağlar ve buna istinaden aşağıdaki şekilde derleyicinin uygulamasına etki edebilirsiniz;


class İnek
{
bool SüteSahip = false;
int Süt;
public int Süt {
get
{
if (!SüteSahip)
{
Süt = SütMakinesiUygula();
SüteSahip = true;
}
return Süt;
}
set
{
TersSütMakinesiUygula(value);
Süt = value;
}
}
}

Ayrıca, aşağıdaki kullanım ile hesaplanmış değer sizin programınızda kullanılır;


class İnek
{
public İnek()
{
Süt = SütMakinesiUygula();
}
public int Süt { get; private set; }
...
}

Bu durumda, özelliği tekrar hesaplıyoruz, ki neye ihtiyacımız olduğunu bilmediğimiz durumlarda bu gereksiz bir davranıştır.

Önemli: Özelliklere erişim tıpkı alanlara erişim gibi gözükür insanlara. Ve kişiler de doğal olarak alanlara erişim ile aynı performans göstereceklerini tahmin ederler. Eğer bir erişim belirgin bir iş ihtiyacı duyuyorsa (bir dosyadan okuma, veritabanında bir query çalıştırma gibi) bir method olarak açılmalıdır, bir özellik olarak değil. Bunu çağıranlar methodun daha fazla iş yaptığını düşünürler.

Aynı sebepten dolayı, özelliklere erişimde tekrarlı çağırımlar (koda müdahele etmeden) aynı değeri döndürmelidirler. DateTime.Now bu tavsiyeye uymayan örneklere sahiptir.

Bir özellik, bir alan ile aynı şekilde tanımlanır. İstisna olarak, noktalı virgülle bitirmek yerine Get ve Set erişimcilerini { ve } arasında kullanarak tanımsal biçimde alanlardan ayrılırlar. Hem Get hem de Set erişimcisine sahip özellikler okunur-yazılır (read-write) özellik, sadece Get erişimcisine sahip özellikler sadece okunur (read-only) ve sadece Set erişimcisine sahip özellikler de sadece yazılır (write-only) özellikler olarak tanımlanır.

Bir Get erişimcisi parametresiz bir method ve özellik tipini geri dönüş tipi olarak uygulayan bir method ile uyuşur. Belirlenen hedefin atamalar dışında, bir özellik bir ifadeyi referans ediyorsa, özelliğin Get erişimcisi, özelliğin değerini hesaplamada çağrılır.

Bir Set erişimcisi, value adında bir parametre içeren ve geri dönüş tipi olmayan bir method ile uyuşur. Bir özellik belirli bir atamayı ya da ++ ya da – gibi operandları referans ediyorsa, Set erişimcisi yeni değeri içeren bir argüman ile çağrılır.

Liste<T> sınıfı iki adet özellik tanımlar, Sayma ve Kapasite, sırasıyla sadece okunur ve okunur-yazılır özellikler. Aşağıdaki örnek bunların nasıl kullanıldığına bir örnektir:


Liste<string> isimler = new Liste<string>();
isimler.Kapasite = 100;   // Set erişimcisi çağrılır.
int i = isimler.Sayma;      // Get erişimcisi çağrılır.
int j = isimler.Kapasite;  // Get erişimcisi çağrılır.

Alanlarda ve özelliklerde benzer olarak, C# hem örnek özellikleri hem de static özellikleri destekler. Static özellikler static anahtar kelimesi ile tanımlanır, static anahtar sözcüğü kullanılmayan özellikler örnek özelliklerdir.

Bir özelliğin erişimcileri (Get ve Set) sanal (virtual) olabilirler. Eğer bir özellik tanımlaması virtual, abstract ya da override anahtar sözcüklerine sahipse, o özelliğin erişimcilerine uygular.

Önemli: Eğer bir sanal özellik, private erişimciye sahipse, o erişimci CLR içinde sanal olmayan methodtur ve türetilmiş sınıf içerisinde aşırı yüklenemez.

Özellikler aşağıdaki erişimcilere sahiptirler;

Statik değiştirici static
Erişim değiştirici public, internal, private, protected
Kalıtımsal değiştirici new, virtual, abstract, override, sealed
Yönetilemeyen kod değiştiricisi unsafe, extern
  • İndexleyiciler (İndexers)
    İndexleyiciler, diziler gibi, nesneleri indexleyebilen üyelerdir. Bir indexleyici, bir özellik ile aynı şekilde tanımlanır, tek fark olarak [ ] arasında parametre listesi hariç. Bu parametreler, indexleyicinin erişimcisi için uygundurlar. Özellikler gibi, indexleyiciler de read-write, read-only, write-only ya da bir indexleyici virtual tanımlanabilir.

Aşağıdaki List sınıfı string parametresi olan basit bir indexleyici tanımlamıştır;


List<string> isimler = new List<string>();
isimler.Add("Ülkü");
isimler.Add("Hülya");
isimler.Add("İlknur");
for (int i = 0; i < isimler.Count; i++) {
string s = isimler[i];
isimler[i] = s.ToUpper();
}

İndexleyiciler aşırı yüklenebilirler. Bunun anlamı sınıflar istedikleri uzunlukta parametre listeleri içeren indexleyici tanımlayabilirler.

  • Olaylar (Events)

Bir olay, sınıflara ya da nesnelere bildirim sağlanmasına olanak veren üyelerdir. Olaylar, tıpkı alanlar gibi tanımlanırlar. Fark olarak event anahtar kelimesi kullanılır ve tipi delegate tipi olmalıdır.

Jesse Liberty: Gerçekte, event anahtar sözcüğü sadece C#’ta delegate’in kullanılabilir olmasına bir yol göstericidir.

Chris Sells: event anahtar sözcüksüz, şunu yapabilirsiniz;


delegate void CalismaTamamlandi();
class Calisan
{
public  CalismaTamamlandi Tamamlandi; // Delegate alanı, bir event değil.

}
class  Patron
{
public void CalismaTamamlandi() { }
}
class Program
{
static void Main()
{
Patron Soner = new Patron();
Calisan isci = new Calisan();
isci.Tamamlandi += Soner.CalismaTamamlandi(); // Yapmak istediğiniz
isci.Tamamlandi = Soner.CalismaTamamlandi();  // Derleyicinin izin verdiği
}
}

Ne yazık ki, event sözcüğü ile birlikte, Tamamlandi() sadece delegate tipinde public bir alandır. Derleyici için de sorun oluşturmaz bu durum. Fakat event anahtar sözcüğünü eklediğimizde, += ve –= gibi operatörleri sınırlandırırsınız.


class Calisan
{
public event CalismaTamamlandi Tamamlandi;

}

isci.Tamamlandi += Soner.CalismaTamamlandi(); // Derleyici izin verir.
isci.Tamamlandi = Soner.CalismaTamamlandi(); // Derleyici hata verir.

Burada event sözcüğünün kullanımı sadece bir kere public bir alan oluşturmanın doğaln olmasıdır.Çünkü derleyici güvenli operatörlerin kullanımını daraltır.

Olay üyesi tanımlayan bir sınıf içerisinde, olaya delegate tipindeki bir alana erişimi gibi erişilebilir. Alan, olaylara eklenen olay tutucuları (event handler) temsil eden delegate’e bir referans saklar. Eğer herhangi bir event handler mevcut değilse, o alanın değeri null olur.

List<T> sınıfı basit bir olay üyesi olan Changed tanımlar ki bu yeni elemanların listeye eklenmesini gösterir. Changed olayı, OnChanged sanal methodundan kullanılır ki bu ilk önce olayın boş olup olmadığını kontrol eder. (Bunun anlamı hiçbir event handler’ın olmaması)

İstemciler olaylara olay tutucular (event handler) ile tepki gösterirler. Event handler’lar += operatörü ile eklenir ve –= operatörü ile silinebilirler. Aşağıdaki örnek, bir event handler’ın List<string>’in Changed olayına eklenmesini gösterir;


using System;
using System.Collections.Generic;

class Test
{
static int DegismeSayisi;
static void ListChanged(object sender, EventArgs e)
{
DegismeSayisi++;
}
static void Main()
{
List<string> isimler = new List<string>();
isimler.Changed += new EventHandler(ListChanged);
isimler.Add("Ülkü");
isimler.Add("Hülya");
isimler.Add("İlknur");
Console.WriteLine(DegismeSayisi); // Çıktı 3 olur.
}
}

Chris Sells: isimler.Changed += new EventHandler(ListChanged); yerine isimler.Changed += ListChanged; şeklinde bir kısaltma da kullanılabilir.

  • Yok Ediciler (Destructors)

Bir destructor, bir sınıf örneğini ya da nesnesini yoketmek için kullanılan uygulamaları içerir. Yokedicilerin parametreleri yoktur, erişim kelimelerine sahip değillerdir ve açık bir şekilde çağrılamazlar. Yokediciler otomatik olarak garbage collection zamanında çalıştırılırlar.

Garbage collector, nesnelerin ne zaman toplanacağına ve yokedicilerin ne zaman çalıştırılacağına imkan verir. Özellikle, yokedicilerin çağrılma zamanların rastgele değildir, ve yokediciler herhangi bir thread zamanında çalıştırılabilir. Bu nedenle, sınıflar yokedicileri  sadece başka çözümler olmadığı zamanda uygulamalıdırlar.

Önemli: Yokediciler bazen “Finalizer” olarak anılırlar. Bu isim aynı zamanda Garbage collector API’sinde de görünür; örneğin: GC.WaitForPendingFinalizers.

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 Enum Tipine Genel Bakış

C#’ta enum tipi, sabitlerle isimlendirilmiş ayırt edici veri tipidir. Aşağıdaki örnek 3 sabit değer ile oluşturulmuş enum tipi olan Renk adında bir yapı tanımlayalım:

using System;

namespace ConsoleApplication1
{

enum Renk
 {
 Kırmızı,
 Yeşil,
 Mavi
 }

class Test
 {
 static void RenkBastır(Renk renk)
 {
 switch (renk)
 {
 case Renk.Kırmızı:
 Console.WriteLine("Kırmızı");
 break;
 case Renk.Yeşil:
 Console.WriteLine("Yeşil");
 break;
 case Renk.Mavi:
 Console.WriteLine("Mavi");
 break;
 default:
 Console.WriteLine("Bilinmeyen renk");
 break;
 }
 }

static void Main()
 {
 Renk r = Renk.Kırmızı;
 RenkBastır(r);
 RenkBastır(Renk.Mavi);
 }
 }
}
 

Her enum tipi underlying type adı verilen bir enum tipi ile ilişkilendirilir. Bir enum, underlying type içerisinde formatını ve mümkün olan değerin uzunluğunu tutar. Varsayılan olarak;

  • Her underlying değerleri int tipindedir.
  • Sabitler 0, 1, 2, 3.. enum üyelerinin tanımlama sıralarına göre otomatik olarak atanır.

Aşağıdaki örnek, sbyte tipinde bir underlying type ile Hiza adından bir enum tipini tanımlar;

enum Hiza : sbyte
 {
    Sol = -1,
    Orta = 0,
    Sağ = 1
 } 

Bu örnekte de görüldüğü gibi, bir enum üyesi tanımlama aşamasında sabit bir değere atanabilir. Burada atadığımız her sabitin underlying type (sbyte) tipinin aralığı içerisinde bulunmak zorundadır. Eğer bir enum tipi tanımlamasında açıkça belirli bir değere atanmamışsa, ilk değeri belirtmek için 0 (sıfır) değeri verilir ve tanımlama sıralamasına göre 1 artarak ilerler.


int i = (int)Renk.Mavi; // int i = 2;
Renk r = (Renk)2; // Renk r = Renk.Mavi

Enum tipleri için 0 özel bir değere sahiptir. Bu değer bir enum tipine atandığında otomatik olarak bu int değeri bir enum tipine çevrilir. Örneğin;


Renk r = 0;
Console.WriteLine(r);

Şeklinde bir kod yazdığımızda bu kodun çıktısı “Kırmızı” olur. Bu atama işlemini Hiza enum’u ile denemiş olsaydık bu enum’da bulunan Orta değeri 0 değerine atandığı için bu sefer ilk elemanı değil Orta değerini ekrana yazdırmış olacaktır. Fakat bu atama ve çevrilme işlemleri sadece 0 değeri için geçerlidir. Bu yüzden aşağıdaki kodumuz “Cannot convert type int to Renk” şeklinde bir hata verecektir.


Renk r = 1;
Console.WriteLine(r);

0’a enumlar tarafından bu şekilde davranmalarının iki nedeni vardır:

  • Bir enum’un ilk üyesi sıklıkla enum’un default value’su olarak kullanılır.
  • Birleşik enum tipleri için 0’ın anlamı “Flag Yok”

Ayrıca bu çevirme işlemlerini boolean tipler için de kullanabiliriz:


int i = (int)Hiza.Sol;
Hiza h = (Hiza) i;
bool Solmu = (int) h == -1;
Console.WriteLine(Solmu);

Burada da öncelikle Hiza enumunda yer alan Sol üyesinin üye sıralamasındaki yeri alınır. (0 numaralı üye) Daha sonra bu sıralama değeri Hiza tipinde hangi değişkene denk geldiği belirlenir. (0. üye Sol adlı üye) Ardında da bu üyenin değerinin –1 olup olmadığı (int)h ile belirlenip bool değişkenine atanır.

Enum’ları başka bir enum tipine açıkça atayabiliriz. Örneğin; YatayHiza adında bir enum oluşturalım:


enum YatayHiza
 {
Sol = Hiza.Sol,
Sağ = Hiza.Sağ,
Orta
 }

Bu tanımlamadan sonra aşağıdaki şekilde tanımlamalar yapabiliriz:


YatayHiza y = (YatayHiza)Hiza.Sağ;
YatayHiza i = (YatayHiza)(int)Hiza.Sağ;

Burada y ve i değişkenlerini yazdırdığımızda ikisinde de “Sağ” sonucunu elde ederiz.

Flag Kullanımı

Enum üyelerini birleştirebiliriz. Karışıklığı önlemek için, enum üyelerini değerleri ile birleştiririz. Örneğin aşağıdaki enum tanımımızı yapalım:


YatayHiza y = (YatayHiza)Hiza.Sağ;
YatayHiza i = (YatayHiza)(int)Hiza.Sağ;

Enum üyelerini birleştirmek için | ve & gibi bir operatörlerini kullanırız. Bu yüzden bu üyelerin değerlerini 2’nin üsleri şeklinde tanımladık:


[Flags]
 enum Kenar
 {
Sol = 1,
Sağ = 2,
Üst = 4,
Alt = 8
 }

| operatörü ile SağSol enum değişkenini oluşturalım. SağSol ve Sol üyelerini & operatörü ile and’lersek ortak payda Sol olduğundan altındaki ifadeyi yazdırır. formatlı değişkeni “Sol, Sağ” olarak tanımlanır Kenar tipindeki enum’u string’e çevirdiğimizde. k değişkenini de farklı yolla SağSol değişkenine eşitledik. Son olarak ta k değişkenini Sağ değişkeni ile ^ (XOR) işlemine soktuğumuzda geriye sadece Sol kalır.

Kural ile, Flags niteliği bir enum’un üyeleri birleştiriliyorsa her zaman uygulanmalıdır o enum yapısına. Eğer Flags niteliği kullanmadan bir enum oluşturur ve yine bu üyeleri birleştirmek isterseniz sorun çıkmaz. Yani yukarıdaki enum tanımlamasında [Flags] kısmını silsek bile bu programımız sorunsuz çalışır. Fakat, bir enum örneği için ToString() metodunu çağırmak bir string yerine bize bir numara getirecektir.

Kural ile, birleştirilmiş enum tipi tekil değil de çoğul tipte isimlerden oluşmalıdır.

Ayrıca enum üyelerini enum içerisinde tanımlama aşamasında da birleştirebiliriz:


[Flags]
 enum Kenar
 {
 Sol = 1,
 Sağ = 2,
 Üst = 4,
 Alt = 8,
 SolSağ = Sol | Sağ,
 AltÜst = Alt | Üst,
 Tüm = SolSağ | AltÜst
 }

Enumlarla ilgili işlem yapabileceğimiz operatörler aşağıdaki şekildedir;

= == != < > <= >= + – ^ & | ˜
+= -= ++ – sizeof

Bit bir, aritmetik ve karşılaştırma operatörleri geriye underlying değerlerinin işlemler sonuçlarını geri döndürür.

Type-Safe Konusu

Aşağıdaki enum’ yapısını göz önünde bulunduralım;


enum Kenar
 {
Sol,
Sağ,
Üst,
Alt
 }

Bir enum kendi underlying type versine atanabildiği gibi dışarıdan gerçek bir değer de enum’a bound edilebilir. Örneğin;


Kenar k = (Kenar)12345;
Console.WriteLine(k);

kodu bize 12345 çıktısını verir. Fakat bit ve aritmetik operatörler benzer bir şekilde geçersiz bir değer üretirler. Bu yüzden;


Kenar k = Kenar.Sağ;

k++;

kodu herhangi bir hata oluşturmaz.

Bir enum değerinin geçerli olup olmadığı kontrol etmek için Enum.IsDefined statik metodunu kullanabiliriz. Örneğin;


Kenar kenar = (Kenar)1;
 Console.WriteLine(Enum.IsDefined(typeof(Kenar), kenar));
 Kenar kenar1 = (Kenar)12345;
 Console.WriteLine(Enum.IsDefined(typeof(Kenar), kenar1));
 Kenar kenar2 = Kenar.Sol;
 Console.WriteLine(Enum.IsDefined(typeof(Kenar), kenar2));

kodumuzun çıktısı sırayla “True, False, True” olur. İlk satırımızın neden True döndürdüğünü yine bu konu içerisinde anlatmıştık.

Ne yazık ki, Enum.IsDefined metodu flag kullanılmış enum yapılarında kullanılamaz. Fakat aşağıdaki gibi tanımladığımız FlagTanımlandımı adında bir metod ile bir enum yapısının flag kullanılıp kullanılmadığını kontrol edebiliriz:


[Flags]
 enum Kenar
 {
 Sol = 1,
 Sağ = 2,
 Üst = 4,
 Alt = 8
 }

class Test
 {
 static bool FlagTanımlandımı(Enum e)
 {
 decimal d;
 return !decimal.TryParse(e.ToString(), out d);
 }

static void Main()
 {
 for (int i = 0; i <= 16; i++)
 {
 Kenar kenar = (Kenar)i;
 Console.WriteLine(FlagTanımlandımı(kenar) + " " + kenar);
 }
 }
 }

C# Delegate Tipine Genel Bir Bakış

Bir delegate tipi, özel parametre listesi ve geri dönüş tipi ile birlikte bir metoda referans bildirir. Böylece bir delegate tipi oluşturursak bir metoda referans içeren bir nesne yaratmış oluyoruz. Bu metod ise bu referans ile çağırılabilir konuma gelmiş oluyor.

Aşağıdaki örneğimiz bir Fonksiyon adında bir delegate tanımlar ve onu kullanır;

using System;

namespace ConsoleApplication1
{
 delegate double Fonksiyon (double d);

class Carpma
 {
 double carpim;

public Carpma(double carpim)
 {
 this.carpim = carpim;
 }

public double Carp(double x)
 {
 return x * carpim;
 }
 }

class Test
 {
 static double Kare(double x)
 {
 return x * x;
 }

static double[] Uygula(double[] a, Fonksiyon f)
 {
 double[] sonuc = new double[a.Length];

for (int i = 0; i < a.Length; i++)
 sonuc[i] = f(a[i]);

return sonuc;
 }

static void Main()
 {
 double[] a = { 0.0, 0.5, 1.0 };

double[] kareler = Uygula(a, Kare);

double[] sinusler = Uygula(a, Math.Sin);

Carpma c = new Carpma(2.0);
 double[] ciftler = Uygula(a, c.Carp);
 }
 }

}

Fonksiyon delegate tipinin örneği double tipinde argüman alan ve geriye double türünde bir veri döndüren herhangi bir metoda referans olabilir. Uygula() metodu verilen Fonksiyon’u double[] ile uygular ve geriye bir double[] sonucunu döndürür. Main metodu içerisinde, Uygula 3 farklı fonksiyonu double[] ile uygular.

Bir delegate, bir statik metoda (Kare ve Math.Sin metodları gibi) ya da bir metod örneğine (c.Carp metodu gibi)referans olabilir.

Bir metod örneğine referans olan bir delegate, belirli bir nesne örneğine de referans olabilir. Ve bu metod örneği delegate ile çağırıldığında, bu nesne this ile birlikte yönelir.

Bir delegate örneği yaratmak için, bir metodu delegate değişkenine atayabiliriz:

using System;

namespace ConsoleApplication1
{
 delegate int Donusturme (int x);

class Test
 {
 static int Kare (int x)
 {
 return x * x;
 }

static void Main()
 {
 Donusturme d = Kare; //Delegate örneği oluşturma
 int sonuc = d(3); //Delegate'i çağır
 Console.WriteLine(sonuc); //9
 }
 }

}

Görüldüğü üzere, bir delegate örneği çağırmak, tıpkı bir metod çağırımına benzer. (d(3) gibi) Bu arada;

 Donusturme d = Kare; 

satırı aşağıdaki kodun kısaltılmış halidir;

Donusturme d = new Donusturme (Kare);</p>  <p>d(3)

kodumuz da

d.Invoke(3)

kodunun kısaltılmışıdır.

Delegate Uyumlu Metodlar Yazmak

Bir delegate değişkeni metodlara dimanik olarak atanır. Bu delegate uyumlu metod yazmanın anahtarıdır. Aşağıdaki örnekte, Donustur adından bir yardımcı metodumuz var ve bir dizinin elemanlarını karelerine dönüştürür:

using System;

namespace ConsoleApplication1
{
 delegate int Donusturme (int x);

class Yardımcı
 {
 public static void Donustur(int[] degerler, Donusturme d)
 {
 for (int i = 0; i < degerler.Length; i++)
 degerler[i] = d(degerler[i]);
 }
 }

 class Test
 {
 static int Kare (int x)
 {
 return x * x;
 }

static void Main()
 {
 int[] degerler = { 1, 2, 3 };
 Yardımcı.Donustur(degerler, Kare);

foreach (int i in degerler)
 Console.Write(i + " "); // 1 4 9
 }
 }
}

Her delegate örneği çoklu kullanım yeteneğine sahiptir. Bunun anlamı; bir delegate örneği sadece tek bir metoda referans değil, birden fazla metoda referans olabilir. + ve += operatörleri delegate örneklerini birleştirir. Örneğin:

 DelegateOrnek d = MetodOrnek;</p>  <p>d += FarklıMetodOrnek; 

Bu durumda d delegate’ini çağırmak bu iki metodu çağırmak anlamına gelir. Eklenilme sırasına göre delegate tipleri çağırılır.

– ve –= operatörleri de sağıdaki metodu solundaki delegate örneğinden silerler. Örneğin;

 d –= MetodOrnek; 

Bu durumda d delegate’ini çağırmak sadece FarklıMetodOrnek metodunu çağırmamız anlamına gelir.

NOT: Delegate tipleri immutable tiplerdir. Bunun anlamı += veya –= operatörlerini kullandığınızda yeni bir delegate örneği oluşturmuş olursunuz ve varolan değişkeni ona atarsınız.

Çoklu gönderilen bir delegate tipi eğer void olmayan bir geri dönüş tipine sahipse, çağıran bu delegate’in içerdiği son metodun geri dönüş değerini alır. Önde bulunan metodlar yine de çağırılır fakat bunların geri dönüş değerleri görmezden gelinir.

NOT: Tüm delegate tipleri dolaylı olarak System.MulticastDelegate sınıfından türetilir. O da System.Delegate sınıfından kalıtım alır. += ve –= operatörlerini kullandığımızda aslında System.Delegate sınıfının statik tanımlanmış Combine ve Remove metodlarını kullanmış oluruz.

Statik Metod vs Metod Örneği

Bir delegate nesnesi bir metod örneğine atandığında, sadece atandığı metod olan referansı değil, ayrıca ait olduğu metodu da korur. System.Delegate sınıfının Target özelliği bu örneği gösterir. Örneğin;

using System;

namespace ConsoleApplication1
{
 public delegate void İlerleme (int TamamlanmaYuzdesi);

class Test
 {
 static void Main()
 {
 Ornek o = new Ornek();
 İlerleme i = o.İlerlemeOrnek;
 i(90); // 90
 Console.WriteLine(i.Target == o); // True
 Console.WriteLine(i.Method); // Void İlerlemeOrnek(Int32)
 }
 }

class Ornek
 {
 public void İlerlemeOrnek(int TamamlanmaYuzdesi)
 {
 Console.WriteLine(TamamlanmaYuzdesi);
 }
 }
}

Generic Delegate Tipleri

Bir delegate tipi generic tipinde parametreler içerebilir. Örneğin;

public delegate T Donusturme<T> (T arg);

Bu tanımlama ile, Donustur yardımcı metodunu herhangi bir tip ile kullanabiliriz:

 public class Yardımcı
 {
 public static void Donustur<T> (T[] degerler, Donusturme<T> t)
 {
 for (int i = 0; i < degerler.Length; i++)
 degerler[i] = t(degerler[i]);
 }
 } 

Func ve Action Delegate’leri

Generic delegate’ler ile, herhangi bir geri dönüş tipi ve herhangi sayıda argümanı olan metodlar için küçük delegate tipleri oluşturmak mümkündür. Bu delegate tiplerinden olan Func ve Action delegate’leri System namespace’i içerisinde yer alır.

 delegate Sonuc func<out Sonuc>();
 delegate Sonuc func<in T, out Sonuc>(T arg);

delegate void Action ();
 delegate void Action<in T>(T arg); 

Bir önceki örneğimizde Donustur delegate örneğini, sadece basit bir T argümanı alan ve aynı cinsten bir değer geri döndüren Func delegate’i ile yer değiştirebiliriz:

 public static void Donustur<T> (T[] degerler, Func <T, T> donus)
{
 for (int i = 0; i < degerler.Length; i++)
 degerler[i] = donus (degerler[i]);
} 

Delegate  Uyumluluğu

Tip

Tüm delegate tipleri, imzaları aynı olsa bile birbiri ile uyuşmayan tiplerdir.

 delegate void D1();
 delegate void D2();

D1 d1 = Metod1;
 D2 d2 = d1; // Derleme zamanı hatası 

Aşağıdaki örnek ise herhangi bir hata vermez:

 D2 d2 = new D2 (d1); 

Delegate örnekleri eğer aynı metodlara hedef gösterilirse bunlar eşit olarak nitelendirilir. Örneğin:

 delegate void D();
 ...
 D d1 = Metod1;
 D d2 = Metod1;
 Console.WriteLine (d1 == d2); // True döndürür. 

Birden fazla metoda referans gösteren delegate tipleri, eğer aynı metodlar aynı sırayla ise bu tipler de eşit olarak nitelendirir.

Parametre

Bir delegate tipi, hedef gösterdiği metodtan daha belirli parametre tiplerine sahip olabilir. Bu duruma contravariance adı verilir. Örneğin;

 using System;

namespace ConsoleApplication1
{
 delegate void StringEtki (string s);

class Test
 {
 static void Main()
 {
 StringEtki se = new StringEtki(NesneEtki);
 se("Merhaba");
 }
 static void NesneEtki(object o)
 {
 Console.WriteLine(o);
 }
 }
} 

Bu örneğimizde, StringEtki delegate tipi string tipindeki bir argüman ile çağırılır. Ve bu argüman hedef metoda aktarıldığında, bu argüman dolaylı olarak object tipine çevrilir (üst tipi olduğundan).

Geri Dönüş

Delegate’in geri dönüş tipi hedeflediği metodun geri dönüş tipinden daha az belirli olabilir. Bu duruma da covariance adı verilir. Örneğin;

 using System;

namespace ConsoleApplication1
{
 delegate object NesneGetir ();

class Test
 {
 static string StringGetir()
 {
 return "Merhaba";
 }
 static void Main()
 {
 NesneGetir o = new NesneGetir(StringGetir);
 object sonuc = o();
 Console.WriteLine(sonuc); //Merhaba
 }
 }
} 

NesneGetir geriye bir object döndürmesi bekler, fakat object alt sınıfı bunu ayrıca yapar. Delegate’in geri dönüş tipi “covariant”’tir.

C#’ta Dizi Yapısı

C#’ta bir dizi, doğrudan ulaşılabilen sayısı belli üyeleri içeren bir veri yapısıdır. Dizinin içindeki elemanlar, dizi elemanları, hepsi aynı tipte ve bu tip dizi elemanlarının tipi olarak adlandırılır.

Dizi tipleri birer referans tipidir ve dizi elemanlarının tanımlanması bu dizi örneğine referans içerir. Gerçek dizi örneği dinamik olarak runtime zamanında new operatörü kullanılarak elde edilir. New operatörü kullanılarak dinamik olarak dizilere bellek ayrılır. New operatörü otomatik olarak dizi elamanlarının default değerlerini ilk kullanıma hazırlar. Örneğin; tüm sayısal tipler için 0, tüm referans tipler için ise null.

Örneğin aşağıdaki örnek, bir int dizisi yaratıp, ilk kullanıma hazırlayıp, sonra da çıktılarını gösteren bir örnektir;

using System;

class Test 
{ 
    static void Main() 
    { 
        int[] dizi = new int[10]; 
        for (int i = 0; i < dizi.Length; i++ ) 
        { 
            dizi[i] = i * i; 
        } 
        for (int j = 0; j < dizi.Length; j++ ) 
        { 
            Console.WriteLine("dizi[{0}] = {1}", j, dizi[j]); 
        } 
    } 
}

Buradaki örnekte tek bir boyutlu dizi ile çalıştık. C# birden fazla boyutlu dizi kullanımına izin vermektedir. Bir dizinin boyut numarası aynı zamanda o dizi tipinin rank’ı olarak ta bilinir. Bir dizinin boyut sayısını dizi tanımında kullanılan dizinin içindeki virgül sayısını bir arttırarak bulabiliriz. Aşağıdaki örnekler bir, iki ve üç boyutlu dizilere örneklerdir;

int[] dizi1 = new int[10]; 
int[,] dizi2 = new int[10, 5]; 
int[, ,] dizi3 = new int[10, 5, 2];

dizi1 dizisi 10 elemana, dizi2 dizisi 50 (10 * 5) elemana ve dizi3 dizisi de 100 (10 * 5 * 2) elemana sahiptirler.

Bir dizinin eleman tipi herhangi bir tip olabilir hatta dizi tipinde bile olabilir. Dizi tipindeki elemanlara sahip dizilere Jagged array adı verilir çünkü bu dizinin elemanlarının uzunlukları aynı olmak zorunda değildir. Aşağıdaki örnek int dizi tipi içeren elementlere sahip olan bir diziyi ifade eder.

int[ ][ ] dizi = new int[3][ ]; 
dizi[0] = new int[10]; 
dizi[1] = new int[7]; 
dizi[2] = new int[20];

array-1

Yukarıdaki ilk satır, 3 elemanlı bir dizi oluşturur, bu elemanların hepsi int[] tipinde ve hepsinin varsayılan değeri null’dır. Altındaki satırlar, bu int[] tipindeki dizi elemanlarının uzunluklarını belirlerler.

Unutmayın ki new operatörü { } sınır belirleyiciler içinde dizinin elemanlarına ilk oluşturulma anında değer atamaya imkan verir.

int[ ] dizi = new int[ ] { 1, 2, 3 };

Hatta bu kullanımı aşağıdaki şekilde kısaltabiliriz de.

int[] dizi = { 1, 2, 3 };

Bu iki kullanım da aşağıdaki gösterilen adımlara eşittir:

int[ ] t = new int[3]; 
t[0] = 1; 
t[1] = 2; 
t[2] = 3; 
int[ ] dizi = t;

Dizilerde referans atama konusunda tıpkı nesneler gibi, bir diziyi diğer diziye atadığınızda sadece değişkenin ilişkili olduğu nesneyi değiştiriyorsunuz. Örneğin aşağıdaki programda, dizi tanımlamaları yapılan dizi1 ve dizi2 dizileri, atama yapıldıktan sonra her iki dizi referans değişkenleri de aynı nesneyi gösterdiğinden dizi değerleri aynı olur.

using System;

class Test 
{ 
    static void Main() 
    { 
        int[] dizi1 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 
        int[] dizi2 = { 0, -1, -2, -3, -4, -5, -6, -7, -8, -9 };

        dizi2 = dizi1;

        foreach(var i in dizi1) 
        { 
            Console.Write(dizi1[i]); 
        }

        Console.WriteLine();

        foreach (var i in dizi2) 
        { 
            Console.Write(dizi2[i]); 
        } 
    } 
}

Çıktı:

0123456789

0123456789 Press any key to continue . . .

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.

C#’ta Method Aşırı Yüklemek

C#’ta method aşırı yüklenmesi, method imzaları farklı olan aynı isimdeki methodları aynı sınıf içerisinde kullanıma izin verir. Aşırı yükleme mekanizması, parametrelere uygun eşleşen methodu bulur ya da herhangi bir basit eşleşme olmazsa bir hata üretir. Aşağıdaki örnek, arışı yükleme mekanizmasının nasıl çalıştığına bir örnektir. Main() methodunun içinde hangi methodun çağrılacağını yorum olarak yazdım.

using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
class Test
{
static void A()
{
Console.WriteLine("A()");
}
static void A(object x)
{
Console.WriteLine("A(object x)");
}
static void A(int x)
{
Console.WriteLine("A(int x)");
}
static void A(double x)
{
Console.WriteLine("A(double x)");
}
static void A<T>(T x)
{
Console.WriteLine("A<T>(T x)");
}
static void A(double x, double y)
{
Console.WriteLine("A(double x, double y)");
}
static void Main()
{
A();          // Çağırılan F()
A(1);         // Çağırılan F(int)
A(1.0);       // Çağırılan F(double)
A("abc");     // Çağırılan F(object)
A((double)1); // Çağırılan F(double)
A((object)1); // Çağırılan F(object)
A<int>(1);    // Çağırılan F<T>(T)
A(1, 1);      // Çağırılan F(double, double)
}
}
}

Önemli: Method aşırı yükleme yanlış kullanımlara neden olabilir. Genel olarak bakarsak, method yüklemeyi sadece methodların hepsi anlamsal olarak aynı işi yaptığı zamanda kullanmalıyız. Bir çok yazılımcı, bu olayı “tek bir method farklı argümanlara sahip” olabilir şeklinde düşünür. Aslında, yerel değişkenin, parametrenin veya özelliğin tipini değiştirme, farklı yüklemelerin çağırılmasına neden olabilir. Yazılımcılar method aşırı yüklemesinin yan etkisini göremeyebilirler. Fakat kullanıcılar için bu tamamen farklı sonuçlar doğurabilir.

Brad Abrams, .NET framework’ü geliştirme aşamasının başlarında, String adında bir sınıfın içinde şu method yüklemelerin problem yarattığını söylüyor:

public class String
{
public int IndexOf (string value);
// Örnekteki değerin index'ini geri döndürür.
public int IndexOf (char value);
// Örnekteki değerin index'ini geri döndürür.
public int IndexOf (char[] value);
// Değerin içindeki karakterlerin ilk index'ini geri döndürür.
} 

Buradaki son method, farklı sonuçlara yol açtığını söylüyor. Şöyle ki;

 "Joshua, Hannah, Joseph".IndexOf("Hannah"); // Geriye 7 döndürür. 

Fakat

"Joshua, Hannah, Joseph".IndexOf(new char [] {'H','a','n','n','a,'h;");
 // Geriye 3 döndürür.

Buradaki son methodun isminin method aşırı yüklemesine bağlı kalınarak aşağıdaki şekilde olması gerektiğini söyler:

public int IndexOfAny (char [] value);

C#’ta Virtual, Override ve Abstract Methodlar

C#’ta bir method örneği, virtual anahtar sözcüğü ile tanımlanırsa, o methoda virtual (sanal) method adı verilir.

Bir sanal method, ana sınıf tipinden ziyade, güncel atanmış tip ile ilişkilendirilmiş methodu kullanmanıza izin verir.

Bir sanal method türetilmiş sınıfı tarafından etkisiz hale getirilebilir. Bir method örneğinde override anahtar sözcüğü varsa, o method, kalıtım alınmış sanal methodu aynı method imzası ile geçersiz kılar. Bu tür bir sanal method eğer overload olmuşsa, tanımlanmış değişkenin data tipinden ziyade verinin gerçek sınıf tipi runtime zamanında kullanılır. Bunun anlamı; temel sınıf bir çok türetilmiş sınıf tarafından kullanılabilir.

Önemli: Buradaki ince bir nokta, override edilmiş bir sanal method, hala o sınıfın bir methodu olarak hesab katılır. Türetilmiş sınıf, sanal sınıf override edildiği zaman onu göstermek zorundadır. Bu override anahtar sözcüğünü yeni method oluşumunda kullanılarak gösterilir.

Bir abstract (soyut) method, uygulanmayan bir sanal methodtur. Bu methodlar, abstract anahtar sözcüğü ile tanımlanır ve sadece astract olarak tanımlanmış sınıflara izin verilir. Bir soyut sınıf, soyut olmayan türetilmiş tüm sınıflar tarafından override edilmelidir.

Aşağıdaki örnekte; soyut sınıf olarak tanımlanmış Deyim sınıfı, ki bir deyim ifade ağacı düğümü temsil eder, 3 tane de türetilmiş sınıf, Sabit, DegiskenReferansi ve İslem, ki bunlar da sabitlerin, değişken referanslarının ve aritmetik işlemlerin deyim ifade ağaçlarını uygularlar.


using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
public abstract class Deyim
{
public abstract double Hesapla(Hashtable vars);
}
public class Sabit : Deyim
{
double value;
public Sabit(double value)
{
this.value = value;
}
public override double Hesapla(Hashtable vars)
{
return value;
}
}
public class DegiskenReferansi : Deyim
{
string name;
public DegiskenReferansi(string name)
{
this.name = name;
}
public override double Hesapla(Hashtable vars)
{
object value = vars[name];
if (value == null)
{
throw new Exception("Bilinmeyen deşiken " + name);
}
return Convert.ToDouble(value);
}
}
public class İslem : Deyim
{
Deyim sol;
char op;
Deyim sag;

public İslem(Deyim sol, char op, Deyim sag)
{
this.sol = sol;
this.op = op;
this.sag = sag;
}

public override double Hesapla(Hashtable vars)
{
double x = sol.Hesapla(vars);
double y = sag.Hesapla(vars);
switch (op)
{
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;
}
throw new Exception("Bilinmeyen operatör");
}
}
} 

Bu 4 sınıf, modern matematik işlemler için kullanılabilir. Örneğin, bu sınıf örneklerini kullanarak x + 7 deyimi şu şekilde gösterilebilir;


static void Main()
{
Deyim d = new İslem(new DegiskenReferansi("x"), '+', new Sabit(7));
} 

Burada Deyim sınıfının Hesapla methodu verilen değeri hesaplamak ve bir dobule değeri üretmek görevindedir. Bu method Hashtable adında değişken isimlerini ve değerlerini içeren bir parametre alır. Hesapla methodu, sanal soyut bir method olduğundan, bunun anlamı; soyut olmayan türetilmiş sınıflar güncel bir uygulama sağlamak için bu methodu override etmek zorundalar.

Sabit sınıfının Hesapla implementasyonu, basit olarak geriye depolanmış bir sabit (constant) döndürür. DegiskenReferansi implementasyonu, Hashtable içerisindeki değişken ismine bakar ve sonuçlanan değeri geri döndürür. İslem implementasyonun görevi, öncelikle sol ve sağ operandlarını (Recursif olarak çağırılan Hesapla() methodları ile) hesaplayarak, verilen aritmetik işlemi gerçekleştirmektir.

Aşağıdaki program Deyim sınıfını kullanarak x * (y + 3) işlemini farklı değerler kullanarak hesaplar;


static void Main()
{
Deyim d = new İslem(new DegiskenReferansi("x"), '*', new İslem(new DegiskenReferansi("y"), '+', new Sabit(3)));

Hashtable vars = new Hashtable();

vars["x"] = 2;
vars["y"] = 5;
Console.WriteLine(d.Hesapla(vars)); // Çıktı 16 olur.

vars["x"] = 3;
vars["y"] = 7;
Console.WriteLine(d.Hesapla(vars)); // Çıktı 30 olur.
} 

C#’ta Method Parametreleri

Parametreler, bir değere atanan ya da bir methoda referans eden değişkenler için kullanılır. Method parametreleri, method çağırıldığında, asıl değerlerini argümanlardan alırlar. 4 çeşit parametre tipi bulunur; değer parametreleri (value parameters), referans parametreleri (reference parameters), çıktı parametreleri (output parameters) ve parametre dizileri (parameters arrays).

Değer parametreleri (value parameters), girdilerde parametre geçirilmelerinde kullanılır. Bir değer parametresi, methodlara geçirilen argümanların ilk değerlerini içeren yerel değişkenler ile uyuşur.

Değer parametrelerindeki değişiklikler, parametreye geçirilen argümanları etkilemezler.

Önemli: Değer parametrelerindeki değişiklik durumları, argümanları etkilemezler kelimeleri yanıltıcı olabilir çünkü türetilmiş sınıflar referans tip’te parametre içeriğini değiştirebilir. Parametre değerleri değiştiremez fakat referans edilmiş içerik (nesne) değiştirebilir.

Değer parametreleri seçimli olabilir, varsayılan değeri belirlemek için, böylece argümanlara ilişkilendirme yapılabilir.

Referans parametreleri, hem girdi (input) hem de çıktı (output) parametre geçişleri için kullanılır. Referans parametrelerini geçiş argümanları; bir değişken, yürütme süresinceki methodlar, ayrıca referans parametreleri argüman değişkenleri ile  aynı bellek bölgesini simgelerler. Bir referans parametresi ref anahtar sözcüğü ile gösterilir. Aşağıdaki örnek; ref anahtar sözcüğünün kullanımını gösterir.

class Test
{
static void Degistir(ref int x, ref int y)
{
int temp = x;
x =  y;
y = temp;
}
static void Main()
{
int i = 10, j = 20;
Degistir(ref i, ref j);
Console.WriteLine("{0} {1}", i, j); // Çıktı 20 10 olur.
}
}

Bu örneği ref anahtar sözcüksüz yazsaydık, çıktı “10 20” olurdu. Yani o değişkenlere referans vermeden bunların yerleri değiştirilemezdi.

Önemli: Referans parametreleri C# tarafından “referans ile geçiş” olarak adlandırılır; önce bir nesne örneğini bir methoda geçirir ve o method o nesne örneğine bir referans alır.

Referans parametreleri, çok küçük bir farkla “referans ile geçiş”’ten farklıdır. Bu durumda, referans bir değişkenin kendisindedir, bir nesne örneğinde değil. Eğer bu değişken, bir değer tipi içerirse bu tamamen normal bir durumdur. Bu değere “referans ile geçiş” yapılmamıştır değişken bu değeri tuttuğu sürece.

Referans parametrelerini düşünmenin iyi bir yolu, referans parametreleri, değişkenleri argümanlara geçiştirmek için bir takma ad’a dönüşürler şeklinde düşünmektir. Yukardaki örnekte, x ve i aslında aynı değişkendirler. Bellekte aynı bölgeye kaynak gösterirler.

Çıktı parametreleri (Output parameters), çıktı parametre geçişleri için kullanılırlar. Çıktı parametreleri referans parametrelerine benzerdir, fark olarak burada değeri ilk çağıran-sağlayan argüman önemsizdir. Çıktı parametreleri, out anahtar sözcüğü ile gösterilirler. Aşağıdaki örneği inceleyebiliriz;

using System;

namespace HelloWorld
{
class Test
{
static void Bolme(int x, int y, out int sonuc, out int kalan)
{
sonuc = x / y;
kalan = x % y;
}
static void Main()
{
int son, kal;
Bolme(20, 3, out son, out kal);
Console.WriteLine("{0} {1}", son, kal); // Çıktı 6 2 olur.
}
}
}

Önemli: CLR, direkt olarak sadece ref parametrelerini destekler. Out parametresi metadata içerisinde temsil edilir.

Parametre dizileri, bir methoda birden fazla sayıda argüman değişkeninin geçirilmesine izin verirler. Parametre dizileri, params anahtar sözcüğü ile gösterilirler. Bir methodun sadece son parametresi bir parametre dizisi olabilir ve parametre dizisinin tipi tek boyutlu dizi tipi olmak zorundadır. System.Console sınıfının Write ve Writeline methodları parametre dizilerinin kullanımına iyi bir örnektir. Bu iki method şu şekilde tanımlanmıştır:

 public class Console
{
public static void Write(string fmt, params object[] args) {...}
public static void WriteLine(string fmt, params object[] args) {...}
} 

Bu tür method tanımlamalarına, Visual Studio içerisinde methodun üzerine gelip sağ tıkladığımızda “Go to definition” sekmesiyle ulaşabiliriz. (Kısayol F12)

 

 

 

 

 

 

 

Parametre dizileri içeren bir method içerisinde, parametre dizileri bir dizi tipinin parametreleri ile tam olarak aynı şekilde davranır. Fakat, bir parametre dizisine sahip method, hem parametre dizisi tipindeki basit argümanı hem de parametre dizisindeki element tiplerinin argüman numaralarını geçirebilir. Sonrasında, bir dizi örneği otomatik yaratılır ve verilen argümanları ilişkilendirir. Örneğin;

 Console.WriteLine("x={0} y={1} z={2}", x, y, z); 

aşağıdaki koda eşdeğerdir

 string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);

Önemli: Params anahtar sözcüğü kullanımının güzel bir tarafı da, opsiyonel bir kısayol oluşturmasıdır. Yani aşağıdaki şekilde bir kod yazmamızı önler:

 static object[] GetArgs() { ... }

static void Main()

{
object[ ] args = GetArgs();
object a = args[0];
object b = args[1];
object c = args[2];
Console.WriteLine("a={0} b={1} c={2}", a, b, c );
} 

Şimdi de bu methodu çağırıyorum ve compiler benim yerine bu parametreler için bir dizi oluşturuyor.

 static object[] GetArgs() { ... }

static void Main()

{
Console.WriteLine("a={0} b={1} c={2}", GetArgs() );
} 

Fakat bu günlerde, .NET içerisinde sadece çok az method bir dizi geriye döndürüyor. Bu nedenle bir çok yazılımcı, IEnumerable<T> kullanmayı esneklik amacıyla tercih ediyorlar. Bu nedenle ileride şöyle bir kod yazabilirsiniz;

 static IEnumerable<object> GetArgs { }

static void Main()
{
Console.WriteLine("x={0} y={1} z={2}", GetArgs().ToArray());
}