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.

.NET içerisinde Değer Tipi ve Referans Tipi

.NET kodunun çalıştırılmasını sağlayan CLR yapısı iki çeşit veri tipini destekler; referans tipi (reference types) ve değer tipi (value types). FCL (Framework Class Library) içerisinde bulunan tiplerin çoğu referans tipi olsa da, programcıların kullandığı tiplerin çoğunu değer tipleri oluşturur. Referans tiplerine her zaman belleğin “Heap” kısmından yer ayrılır ve C#’ın new operatörü de bu nesnenin bellek adresini döndürür (Bellek adresi de nesnenin bitlerini kaynak gösterir). Şunu aklınızda tutmalısınız ki, referans tipleri ile çalışırken bazı performans özelliklerine dikkat etmelisiniz. Şu 4 maddeyi göz önünde bulundurun:

1. Yönetilen heap kısmından bellek ayrılmak zorundadır.

2.Heap bölgesine ayrılmış her nesnenin kendisi ile ilişkili ilave üyeleri mevcuttur.

3.Nesnenin içineki diğer bitler her zaman sınıfa ayarlanır. (Alanlar için)

4.Bir nesneye heap bölgesinden kaynak ayırabilmek bazen Garbage Collection’ı ortaya çıkartabilir.

Her tip referans tipi olsaydı, bir uygulamanın performansı fazlasıyla kayba uğrardı. Düşünün ki, her Int32 tipini kullandığınızda bellekten bir yer tahsis etmek zorundasınız. Ne kadar kötü bir performans alacağınızı farketmişsinizdir. Performans arttırmak için, sıkça tip kullanılan uygulamalarda, CLR “hafif siklet” olarak adlandırdığı değer tiplerini önerir. Değer tipi örnekleri için çoğunlukla belleğin stack bölgesinden yer tahsis edilir (Ayrıca bir referans tipi nesnenin içerisine bir alan olarak gömülebilirler). Bu tipin bir değişkeni, örneğine bir pointer içermez, bu değişken örneğinin kendisinin bir alanını içerir. Değer tipi örnekleri, garbage collector’ın kontrolü altına girmezler ve bu sayede yönetilen heap kısmının üzerindeki yükü azaltır.

.NET Framework SDK dökümanı hangi tiplerin değer tipi, hangi tiplerin referans tipi olduğunu açık bir şekilde gösterir. Bu dökümana baktığımızda sınıf tipleri referans tipleridir. Örneğin; System.Exception, System.IO.FileStream ve System.Random birer referans tipleridir. Diğer taraftan döküman structure (yapı) ve enum tiplerinin değer tipi olduğunu gösterir. Örneğin; System.Int32 yapısı, System.DayOfWeek enum tipi, System.Decimal yapıları birer değer tipidir.

Biraz daha derinlere inmeye çalışırsak, tüm structure yapıların System.ValueType abstract tipinden türediğini görürüz. System.ValueType tipinin de System.Object tipinden türetildiğini görürürz. Tüm enum tipleri System.Enum abstract tipinden türetilmişlerdir ki bu tip te yine System.ValueType tipinden türetilir. CLR ve diğer tüm programlama dilleri enum tiplerine ayrı bir davranış sergilerler. Burada kafanıza şu soru takılabilir. Görüldüğü üzere System.ValueType bir sınıf tipidir ve sınıf tipleri referans tipidir. O halde Nasıl bir değer tipi bir referans tipinden türeyebilir?

Kendi değer tipinizi tanımlamaya çalışırken değer tiplerini temel tip olarak kullanamazsınız. Çünkü tüm değer tipleri sealed (yani o sınıf için türetilme işlemini önler) olarak tanımlıdır. Bu nedenle yeni oluşturmak istediğini referans tipinde ya da değer tipinde bir tip için, temel tip olarak değer tiplerini kullanamazsınız. Bir örnekle açıklamaya çalışırsam; Boolean, Char, Int32, Uint64, Single, Double tiplerini temel tip şeklinde kullanıp yeni bir tip oluşturmak imkansızdır.

