5. Kolekce

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í.

5.1. Iterování kolekcemi

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.

5.1.1. Rozhraní 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.

[ukázka kódu]
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 MoveNext() a Reset()) a převzetí aktuálního prvku (vlastnost Current()) jako obecný odkaz na object.

Následuje příklad procházení kolekce:

[ukázka kódu]
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:

[ukázka kódu]
MyCollection myCollection = new MyCollection()
...
//usage of foreach, insert your type instead of XXX 
foreach(XXX item in myCollection)
{
  Console.WriteLine(item);
  ...
}

5.1.2. Implementování IEnumerable a IEnumerator

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:

[ukázka kódu]
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;
    }
  }
}

5.1.3. Rozhraní IDictionaryEnumerator

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ů.

[ukázka kódu]
public interface IDictionary : IEnumerator
{
  DictionaryEntry Entry {get;}
  object Key {get;}
  object Value {get;}
}

5.1.4. Iterátor implementovaný pomocí yield

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:

[ukázka kódu]
class Stack<T> : IEnumerable<T> {
   public IEnumerator<T> GetEnumerator() {
      for(int i=0; i<pos; i++) {
         yield return data[i];
      }
   }
}

5.2. Standardní rozhraní kolekcí

.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ě.

5.2.1. Rozhraní ICollection

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.

[ukázka kódu]
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.

[ukázka kódu]
foreach(string v in s) {
   Console.WriteLine(v);
}

Vlastní vykonávání kódu probíhá takto:

  1. Na začátku je zavolána metoda s.GetEnumerator().

  2. V místě příkazu yield je provádění kódu pozastaveno, je uchován kontext a hodnota data[i] je dosazena do proměnné v.

  3. Je provedeno tělo příkazu foreach (je vypsána hodnota v).

  4. Kontext je přepnut zpět do metody GetEnumerator a vykonávání pokračuje za příkazem yield opět až po další příkaz yield

    (můžeme tedy také umístit několik yield příkazů na různá místa v metodě).

  5. 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).

5.2.2. Rozhraní IList

Standardní rozhraní pro indexované kolekce. Kromě ICollection umožňuje indexovat prvky pomocí pozice. Lze odstraňovat, přidávat a měnit prvky kolekce.

[ukázka kódu]
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);
}

5.2.3. Rozhraní IDictionary

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.

[ukázka kódu]
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);
}

5.3. Předdefinované třídy kolekcí

K dispozici je rozsáhlá sada předem sestavených datových struktur.

5.3.1. Třída Array

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í:

[ukázka kódu]
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);

5.3.2. Třída ArrayList

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í:

[ukázka kódu]
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]);

5.3.3. Třída Hashtable

standardní slovníková klíč/hodnota datová struktura. Používá hashovací algoritmus k ukládání a indexování hodnot. Příklad použití:

[ukázka kódu]
Hashtable ht = new Hashtable();
ht["one"] = 1;
ht["two"] = 2;
ht["three"] = 3;
Console.WriteLine(ht["two"]);

5.3.4. Třída Queue

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ě.

5.3.5. Třída Stack

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).

5.3.6. Třída BitArray

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.

5.3.7. Třída SortedList

Slovníková datová struktura implementující rozhraní IDictionary. K indexování prvků používá prohledávání binárním odseknutím.

5.3.8. Třída StringCollection

Datová struktura kolekce pro ukládání řetězců. Implementuje rozhraní ICollection.

5.3.9. Třída StringDictionary

Slovníková datová struktura pro ukládání dvojic klíč/hodnota v nichž je typem klíče řetězec. Nabízí podobné metody jako třída Hashtable. Implementuje standardní rozhraní IEnumerable.

5.4. Třídění instancí

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).

5.4.1. Rozhraní IComparable

Umožňuje jednomu objektu indikovat své pořadí vzhledem k jiné instanci téhož typu.

[ukázka kódu]
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;

  • null je první: a.CompareTo(null) > 0;

  • když a.CompareTo(b), pak a.GetType() == b.GetType() .

Příklad implementace rozhraní:

[ukázka kódu]
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;   
  }
}

5.4.2. Rozhraní IComparer

Implementace tohoto rozhraní provádí porovnávání (či řazení). Porovnává dva nezávislé objekty. Obsahuje jedinou metodu int Compare(object x, object y).

5.5. Generování hešovacího kódu

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:

[ukázka kódu]
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;
  }
}