System.Collections veri saklama için kullanılan
yegane sınıflarımızın bulunduğu isim alanıdır. Bu alan altındaki karışık
sınıflar ve arayüzler hakkında yazılmış bilgiler her ne kadar fazla olsa da bazı
belirsizlikleri giderememesi bu makaleyi yazmamdaki temel amaç olmuştur. Hangi
sınıf nerde kullanılmalı, bu sınıfların farklılıkları neler, performansları
nasıl gibi
bazı soruları bu makalede birlikte arayalım.
Öncellikle hatırlatmak istediğim konu boxing
işlemidir. Bir değer tipinin (int, bool, double vs.), referans tipine(delegate,
interface, string, class) veya Object sınıfına dönüştürülmesi işlemine Boxing,
tersine ise Unboxing denmektedir. Örnek vermek gerekirse:
int i = 1;
object o = i; //Boxing
i = (Int32)o; //Unboxing |
Bu işlem programın daha yavaş çalışmasına ve tipin
objeye dönüşmesinden dolayı bellekte daha fazla yer tutmasına sebep olur.
Dolayısıyla ne kadar az boxing eşittir o kadar verimli program.
System.Collections ve System.Collections.Generic
isim alanları altındaki sınıfların ve arayüzlerin en çok bilinenleri aşağıdaki
tabloda bulunmaktadır. Bunları gruplarsak;
System.Collections |
System.Collections.Generic |
Non Generic |
Generic Karşılığı |
Array
ArrayList
BitArray
-
HashTable
SortedList
-
Queue
Stack
IEnumerable
ICollection
IList
IDictionary
CollectionBase
ReadOnlyCollectionBase
DictionaryBase
List<T>
-
LinkedList<T>
Dictionary<TKey, TValue>
SortedList<TKey, TValue>
SortedDictionary<TKey, TValue>
Queue<T>
Stack<T>
IEnumerable<T>
ICollection<T>
IList<T>
IDictionary<TKey, TValue>
Collection<T>
ReadOnlyCollection<T>
| |
Öncelikle System.Collections sınıfına bir göz atalım. Bu
isim alanının içindeki en çok kullanılan arayüzleri ve sınıfları soyağacına göre
tablolaştırırsak:
Şimdide generic olmayan bu sınıf ve arayüzlerimize genel bir bakış atalım:
-Array:
Temel
dizidir. Kötü yanı başlangıçta sınırlarının çizilmesi gerektiğidir. Dolayısıyla
boş kaldığında bellekte gereksiz yere kalabalık yapar. Ayrıca indeks değeri
belirlenen alanı aştığında da çalışma zamanında hata vermesi de bir diğer kötü
yanıdır. Array sınıfı abstract bir sınıftır. Dolayısıyla new anahtar sözcüğü ile
örneklendirilemez. Açıklamak gerekirse biz bir “string[] dizi” oluşturduğumuz
zaman arka tarafta bir Array sınıfı örneklenir. Bu yapının faydası diziye
istediğimiz türden bir nesne atabilme imkanımızdır. Her ne kadar başlangıç
değeri, belleği fazladan kullansada boxing işlemi yapılmadığından bu değer biraz
azalır. Array sınıfı IList arayüzünden türesede Add, Remove, Sort gibi temel
metodlara sahip değildir.
Ayrıca IndexOf, FindIndex gibi indeksleme metodlarını da kullanımımıza sunmaz.
Bu yüzden esnek bir yapıya sahip değildir. Array
sınıfı matris yapısında kullanılan yegane sınıftır.
-ArrayList:
Dizinin benzeridir fakat sadece object tipinden verileri saklar. Dolayısıyla da
gönderilen veri object değilse boxing işlemine tabi tutulur. Fakat belirli bir
sınır vermemize gerek yoktur. Yeni nesne eklendikçe boyutunu otomatikman
arttırır. Add, Remove, Sort ve indeksleme metodlarına sahip olması işlem yapmayı
kolaylaştırır.
-BitArray:
Sadece int, byte veya bool değerler almaktadır. Değişkenlerin bit değerleri ile
işlem yapmak için kullanılır. Başlangıçta sınır vermek zorunludur.
-HashTable ve Sorted List:
Bu iki sınıfın çalışma prensipleri
birbirine çok yakındır. Bir bölgede iki adet veri barındırırlar. Bunlar ‘Key’ ve
‘Value’dur. Verdiğimiz ‘Key’ değeri unique olmak ve null olmamak zorundadır.
HashTable ve SortedList, verilere ulaşırken barındırdıkları unique anahtarları kullanarak
ulaşırlar. Böylece aldıkları verilere bir nevi Identity kolonuna bağlamış
olurlar. Key değerleri atandıktan sonra asla değişmez. Verileri eklerken kendi
içlerinde ‘Key’lerine göre sıralarlar. HashTable bu işlemi Hash kodlarına göre
SortedList ise alfabetik sıralamaya göre yapar. Bu işlem HashTable sınıfının
performansını yükseltsede verilerin düzensiz saklanmasını sağlar. Boyut belirtme
zorunluluğu yoktur ve arama performansları çok yüksektir. SortedList,
HashTable’a göre her ne kadar daha yavaş olsa da IndexOfKey, IndexOfValue,
RemoveAt, GetByIndex, SetByIndex gibi metodları barındırırak indekslemeyi daha
esnek kılar.
-Stack ve Queue:
Stack LIFO, Queue ise FIFO mantığına göre çalışır. İçlerindeki verilerde dolaşma
imkanı yoktur. Sadece en sondaki veya en baştaki veriye ulaşılabilir. Bunun
açıklaması şudur: en baştaki veriye gitmek için sırayla tüm verilerin silinmesi
gerekir. Boyut belirtme zorunluluğu yoktur, kendi otomatik olarak arttırır.
Arayüzler
IEnumerable:
Bilmemiz gereken en önemli arayüzdür ve collection isim alanının en üstündedir.
IEnumerable ve IEnumerator arayüzleri foreach yapısının temelidir. Dizi bazlı
bir sınıf yaptığımızda, foreach yardımıyla bir iterasyon yapmak istersek
sınıfımızı bu arayüzlerden türetmeliyiz. IEnumerable arayüzü IEnumerator
arayüzünden gelen GetEnumerator metodunu kullanır. GetEnumerator metodu ise
MoveNext adlı bir metoda sahiptir. Bu metodu kullanarak ileriye doğru tüm
koleksiyon içinde dolaşılabilir. C#’ta tüm koleksiyonlar IEnumerable arayüzünü
implemente etmektedir. Bu da demektir ki biz foreach yardımıyla hepsinin içinde
dolaşabilme imkanına sahibiz.
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
} |
GetEnumerator metodunun kullanımına bir örnek vermek gerekirse:
ArrayList aryList = new ArrayList();
IEnumerator numerator = aryList.GetEnumerator();
while (numerator.MoveNext())
{
object obj = numerator.Current;
//Current özelliği bize numeratorün üzerinde bulunduğu
nesneyi getirir. MoveNext metodu ise iterasyon bitene kadar çalışır.
}
foreach (object obj in aryList)
{
//foreach döngüsü ile yukarıdaki numerator, tamamen aynı
işlevi görmektedir.
} |
ICollection:
Bu interface de tüm koleksiyonlar tarafından kullanılmaktadır. Count adlı
çok bilindik bir özelliği barındırmaktadır. Ayrıca 2 adet thread özelliği
bulunmaktadır.
public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo(Array array, int index);
} |
IList:
Add, Clear, Contains, IndexOf, Insert, Remove, RemoveAt gibi en çok
kullanılan koleksiyon metodlarını barındırır. Ayrıca indeksleyici içerir.
public interface IList : ICollection, IEnumerable
{
bool IsFixedSize { get; }
bool IsReadOnly { get; }
object this[int index] { get; set; }
int Add(object value);
void Clear();
bool Contains(object value);
int IndexOf(object value);
void Insert(int index, object value);
void Remove(object value);
void RemoveAt(int index);
} |
IDictionary:
HashTable, SortedList gibi sınıfların implemente olduğu
arayüzdür. Key ve value değerleri için Icollection arayüzünü kullanır. Ayrıca
Add, Clear, Contains ve Remove metodlarını içerir. Key değerine göre de bir
indeksleyici barındırır. Bu arayüzlü koleksiyonların seçilme sebebi indeks
değerlerinin sabit kalma isteğidir. Bir veri silindiğinde arkasındaki verilerin
indeks değerleri sabit kalır.
public interface IDictionary :
ICollection, IEnumerable
{
bool IsFixedSize { get; }
bool IsReadOnly { get; }
ICollection Keys { get; }
ICollection Values { get; }
object this[object key] { get; set; }
void Add(object key, object value);
void Clear();
bool Contains(object key);
IDictionaryEnumerator GetEnumerator();
void Remove(object key);
} |
Generic Mimari
System.Collections.Generic isim alanı projeye eklendiğinde karşımıza 7 yeni
sınıf çıkmaktadır; Dictionary, SortedDictionary, LinkedList, List ve tanıdık
gelen Queue, Stack ve SortedList. İsimleri farklı olsa da generic sınıflardan
sadece 1 tanesi (LinkedList<>) tamamen farklıdır. Diğerleri generic olmayan
akrabalarıyla benzer şekilde çalışmaktadır.
-List<>: Heralde en verimli olan dolayısıyla da en çok kullanılan güzide sınıfımızdır. ArrayList
sınıfının generic versiyonudur.
-LinkedList<>: Node yapısına sahip bir listedir. İçindeki her bir node hem önündekine hem
arkasındakine bir göstericiyle bağlıdır. Doubly sıfatını almasının sebebi işte
bu çift taraflı ilişkidir. (Ek bir bilgi vermek gerekirse: LinkedList sınıfının
C’deki kuzeni sadece önündekiyle ilişkilidir, dolayısıyla singly LinkedList
olarak anılır.) Yeni bir node eklenmek istendiğinde LinkedListNode<> sınfı
kullanılır. Bu sınıfın kullanımı çok pratik değildir ve özel durumlarda
kullanılmalıdır. İçindeki nodelara Next ve Previous metodlarıyla ulaşılır.
İndeksleme yapılamadığından, Find metoduyla arama yapılır ve bu da tüm listenin
içinde dolaştığından bize maliyeti fazla olur.
-Dictionary<> ve SortedList<>:
Hash Table ve
SortedList yapısını kullanmaktadırlar. Tek fark Key ve Value değerlerinin
generic olmasıdır. Bu da her bir veri için iki adet boxing işleminden kurtulmak
demektir. Bu da bize çok fazla performans artışı sağlar.
-SortedDictionary<>:
Bu koleksiyon HashTable ve SortedList sınıflarının
yoğurulmasıyla oluşmuş bir sınıftır. Aralarında çok ince farklar vardır.
SortedList’ten daha fazla bellek kullanır. Hız bakımından ise SortedList’ten bazı
durumlara göre yavaş veya hızlıdır. HashTable sınıfının bize sunduğu tüm
imkanları bu sınıfta sağlar. Fakat verileri hash kodlarına göre değil, sıralama yaparak bellekte tutar. Bu
bakımdan da SortedList sınıfını andırır.
IDictionary tabanlı nesnelerin nasıl sıralama yaptığı ile ilgili şu kodu yazarak
bir test yaparsak:
Hashtable hashTable = new Hashtable();
hashTable.Add("5", "m");
hashTable.Add("4", "as");
hashTable.Add("1", true);
hashTable.Add("3", "bir");
hashTable.Add("2", "2");
hashTable.Add("6", 123);
hashTable.Add("a", "öxmcnxö");
hashTable.Add("c", "c");
hashTable.Add("z", "a");
listBox1.Items.Add("HASH TABLE");
foreach (object obj in hashTable.Values)
{
listBox1.Items.Add(obj.ToString());
}
Dictionary<object, object> dictionary = new Dictionary<object,
object>();
dictionary.Add("5", "m");
dictionary.Add("4", "as");
dictionary.Add("1", true);
dictionary.Add("3", "bir");
dictionary.Add("2", "2");
dictionary.Add("6", 123);
dictionary.Add("a", "öxmcnxö");
dictionary.Add("c", "c");
dictionary.Add("z", "a");
listBox2.Items.Add("DICTIONARY<>");
foreach (object obj in dictionary.Values)
{
listBox2.Items.Add(obj.ToString());
}
SortedDictionary<object, object> sortedDictionary = new
SortedDictionary<object, object>();
sortedDictionary.Add("5", "m");
sortedDictionary.Add("4", "as");
sortedDictionary.Add("1", true);
sortedDictionary.Add("3", "bir");
sortedDictionary.Add("2", "2");
sortedDictionary.Add("6", 123);
sortedDictionary.Add("a", "öxmcnxö");
sortedDictionary.Add("c", "c");
sortedDictionary.Add("z", "a");
listBox3.Items.Add("SORTED DICTIONARY<>");
foreach (object obj in sortedDictionary.Values)
{
listBox3.Items.Add(obj.ToString());
}
SortedList sortedList = new SortedList();
sortedList.Add("5", "m");
sortedList.Add("4", "as");
sortedList.Add("1", true);
sortedList.Add("3", "bir");
sortedList.Add("2", "2");
sortedList.Add("6", 123);
sortedList.Add("a", "öxmcnxö");
sortedList.Add("c", "c");
sortedList.Add("z", "a");
listBox4.Items.Add("SORTED LIST");
foreach (object obj in sortedList.Values)
{
listBox4.Items.Add(obj.ToString());
}
SortedList<object, object> sortedListGeneric = new SortedList<object,
object>();
sortedListGeneric.Add("5", "m");
sortedListGeneric.Add("4", "as");
sortedListGeneric.Add("1", true);
sortedListGeneric.Add("3", "bir");
sortedListGeneric.Add("2", "2");
sortedListGeneric.Add("6", 123);
sortedListGeneric.Add("a", "öxmcnxö");
sortedListGeneric.Add("c", "c");
sortedListGeneric.Add("z", "a");
listBox5.Items.Add("SORTED LIST<>");
foreach (object obj in sortedListGeneric.Values)
{
listBox5.Items.Add(obj.ToString());
} |
Bu kod çalıştığında şu sonuç çıkmaktadır:
Görüldüğü üzere SortedDictionary<> ve her iki SortedList aynı şekilde
çalışmışlardır. Önce numerik key değerlerini küçükten büyüğe sıralayıp
dizmişlerdir. Sonrasında kalanları alfabetik sıraya göre eklemişlerdir.
HashTable hash kodlarına göre dizdiğinden belirli bir sıralaması
yoktur.Algoritması bu kodların içinde gizlidr. Dictionary<> ise verilerin
ekleniş sırasını hiç değiştirmeden saklamıştır.
Eğer key değerlerinin tiplerini birbirinden farklı
yaparsak şöyle bir sonuç elde ederiz.
object o = false;
X.Add("5", "m");
X.Add("4", "as");
X.Add(1, true);
X.Add(3, "bir");
X.Add("2", "2");
X.Add("6", 123);
X.Add("a", "öxmcnxö");
X.Add(o, "c");
X.Add("z", "a"); |
Dictionary<> sınıfı gene eklendiği sırayı bozmadan belleğe
almıştır. HashTable ise önce string anahtarları sonra, diğer anahtarları hash
algoritmasına göre sıralamıştır. Diğer 3 sınıfın sonucu ise hüsran olmuştur.
Aşağıda görüldüğü üzere sorted sıfatlı sınıflarımız karşılaştırma hatası
vermişlerdir. Bunun sebebi ise IComparer arayüzünden gelen Comparer adlı
özelliği bulundurmalarıdır.
Sonuç olarak System.Collections
isim alanı altında karşımıza 7 adet non-generic ve 7 adette generic sınıf
çıkmaktadır. Asıl önemli olan doğru sınıfı doğru yerde kullanmaktır. Bu yüzden
her bir koleksiyonun performansını ve nerede kullanılması gerektiğini iyi bilmek
gerekir. Makalemizin son kısmında, tüm koleksiyonlar kullanım amaçları için gruplandırılmış olup
aşağıdaki test koduyla ayrı ayrı performans testleri yapılmıştır.
List<int> list = new List<int>();
int value = 0;
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();
for (int i = 0; i < 1000000; i++)
{
list.Add(i);
}
for (int i = 0; i < 1000000; i++)
{
value = list[i];
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
|
Listeleme Koleksiyonları
Array
ArrayList
List<>
BitArray
LinkedList<> |
*Değer Tipi atandığında
yaklaşık değerler
(Örnekte ‘int’ kullanılmıştır):
1.Array (int[]): 00.1264
2.List<int>: 00.4132
3.ArrayList: 02.6250
*Referans tipi atandığında yaklaşık değerler
(‘Class’ kullanılmıştır):
1.Array (MyClass[]): 00.2376
2.List<MyClass>: 00.6224
3.ArrayList: 00.6721
*Object dizileri kullanıldığı zaman:
1.Array(object[]): 02.1963
2.List<object>: 02.8552
3.ArrayList: 02.8726
*Bellek kullanımı
(1 milyon değer için):
İşlemci ArrayList List<int>
32-bit 19MB 8MB
64-bit 39MB 8.1MB |
Yukarıda görüldüğü üzere Array sınıfı her halükarda en
hızlı sınıftır. Fakat bellekte fazla yer tutar. Bellek kullanımı için en uygun
koleksiyon List<>’dir. Dolayısıyla hız istenirse Array, az bellek kullanımı
istenirse List<> tercih edilmelidir. Eğer koleksiyona eklenicek tip belirli
değilse ArrayList kullanılabilir. Fakat aynı işlevi generic listemizde
karşılayabilme yetisine sahiptir.
dip not: BitArray ve LinkedList<> sınıflarının kullanım amacı farklı
olduğundan teste tabi tutmaya gerek görmedim.
IDictionary Arayüzlü Koleksiyonlar
HashTable
Dictionary
SortedList
SortedList<>
SortedDictionary<> |
*Değer Tipi atandığında yaklaşık değerler
(‘int’ kullanılmıştır):
1.Dictionary<int,int>: 02.9887
2.SortedList<int,int>: 07.4509
3.HashTable: 11.0872
4.SortedDictionary<int,int>: 36.2299
5.SortedList: 40.4208
*Referans tipi atandığında yaklaşık değerler
(‘Class’ kullanılmıştır):
1.Dictionary<int,MyClass>: 03.0526
2.SortedList<int,MyClass>: 07.4548
3.HashTable: 08.0603
4.SortedList: 36.8587
5.SortedDictionary<int,MyClass>:47.3536 |
Dictionary<> ve SortedList<> sınıfları için herhalde söylenicek söze gerek
yoktur. Generic sınıflarımız 1.0’daki atalarına göre çok daha hızlıdır.
SortedDictionary<> ise özel durumlar için tercih edilmelidir.
FIFO / LIFO Koleksiyonları
Stack
Stack<>
Queue
Queue<>
|
*Değer Tipi atandığında yaklaşık değerler
(‘int’ kullanılmıştır):
1.Stack<int>: 00.7244
2.Queue<int>: 01.3969
3.Stack: 02.6948
4.Queue: 03.7186
*Referans tipi atandığında yaklaşık değerler
(‘Class’ kullanılmıştır):
1.Stack: 00.7607
2.Stack<MyClass>: 00.9163
3.Queue<MyClass>: 01.3468
4.Queue: 01.3865 |
Bu tablo için ise yoruma gerek yoktur. Stack sınıfı Queue sınıfına göre daha
hızlıdır.
Bu makalenin genel olarak en önemli sonucu heralde generic mimarinin c sharp’a
ne kadar hız kattığıdır. Geriye kalan ise yazılım geliştiricilerin
doğru sınıfı kullanabilme kabiliyitlerinde gizlidir. Herkese bol koleksiyonlu
günler.
Serkan Yazıcıoğlu
[email protected]
Kaynak:
http://blogs.msdn.com/joshwil/archive/2004/04/13/112598.aspx
Makale:
Tüm Yönleriyle Koleksiyonlar(Collections) C#, Visual C# ve .NET Serkan Yazıcıoğlu
|