Aşağıdaki örnek size referans tipi ve değer tiplerinin farklılığını açıklar:

using System;
namespace Program
{
 //Referans Tipi [Sınıf olduğundan]
 class ReferansTipi
 {
 public int x;
 }

//Değer Tipi [Struct olduğundan]
 struct DegerTipi
 {
 public int x;
 }

public class Program
 {
 public static void Main(string[] args)
 {
 ReferansTipi r1 = new ReferansTipi(); // Yer Tahsisi - Heap
 DegerTipi d1 = new DegerTipi(); // Yer Tahsisi - Stack

r1.x = 5; // Pointer referansı
 d1.x = 5; // Stack içerisinde değişim

Console.WriteLine(r1.x); // 5
 Console.WriteLine(d1.x); // 5
 }
 }
}

Bu kod parçasını çalıştırdığımızda bilgisayarımızın bellek kısmında aşağıdaki resimde görülen bir durum oluşur:

stack-heap

Harika(!) bir çizer olduğumu da bu resimden anlamışsınızdır. Şimdi aşağıdaki kodları yukarıda belirttiğimiz kodun içerisindeki Main() metodunun sonuna ekleyelim ve bellek kısmında nasıl bir değişiklik olduğuna bakalım:

ReferansTipi r2 = r1; // Sadece referansı (pointer) kopyalama

DegerTipi d2 = d1; // Yer Tahsisi - Stack ve üyeyi kopyalama

r1.x = 6; // r1.x ve r2.x değerlerini değiştirir.
d1.x = 7; // d1.x'i değiştirir, d2.x'i değiştirmez.

Console.WriteLine(r1.x); //6
Console.WriteLine(r2.x); //6
Console.WriteLine(d1.x); //7
Console.WriteLine(d2.x); //5

stack-heap1

Bu kod içerisinde bazı satırlara değinmek istiyorum:

DegerTipi d1 = new DegerTipi(); // Yer Tahsisi – Stack

Bu koda direkt olarak baktığımızda belleğin heap bölgesinden yer tahsis edildiği bilgisine varılır. Fakat, C# DegerTipi örneğinin bir değer tipi olduğunu bilir ve bununiçin stack bölgesinden yer ayıracak kodu üretir. C# ayrıca bu değer tipinin içerisindeki tüm alanların sıfıra set edildiğine emin olur. Aynı satır kod şu şekilde de yazılabilir;

DegerTipi d1;

Bu kod aynı şekilde thread kısmındaki stack bölgesinden yer ayırır ve tüm alanları sıfıraatar. Bu iki kod arasındaki tek fark, eğer new operatörünü kullanırsak C# düşünür ki bu örnek ilk kullanıma hazırdır. Hemen bu konuyu daha da açık hale getirelim;

DegerTipi d3 = new DegerTipi(); // Bu iki satır derlenir çünkü
int a = d3.x; // C#, d3'ün alanlarını sıfıra atandığını bilir.

DegerTipi d3; // Bu iki satır derlenimez çünkü C#, d3'ün alanlarını sıfıra
int a = d3.x; // atanmadığı düşünür. Use of possibly unassigned field 'd3' hatası alırız.

Eğer .NET içerisinde kendi değişken tipinizi tasarlamak isterseniz, bu tipin referans tipi mi yoksa değer tipi mi olacağına dikkatle karar vermelisiniz. Bazı durumlarda değer tipleri daha iyi performans verebilir. Eğer aşağıdaki 3 özellikte tasarlamak istediğiniz tip için uygunsa bu tipi değer tipi olarak tasarlamalısınız;

  1. Tip ilkel bir tip olursa (primitive types). Burada bahsetmek istediğim bu tipin içerisindeki alanlarda herhangi bir düzenlemesi gereken üyesi yoksa mantığıdır.
  2. Bu tipin diğer tiplerden kalıtım almasına gerek yoksa.
  3. Bu tipten türetilecek başka tipler yoksa.
    Yeni değişken tipinizin örnek boyutu da burada hesaba katılmalıdır. Çünkü varsayılan olarak, argümanlar metodlara değer ile geçirilirler (pass by value). Böylece değer tipi örneğinin içindeki alan kopyalanır ki bu da performansı azaltır. Tekrardan, geriye bir değer tipi döndüren metodlar, metodu çağıran tarafından bellekte örneğin içindeki alananın kopyası için yer tahsisi yaparlar ki bu da performansı azaltır.

