Kolekce jsou standardní datové struktury doplňující pole, což je jediná vestavěná datová struktura v jazyce C#. Jazyk obsahuje sadu typů poskytujících datové struktury a podporu vytváření vlastních typů. Ty se dělí do dvou kategorií: rozhraní, jež definují standardizovanou sadu vzorů návrhu pro kolekce obecně, a konkrétní třídy implementující tato rozhraní.
Existuje mnoho různých druhů kolekcí. Interní implementace se značně liší, procházení kolekcemi je téměř univerzální. Tato funkčnost je zajištěna dvěma rozhraními IEnumerable a IEnumerator.
Potřebuje-li kolekce vystavit enumerátor, implementuje rozhraní IEnumerable. Enumerátor umožňuje procházet iterovanými prvky směrem vpřed.
![]() | |
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
bool MoveNext();
object Current {get;}
void Reset();
} | |
Rozhraní IEnumerable má metodu GetEnumerator(), která vrací odkaz na rozhraní IEnumerator.
Rozhraní IEnumerator nabízí standardní mechanismus pro iterování kolekcí (metody a MoveNext()) a převzetí aktuálního prvku (vlastnost Reset()) jako obecný odkaz na Current().object
Následuje příklad procházení kolekce:
![]() | |
MyCollection myCollection = new MyCollection()
...
//IEnumerator usage, insert your type instead of XXX
IEnumerator ie = myCollection.GetEnumerator();
while(ie.MoveNext())
{
XXX item = (XXX) ie.Current;
Console.WriteLine(item);
...
} | |
Příklad procházení kolekce pomocí příkazu foreach:
![]() | |
MyCollection myCollection = new MyCollection()
...
//usage of foreach, insert your type instead of XXX
foreach(XXX item in myCollection)
{
Console.WriteLine(item);
...
} | |
Při implementaci zmíněných rozhraní je třeba věnovat pozornost sémantickým kontraktům rozhraní. Rozhraní IEnumerator je často implementováno jako vnořený typ, který se inicializuje předáním kolekce konstruktoru IEnumerator:
![]() | |
public class MyCollection : IEnumerable
{
int[] data;
public virtual IEnumerator GetEnumerator()
{
return new MyCollection.Enumerator(this);
}
private class Enumerator:IEnumerator
{
MyCollection outside;
int actualIndex = -1;
internal Enumerator(MyCollection outside)
{
this.outside = outside;
}
public object Current
{
get
{
if (actualIndex == outside.data.Length) throw new InvalidOperationException();
return outside.data[actualIndex];
}
}
public bool MoveNext()
{
if (actualIndex > outside.data.Length) throw new InvalidOperationException();
return ++actualIndex < outside.data.Length;
}
public void Reset()
{
actualIndex = -1;
}
}
} | |
Používá se u slovníkových datových struktur. Je to standardizované rozhraní používané k postupnému procházení obsahu slovníku, kde má každý element klíč a hodnotu. Vlastnost Entry je obdobou Current a Key a Value zajišťují přímý přístup ke klíčům a hodnotám elementů.
![]() | |
public interface IDictionary : IEnumerator
{
DictionaryEntry Entry {get;}
object Key {get;}
object Value {get;}
} | |
Pokud bychom do kódunapříklad zásobníku chtěli přidat podporu pro procházení všech prvků příkazem foreach, ve staré verzi C# by to vyžadovalo implementovat v naší třídě rozhraní IEnumerable, čili metodu vracející IEnumerator. Naše datová struktura je velmi jednoduchá, ale u složitějších datových struktur může být implementace enumerátorů dosti pracná. Proto nyní lze podporu foreach zajistit i novým jednodušším způsobem - pomocí iterátorů. Iterátory přinášejí do kódu určitý nový prvek náhledu, patří tak patří mezi složitější z novinek v C# 2.0. Iterátor je programový blok, ve kterém se vyskytuje příkaz yield. Může to být tělo metody, operátoru nebo akcesoru (neboli přistupovače, anglicky accessor). Pro příklad uveďme doplnění iterátoru do výše uvedeného zásobníku:
![]() | |
class Stack<T> : IEnumerable<T> {
public IEnumerator<T> GetEnumerator() {
for(int i=0; i<pos; i++) {
yield return data[i];
}
}
} | |
.NET definuje sadu tří standardních rozhraní, které implementují v zájmu zajištění operací jako určení velikosti, prohledávání kolekce a podobně.
Standardní rozhraní pro počitatelné kolekce. Umožňuje určit velikost , možnost změny, synchronizaci kolekce a podobně. ICollection rozšiřuje IEnumerable, takže je možné typy implementující ICollection procházet podobně jako v předchozích případech.
![]() | |
public interface ICollection : IEnumerable
{
void copyTo(Array array, int index);
int Count {get;}
bool IsReadOnly {get;}
bool IsSynchronized {get;}
object SyncRoot {get;}
} | |
Do deklarace zásobníku jsme přidali informaci, že budeme implementovat generické rozhraní IEnumerable<T>, což je generická varianta klasického rozhraní IEnumerable. Rovněž deklarace metody GetEnumerator se od klasické implementace enumerátorů liší jen použitím generické verze rozhraní IEnumerator<T>. Tělo této metody je však iterátorem.
Procházíme zde naše vnitřní pole, kterým implementujeme zásobník, a příkazem yield postupně jakoby "vracíme" jednotlivé prvky uložené na zásobníku. Překladač tuto konstrukci přeloží do podoby přepínání kontextů. Ukažme si tedy způsob provádění kódu, který používá náš iterátor.
![]() | |
foreach(string v in s) {
Console.WriteLine(v);
} | |
Vlastní vykonávání kódu probíhá takto:
Na začátku je zavolána metoda
s.GetEnumerator().V místě příkazu
yieldje provádění kódu pozastaveno, je uchován kontext a hodnotadata[i]je dosazena do proměnnév.Je provedeno tělo příkazu
foreach(je vypsána hodnotav).Kontext je přepnut zpět do metody
GetEnumeratora vykonávání pokračuje za příkazemyieldopět až po další příkazyield(můžeme tedy také umístit několik
yieldpříkazů na různá místa v metodě).Body 2 až 4 se opakují, dokud neskončí provádění metody
GetEnumerator. Tím je ukončeno i prováděníforeach.
Použití iterátoru ve formě (pojmenované) metody může mít následující podobu: Vytvoříme iterátor vracející pro dané N mocniny dvojky od 2^1 až po 2^N.
IEnumerable Power(int N) {
int counter = 0;
int result = 1;
while(counter++ < N) {
result *= 2;
yield return result;
}
}Iterátor použijeme takto (vypíšeme prvních 10 mocnin dvojky, kód umístíme do jiné metody téže třídy):
foreach(int i in Power(10)) {
Console.WriteLine(i);
}Tímto způsobem tedy můžeme iterátory použít i privátně (bez samostatné třídy) nebo naopak umístit více různých typů iterátorů do jedné třídy (základní iterátor se bude jmenovat GetEnumerator, další si pojmenujeme dle vlastního uvážení). Zbývá dodat, že příkazem yield break; ukončíme vykonávání kódu iterátoru (nelze použít return, neboť metoda s iterátorem je deklarována jako vracející IEnumerable).
Standardní rozhraní pro indexované kolekce. Kromě ICollection umožňuje indexovat prvky pomocí pozice. Lze odstraňovat, přidávat a měnit prvky kolekce.
![]() | |
public interface IList : ICollection, IEnumerable
{
object this[int index] {get; set;}
int Add(object o);
void Clear();
bool Contains(object value);
int IndexOf(object value);
void Insert(int index, object value);
void Remove(object value);
void RemoveAt(int index);
} | |
Standardní rozhraní pro kolekce používající dvojice klíč/hodnota jako hešovací tabulky a mapy vlastností. podobá se IList, ale umožňuje přistupovat k prvkům na základě klíče.
![]() | |
public interface IDictionary : ICollection, IEnumerable
{
object this[object key] {get; set;}
ICollection Keys {get;}
ICollection Values {get;}
void Clear();
bool Contains(object value);
IDictionaryEnumerator GetEnumerator();
void Remove(object key);
} | |
K dispozici je rozsáhlá sada předem sestavených datových struktur.
Datová struktura představující pole pevné velikosti odkazů na objekty jednotného typu. Implementuje rozhraní ICollection, IEnumerable a IList, takže s poli lze pracovat jako se seznamy, s kolekcemi nebo množinami elementů, jimiž lze procházet. Array dále nabízí možnost řazení a prohledávání pole. Přiklad použití:
![]() | |
string[] strs1 = { "now", "time", "right", "is" };
Array.Reverse(strs1);
Array strs2 = Array.CreateInstance(typeof(string), 4);
strs2.SetValue("for", 0);
strs2.SetValue("all", 1);
strs2.SetValue("good", 2);
strs2.SetValue("people", 3);
Array strings = Array.CreateInstance(typeof(string), 8);
Array.Copy(strs1, strings, 4);
strs2.CopyTo(strings, 4);
foreach(string s in strings)
Console.WriteLine(s); | |
Dynamické pole objektů implementující rozhraní IList. Udržuje si interní pole objektů, které je nahrazeno větším polem, jakmile je zcela naplněno elementy. Třída je efektivní při vkládání objektů, protože na konci obvykle bývá volné místo. Příklad použití:
![]() | |
ArrayList a = new ArrayList();
a.Add("Vernon");
a.Add("Corey");
a.Add("William");
a.Add("Muzz");
a.Sort();
for(int i = 0; i< a.Count; i++)
Console.WriteLine(a[i]); | |
standardní slovníková klíč/hodnota datová struktura. Používá hashovací algoritmus k ukládání a indexování hodnot. Příklad použití:
![]() | |
Hashtable ht = new Hashtable(); ht["one"] = 1; ht["two"] = 2; ht["three"] = 3; Console.WriteLine(ht["two"]); | |
Datová struktura reprezentující frontu (FIFO - First In First Out). Poskytuje jednoduché operace pro přidání do fronty (Enqueue), odebrání z fronty (Dequeue), prohlédnutí elementu na začátku fronty a podobně.
Datová struktura reprezentující zásobník (LIFO - Last In First Out). Poskytuje jednoduché operace pro přidání do zásobníku (Push), odebrání ze zásobníku (Pop).
Dynamické pole hodnot bool. Z hlediska paměti je vhodnější než pole hodnot bool, protože používá pro každou položku právě jeden bit, zatímco pole bool používá celý bajt.
Slovníková datová struktura implementující rozhraní IDictionary. K indexování prvků používá prohledávání binárním odseknutím.
Datová struktura kolekce pro ukládání řetězců. Implementuje rozhraní ICollection.
Implementace schopnosti řazení a prohledávání kolekcí závisí na prvcích obsažených v samotných objektech kolekce. K porovnávání se využívá vygenerované číslo, tzv. hešovací kód (hashcode).
Umožňuje jednomu objektu indikovat své pořadí vzhledem k jiné instanci téhož typu.
![]() | |
public interface IComparable
{
int CompareTo(object rhs);
} | |
Sémantická pravidla:
a patří před b, pak
a.CompareTo(b) < 0;a patří za b, pak
a.CompareTo(b) > 0;a je rovno b, pak
a.CompareTo(b) = 0;nullje první:a.CompareTo(null) > 0;když
a.CompareTo(b), paka.GetType() == b.GetType() .
Příklad implementace rozhraní:
![]() | |
public sealed class Person : IComparable
{
public string Name;
public int Age;
public int CompareTo(object o)
{
if (o == null) return 1;
if (o.GetType() != this.GetType()) throw new ArgumentException();
Person rhs = o as Person;
if (Age < rhs.Age) return 1;
if (Age < rhs.Age) return -1;
return 0;
}
} | |
Všechny instance objektů mohou poskytnout 32bitový celočíselný hešovací kód svého obsahu s využitím metody GetHashCode(). Metoda se používá u rychlého (méně důvěryhodného) porovnání dvou objektů. Tzn. v případě, že chceme porovnávat objekty, lze nejprve provést porovnání pomocí GetHashCode() a až po úspěchu použít Equals(..).
U použití GetHashCode() u řazení je dobré si uvědomit, jak generování heše probíhá. Záleží na implementaci, na tom jaký zvolí programátor algoritmus. Dobré algoritmy využívají všech 32 bitů a poskytují dobrou rovnoměrnou distribuci a v ideálním případě zachovávají pořadí proměnných. To aby bylo zajištěno, že například bod(10,20) bude hešován jinak než bod(20,10). Zachování pořadí se obvykle provádí vynásobením každé proměnné nějakým konstantním prvočíslem (obvykle jde o číslo 37).
Některé datové struktury (jako Array) nemusejí svůj obsah hešovat správně, proto může být zapotřebí hešovat je ručně.
Příklad implementace hešování tak aby došlo ke vhodné distribuci:
![]() | |
public sealed class Data
{
public readonly short x, y;
public readonly Color c;
ArrayList al = new ArrayList();
public override int GetHashCode()
{
int hc = 1; //base.GetHashCode when base!=object
hc = 37*hc + x<<16|(ushort)x;
hc = 37*hc + y.GetHashCode();
hc = 37*hc + (c==null ? 0 : c.getHashCode());
foreach (object o in al)
hc = 37*hc + o.GetHashCode()
return hc;
}
} | |
![[ukázka kódu]](images/tip.png)