Değer tiplerinin temel avantajı, bir nesne olarak belleğin yönetimli heap kısmında yer kaplamazlar. Tabi ki değer tiplerinin, referans tiplerine göre birkaç sınırlaması mevcuttur. Şimdi bu durumlara bir göz atalım;

1. Değer tipi nesnelerinin iki farklı gösterimi vardır. Boxed ve unboxed. Referans tipleri her zaman boxed formdadırlar.

2. Değer tipleri System.ValueType sınıfından türetilmişlerdir. Bu tip System.Object’te tanımlanan aynı metodları sağlar. Fakat, System.ValueType Equals() metodunu override eder. Böylece iki nesnedeki alanların içindeki değerler eşleşirse geriye true döndürür. Ek olarak, System.ValueType GetHashCode() metodunu da override eder ki bu method bir değer tipi için algoritma kullanarak bir hash kodu üretir.

3.Değer tiplerini temel sınıf (base class) olarak, herhangi bir değer tipi veya referans tipi üretemeyeceğimiz için, bir değer tipine yeni bir sanal (virtual) metod tanımlamamalıyız. Hiçbir metod soyut (abstract) olmamalıdır.

4. Referans tipi değişkenler belleğin heap kısmındaki nesnenin bellek adresini içerirler. Default olarak, referans tipinde bir değişken oluşturulduğunda, null’a atanır ki bunun anlamı bu değişken henüz geçerli bir nesneyi göstermiyordur. Null olarak atanmış bir referans nesnesini kullanmaya çalıştığımızda NullReferanceException hatası alırız. Buna karşı, değer tipi değişkenleri her zaman tanımlandığı tipin değerini içerir ve bu değer tipinin tüm üyeleri 0’a atanır. Değer tipi değişkeni bir referans içermediği için, bir değer tipine ulaşırken NullReferanceException hatası almamız imkansızdır.

5.Bir değer tipi değişkenini başka bir değer tipi değişkenine atadığımızda, alandan alana kopyalama gerçekleşir. Bir referans tipi değişkenini başka bir referans tipi değişkenine atadığımızda sadece bellek adresi kopyalanır.

6.Bir önceki maddeden dolayı, referans tipleri heap bölgesinde aynı nesneye referans olabileceklerinden, bir referans tipindeki değişkenin üzerindeki değişiklik diğer bir referans tipindeki değişkeni değiştirebilir. Diğer taraftan, bir değer tipindeki değişkenin diğer değer tipindeki değişkeni etkilemesi imkansızdır.

Değer tiplerinde Equals() metodunun varsayılan uygulaması olarak bit bit karşılaştırma yapması bazı durumlarda performansı yükseltebilir. Eğer iki adet değer tipi değişkeninizin içerisi pozitif sıfır ve negatif sıfır ise, sırasıyla eşit değil olarak karşılaştırılırlar. Ama Equals() metodunu varsayılan davranışı için override edebilirsiniz. Örneğin;

using System;

namespace Program
{
    public class Program
    {
        struct S
        {
            public double X;
        }

        public static void Main(string[] args)
        {
            var i = new S { X = 0.0 };
            var j = new S { X = -0.0 };

            Console.WriteLine(i.X.Equals(j.X)); // True
            Console.WriteLine(i.Equals(j)); // False
        }
    }
}

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());
} 

C#’ta Methodlar

C#’ta methodlar, bir sınıf ya da nesne tarafından gerçekleştirilen hesaplamaları ya da eylemleri uygulayan üyelerdir. Static methodlara sınıflar aracılığıyla erişilebilirken, Instance (örnek) methodlara bir sınıfın örneği tarafından erişilebilir.

Methodlar bir parametre listesine (boş olabilen) sahip olabilir ki bunlar methoda geçirilen değer ya da değişkenlere referans gösterirler. Bununla birlikte bir geri dönüş tipine sahip olabilirler (return type) ki bu da method içinde hesaplanan değerlerin programa geri vereceği değeri belirler. Eğer bir methodun geri döüş tipi “void” ise, o method bir değer döndürmez.

Tıpkı tipler gibi, methodlar da, bir takım tip parametre setlerine sahip olabilirler. Bunun amacı method çağırıldığında, hangi tiplerin belirlenmesi gerektiğidir.

Bir method imzası (signature), sınıf içerisinde eşsiz bir değer olmalıdır. Bir method imzası; var olan methodun ismi, tip parametrelerin sayısı, erişim düzenleyicileri (modifier) ve parametrelerin tiplerinden oluşur. Method imzası geri dönüş tipi içermez.

Önemli: Generic tiplerin şanssız bir sonucu olarak, yapısal bir tip (constructed type), özdeş imzalar içeren iki farklı methoda sahip olabilir. Örneğin;


class Sınıf<T>
{
void Method(T t){}
void Method(int t){}
}

şeklide bir kod hiçir hata içermez. Fakat Sınıf<int>’in özdeş imzalara sahip iki farklı methodu olmuş olur. Daha sonra göreceğimiz gibi, bu durum method’ların aşırı yüklenmesinde ve arayüz implementasyonlarında bazı ilginç senaryolara yol açar. İyi bir yönlendirme olarak; bunu yapmayın.

C#’ta Alanlar (Fields)

C#’ta alanlar, bir sınıf ya da bir sınıf örneği ile ilişkilendirilmiş değişkenlerdir. Static değiştiricisi ile tanımlanmış alanlar static field olarak tanımlanır. Static field’lar, tam olarak bir bellek yeri tanımlarlar. Kaç tane sınıf örneği oluşturulursa oluşturulsun, static field’ın sadece bir tane kopyası vardır.

Önemli: Static field’lar her generic type için ayrı bir yapısal yapıdır. Örneğin elinizde;

class Stack<T>
{
public readonly static Stack<T> bos = tralalalala;
}

varsa, Stack<int>.bos ve Stack<string>.bos alanları farklı alanlardır.

Static değiştiricisiz tanımlanmış alanlar birer örnek alanlardır. Her bir sınıf örneği, o sınıfın her bir örnek alanın kopyasını içerir.

Aşağıdaki örnekte; her bir Color sınıfının örneği, her bir r (red), g (green), b (blue) alanlarına sahiptir ama sadece bir adet kopya Siyah, Beyaz, Kırmızı, Yeşil ve Mavi static (durağan) alanlar mevcuttur.

public class Color
{

public static readonly Color Siyah = new Color(0, 0, 0);
public static readonly Color Beyaz = new Color(255, 255, 255);
public static readonly Color Kırmızı = new Color(255, 0, 0);
public static readonly Color Yeşil = new Color(0, 255, 0);
public static readonly Color Mavi = new Color(0, 0, 255);

private byte r, g, b;
public Color(byte r, byte g, byte b)
{
this.r = r;
this.g = g;
this.b = b;
}
}

Yukarıda görüldüğü gibi sadece okunabilir alanlar readonly değiştiricisi ile tanımlanmıştır. Bir readonly alana atama, sadece o alanın tanımlamasındaki bir kısmından ya da aynı sınıftaki bir yapıcı (constructor) sayesinde gerçekleşebilir.

Önemli: Readonly kelimesi dışarıdaki tip yapıcısı tarafından o alanın konumunun değiştirilmesini önler fakat o konumdaki değeri korumaz. Örneğin, aşağıdaki gibi İsimler şeklinde bir sınıfımız olsun;

public class İsimler
{
public static readonly StringBuilder İlkDogan = new StringBuilder("İlker");
public static readonly StringBuilder İkinciDogan = new StringBuilder("Soner");
}

Bir yapıcının dışından direkt olarak İlkdoğan örneğinin sonucunu değiştirmek bize bir derleyici hatası verecektir.

İsimler.İlkDogan = new StringBuilder("Caner"); // Derleyici Hatası

Ama, StringBuilder örneğini modifiye ederek aynı sonuca aşağıdaki şekilde ulaşabilirim;

İsimler.İlkDogan.Remove(0,6).Append("Caner");

Console.WriteLine(İsimler.İlkDogan); // Output “Caner” olur.

Bu yüzden, read-only kullanımı değişmez tipler için sınırlı kullanılmalıdır şeklinde tavsiye edilir. Değişmez tipler (Immutable types), int, double, string gibi açıkça setter’lara (belirleyiciler) sahip değildirler.

C# Sınıflar Kavramı

Sınıflar, C# tiplerinden en temelidir. Bir sınıf, alanları (fields) ve aksiyonları (metod ve diğer fonksiyon üyelerini) basit bir birim içerisinde birleştiren veri yapılarıdır. Sınıflar, dinamik olarak oluşturulmuş örnekler için tanımlama sağlarlar. Ki bunlardan nesneler olarak bilinir. Sınıflar kalıtımı (inheritance) ve polymorphism’i destekleyen yapılardır.

Yeni sınıflar, kullanılan sınıf bildirimlerinden yaratılırlar. Bir sınıf tanımlaması, öncelikle sınıfın niteliği (modifiers), sınıfın ismi, ana sınıf (eğer varsa) ve sınıfa uygulananan arayüzlerler (interface) sıralaması ile olur. Bu tanımlamalardan sonra { ve } işaretleri ile sınıf tanımlaması tamamlanır.

Aşağıda Nokta adında basit bir sınıf tanımlaması yapılmıştır:

public class Nokta
{
public int x, y;
public Nokta(int x, int y)
{
this.x = x;
this.y = y;
}
}

New operatörü ile oluşturulmuş örnek sınıf ki yeni örnek için bellek ayrılmış, örneğe ilk kullanım için yapıcı (constructor) çağrımı yapılır ve geriye bu örneğe bir referans döndürür.

Aşağıda iki Nokta nesnesi oluşturulmuş ve bu iki nesneye refefans iki değişken içinde saklanır:

Nokta n1 = new Nokta(0, 0);
Nokta n2 = new Nokta(10, 20);

Memory bir nesnenin kullanımı ortadan kalktığında o nesnenin kullandığı alanı geri çağırır. Bir nesnenin C#’ta açıkça serbest bırakılması hem gereksiz hem de imkansızdır.

C# İfade Kavramı

C#’ta ifadeler operand ve operatör adı verilen iki elementten oluşur. Bir ifadenin operatörleri, operandlara hangi operasyonların uygulanacağını gösterir. Operatör’lere örnek olarak;  +, -, *,/, ve new anahtar kelimesi gösterilebilir. Operand’lara örnek olarak ise; alanlar (fields), yerel değişkenler ve ifadeler gösterilebilir.

Bir ifade birden fazla operatör içeriyorsa, hangi operatörün işlenileceğine o operatörün öncelik sırası karar verir. Örneğin; X + Y * Z ifadesi X + (Y * Z) olarak işlenir * (çarpma) operatörü + (toplama) operatörüne göre daha önceliklidir.

Önemli: Öncelik sırası hangi operatörlerin hangi sıra ile işleneceğine karar verir. Fakat hangi operandın önce işleneceğine karar vermez. Operand’lar soldan sağa işlenir periyodik olarak. Yukarıdaki örnekte, önce X hesaplanıldı, sonra Y, sonra Z, sonra çarpma işlemi yerine getirildi, sonra da toplama işlemi. X’in Y’den önce hesaplanmasının nedeni solda olması, çarpma işleminin toplama işleminden önce gerçekleşmesinin nedeni önceliğinin yüksek olmasıdır.

Bir çok operatör aşırı yüklenebilir (Overloading). Operatör yüklemesi, kullanıcı tanımlı operatör işlemelerinde spesifik işlemler yapılmasına olanak tanır.

Aşağıdaki tablo, C# operatörlerini yüksek öncelikten düşük önceliğe göre listeler. Aynı kategorideki operatörler eşit önceliğe sahiptir.

Kategori İfade Açıklama
Birincil x.m Üye erişimi
Birincil x(…) Metod ve delegate tanımlama
Birincil x[…] Dizi ve indexer erişimi
Birincil x++ Öncelikli arttırma
Birincil x– Öncelikli azaltma
Birincil new T(…) Nesne ve delegate oluşturma
Birincil new T(…){…} Kullanıma hazır nesne oluşturma
Birincil new {…} Anonymus nesne kullanımı
Birincil new T[…] Dizi oluşturma
Birincil typeof(T) T için mevcut System.Type tipi
Birincil checked(x) İşaretlenmiş konuda hesaplanan ifade
Birincil unchecked(x) İşaretlenmemiş konuda hesaplanan ifade
Birincil default(T) T tipinin mevcut varsayılan değeri
Birincil delegate {…} Anonymus fonksiyon (Anonymus metod)
Tekil +x Özdeşlik
Tekil -x Değil
Tekil !x Lojik değil (Not)
Tekil ~x Bit bit değil (~x = –x-1)
Tekil ++x Ön arttırma
Tekil –x Ön azaltma
Tekil (T)x X’i açık olarak T tipinde çevirme
Çoğulsal x * y Çarpma
Çoğulsal x / y Bölme
Çoğulsal x % y Mod (kalan)
Eklemeli x + y Ekleme, string birleştirme, delegate birleşimi
Eklemeli x – y Çıkarma, delegate uzaklaştırma
Öteleme x << y Sola öteleme
Öteleme x >> y Sağa öteleme
İlişkisel ve tip testi x < y Daha küçük
İlişkisel ve tip testi x > y Daha büyük
İlişkisel ve tip testi x <= y Küçük veya eşit
İlişkisel ve tip testi x >= y Büyük veya eşit
İlişkisel ve tip testi x is T Eğer x, bir T ise true döner, değilse false
İlişkisel ve tip testi x as T Eğer x, bir T tipi ise true döner, değilse false
Eşitlik x == y Eşittir
Eşitlik x != y Eşit değil
Lojik AND x & y Bit bit lojik olarak AND işlemi
Lojik XOR x ^ y Bit bit lojik olarak XOR işlemi
Lojik OR x | y Bit bit lojik olarak OR işlemi
Koşullu AND x && y Eğer x true ise y’yi hesaplar
Koşullu OR x || y Eğer x false ise y’yi hesaplar
Null Kaynaşma X ?? y Eğer x null ise y’yi hesapla, değilse x’i hesapla
Koşullu x ? y : z Eğer x true ise y’yi hesapla, x false ise z’yi hesapla
Atama ve anonymous fonksiyonlar x = y Atama
Atama ve anonymous fonksiyonlar x op= y Bileşik atama. Destekleyen operatörler: *= /= %= += -= <<= >>= &= ^= |=
Atama ve anonymous fonksiyonlar (T x) => y Anonymus fonksiyonlar (lambda expression)

C# Attribute (Nitelik) Kavramı

Bir C# programında assembly’ler, tipler, üyeler, geri dönüş değerleri, parametreler ve diğer varlıklar onların davranışlarını belirleyecek değiştiriciler desteklerler. Örneğin, bir metoda erişebilirlik durumunu public, protedted, private ve internal anahtar kelimeleri belirler. C# bunlar gibi kullanıcı tanımlı bilgileri runtime zamanında programa uygulamak için genelleştirebilir yapıya sahiptir. Program bu gibi ilave bildirim bilgilerini attribute tanımlayarak belirler.

Nitelikler bir sınıf üyesi değildir, sadece ilişkilendirildikleri üyeler için ilave bilgi sağlarlar. Aşağıdaki örnek bir YardımAttribute niteliği tanımlar. Bu nitelik programdaki üyelerin ilişkilerine bir link sağlar:

using System;

namespace ConsoleApplication1
{
public class YardımAttribute : Attribute
{
string url;
string konu;

public YardımAttribute(string url)
{
this.url = url;
}

public string Url
{
get { return url; }
}

public string Konu
{
get { return konu; }
set { konu = value; }
}
}
}

Tüm attribute sınıfları .NET Framework tarafından desteklenen System.Attribute sınıfından türetilirler. Nitelikler ilişkilendirildikleri tanımlamdan önce köşeli parantezler içerisinde nitelik ismi ve herhangi bir argümanı ile tanımlanırlar. Eğer bir niteliğin adı Attribute ile bitiyorsa, o kısmı yazılmadan o niteliğe referans gösterilebilir. Yukarıdaki örneğimiz için konuşursak şu şekilde olur;

 [Yardım("<a href="http://sonergonul.net">http://sonergonul.net")]</a>
public class Oge
{
[Yardım("<a href="http://sonergonul.net&quot;">http://sonergonul.net"</a>, Konu="Göster")]
public void Göster(string text) { }
}

Bu örnek YardımAttribute niteliğini Oge sınıfına ve başka bir YardımAttribute niteliğini de sınıfın içerisindeki Göster metoduna bağlar. Attribute sınıfının public yapıcısı, nitelik bir program varlığına bağlandığında sağlanması gereken bilgiyi kontrol eder. İlave bilgiler attribute sınıfının read-write özelliklerine referans ile sağlanır (Az önceki Konu özelliği gibi).

Aşağıdaki örnek’e bakacak olursa, yansıma kullanılarak runtime zamanında programın varlığının nitelik bilgilerine nasıl erişebileceğimizi gösterir:

class Test
{
static void YardımGöster(MemberInfo uye)
{
YardımAttribute y = Attribute.GetCustomAttribute(uye, typeof(YardımAttribute))
as YardımAttribute;

if (y == null)
{
Console.WriteLine("{0} için yardım yok", uye);
}
else
{
Console.WriteLine("{0} için yardım", uye);
Console.WriteLine(" Url={0} Konu={1}", y.Url, y.Konu);
}
}

static void Main()
{
YardımGöster(typeof(Oge));
YardımGöster(typeof(Oge).GetMethod("Göster"));
}
}

Burada GetCustomAttribute() metodunu okumak istediğimiz bir niteliğin adını bildiğimiz zamanlarda kullanırız. Ve sonra da bir niteliğie ait bir referans elde ettiğimizde o niteliğin üyelerine ulaşabiliriz. Eğer okumak istediğimiz niteliğin adını bilmiyorsak GetCustomAttributes() metodunu kullanabiliriz. Bu metod, bir nesneye bağlanmış tüm niteliklerin listesini okur. Ve aşağıdaki şekilde bu metodu kullanabilirdik;

object[] att = typeof(Oge).GetCustomAttributes(false);
foreach(object o in att)
{

Console.WriteLine(o);

}

Bir nitelik yansıma (Reflection) ile çağırıldığı zaman, attribute sınıfının yapıcısı programın sağladığı bilgiyle çağırılır ve sonuç niteliği geri döndürülür. Eğer ilave bilgiler özellikler ile sağlandıysa, bu özellikler nitelik örneği döndürülmeden önce verilen değere atanırlar.

Attribute parametreleri 2 kategoriye ayrılırlar: konumsal ve isimsel. Konumsal parametreler nitelik sınıfının yapıcısında parametre olarak bulunurlar (public YardımAttribute(string url), http://sonergonul.net). İsimsel parametreler de attribute tipinin public alan ya da özelliklerinde bulunurlar (public string Konu, Konu=”Göster” gibi). Bir nitelik belirlediğinizde, o nitelik ilişkilendirdiği temel alınan nitelik yapıcısının konumsal parametrelerini içermek zorundadır. İsimsel parametreler ise opsiyoneldir. İhtiyaca göre kullanılır ya da kullanılmazlar. Ve bunların sırası önemli değildir. İsimsel parametrelerde değer atanması gerekli değildir. Varsayılan değerleri kullanılabilir.

Bir sınıf varlığına birden fazla nitelik bağlayabiliriz. Her nitelik tek bir köşeli parantez içinde virgül ile ayrılabilir ya da hepsi kendi köşeli parantezleri içerisinde yazılabilir. Aşağıdaki 3 örnek aynı görevi görür;

   [Serializable, Obsolete, Obfuscation]
public class Sınıf { }

[Serializable, Obsolete]
[Obfuscation]
public class Sınıf { }

[Serializable]
[Obsolete]
[Obfuscation]
public class Sınıf { }

Standart Nitelikler (AttributeUsage, Conditional, Obsolete)

Eğer bir nitelik sınıfınızda, tanımlanan niteliğin hangi öğe tipler için uygulanabileceğini belirtmek istersek AttributeUsage kullanmamız gerekir. Örneğin yazımızın başındaki YardımAttribute sınıfından önce;

[AttributeUsage(AttributeTargets.Method)]

şeklinde bir tanımlama yapsaydık, bu nitelik sadece metod yapılarına bağlanabilirdi. AttributeTargets.All seçeneği ile bu niteliğin tüm yapılara bağlanabildiğini gösterebiliriz. Ya da sadece enum ve sınıf tiplerine bağlanabileceğini belirtmek için aşağıdaki şekilde kullanabiliriz;

AttributeTargets.Enum | AttributeTargets.Class

Conditional niteliği ise koşullu metodlar oluşturulmasını sağlar. Koşullu metod ancak #define ile tanımlandığında çağırılır. Aksi halde bu metod geçilir. Bu nitelik System.Diagnostics namespace’inin içerisinde bulunur. Aşağıdaki kodunumuza bir göz atalım;

#define SARI

using System;
using System.Diagnostics;

class Fenerbahce
{
[Conditional("SARI")]
void Sarı()
{ Console.WriteLine("Sarı Rengi"); }

[Conditional("LACİVERT")]
void Lacivert()
{ Console.WriteLine("Lacivert Rengi"); }

public static void Main()
{
Fenerbahce t = new Fenerbahce();

t.Sarı();
t.Lacivert();
}
}

Bu kodumuzun çıktısı sadece “Sarı Rengi” olur. Yukarıdaki gibi Lacivert() metodunda herhangi bir #define tanımı yapılmadığı için bu metod çalıştırılmayacaktır. Aslında bu kadar basittir bu niteliğin kullanımı. Bu nitelik sadece metodlar üzerinde uygulanabilir. Koşullu metodlarla ilgili kısıtlamalardan birisi de bu metodların her zaman void döndürmeleri gerektiğidir.

Obsolete niteliği ise bir program öğesini kullanılmayan olarak işaretlememize olanak verir. Aşağıdaki örneğimizi inceleyelim:

using System;

class Test
{
[Obsolete("Bölme2 metodunu kullan")]
static int Bolme(int x, int y)
{
return x / y;
}

static int Bolme2(int x, int y)
{
return y == 0 ? 0 : x / y;
}

public static void Main()
{
Console.WriteLine(Test.Bolme(5, 4));

Console.WriteLine(Test.Bolme2(5, 4));
}
}

Görüldüğü üzere Bolme2 metodu ile Bolme metodunun daha gelişmiş bir versiyonunu kullandık. Bu programı çalıştırdığımızda iki adet Writeline ifadesi de çalışır ve sonuçlarını 1 gösterir. Fakat kod penceresine baktığımızda Test.Bolme üzerinde aşağıdaki şekilde bir uyarı alırız:

[deprecated] int Test.Bolme(int x, int y)

Warning: ‘Test.Bolme(int x, int y)’ is obsolete: ‘Bolme2 metodunu kullan’