12. Słowniki w C# 2.0

12. Słowniki w C# 2.0

Autor: Paweł Kruczkowski

Opublikowano: 12/12/2006, 12:00 AM

Liczba odsłon: 95230

W poprzednim tygodniu poznaliśmy interfejsy, które są bardzo częstym mechanizmem stosowanym przez programistów. Dzisiaj opowiemy sobie o interfejsach kolekcji. Język C# 2.0 dostarcza nam bowiem 2 rodzaje interfejsów, które służą do wyliczania i porównywania kolekcji. Pierwszy z nich to rodzaj tradycyjny, który jednak nie jest bezpieczny ze względu na typ. Drugi – o którym będzie dzisiaj mowa – to interfejs bezpieczny ze względu na typ. Jak łatwo się domyśleć drugi rodzaj jest jak najbardziej wskazanym do stosowania przez programistów.

Zanim przejdziemy do meritum i zdefiniujemy słowniki w C# 2.0, chcielibyśmy przypomnieć najważniejsze informacje na temat kolekcji oraz wprowadzić pojęcie interfejsów kolekcji.

Na łamach portalu CentrumXP poznaliśmy już pojęcie tablic oraz kolekcji. Oto krótkie ich porównanie:

  1. dla tablicy deklarujemy typ elementów, jakie będą przechowywane w niej, natomiast kolekcje przechowują swoje elementy w postaci obiektów.
  2. egzemplarz tablicy ma ustalony rozmiar, którego nie można zwiększać ani zmniejszać, natomiast kolekcje mogą zmieniać dynamicznie swój rozmiar w zależności od potrzeb
  3. tablica jest strukturą do odczytu i zapisu danych (nie ma możliwości utworzenia tablicy przeznaczonej tylko do odczytu). Natomiast kolekcje można używać w trybie tylko do odczytu (udostępniana jest metoda ReadOnly(), która zwraca wersję kolekcji przeznaczonej tylko do odczytu).

Język C# dostarcza nam wiele standardowych kolekcji takich jak:

  • List<T>
  • Queue<T>
  • Stack<T>
  • Dictonary<T>

Typ T jest dowolnym typem ogólnym (może to być zarówno typ referencyjny jak i bezpośredni). Jak już doskonale wiemy, po kolekcjach bardzo łatwo możemy iterować za pomocą instrukcji foreach.

Na początek napiszmy prosty programik, w którym wykorzystamy klasę List<T>:

public class LiczbyCalkowite
{
private int _liczba;
 
public LiczbyCalkowite(int liczba)
{
this._liczba = liczba;
}
 
public int Liczba
{
get { return _liczba; }
set { _liczba = value; }
}
public override string ToString()
{
return _liczba.ToString();
}
}
 
class Glowna
{
static void Main()
{
string s = "";
List<LiczbyCalkowite> numList = new List<LiczbyCalkowite>();
List<int> intList = new List<int>();
 
//umieszczamy na liscie odpowiednie elementy
for (int i = 0; i < 4; i++)
{
numList.Add(new LiczbyCalkowite(i + 5));
intList.Add(i * 5);
}
//wyswietlamy zawartosc listy numList
foreach (LiczbyCalkowite c in numList)
{
s += c.ToString() + " ";
}
System.Console.WriteLine("Lista pierwsza zawiera: {0}", s);
s = "";
//wyswietlamy zawartosc listy intList
foreach (int i in intList)
{
s += i.ToString() + " ";
}
System.Console.WriteLine("Lista druga zawiera: {0}", s);
}
}

Jak doskonale pamiętamy użyta w powyższym przykładzie klasa List to tablica, która w razie potrzeby dynamicznie zmienia swoją długość. W momencie jej tworzenia nie trzeba definiować liczby przechowywanych przez nią obiektów. Elementy do takiej listy dodajemy używając metody Add(). Jak widzimy w powyższym programie do szybkiej iteracji po liście służy instrukcja foreach, dzięki której w bardzo łatwy sposób możemy wyświetlić wszystkie elementy danej listy.

Pierwsza lista jako typ ogólny przyjmuje klasę LiczbyCalkowite (typ referencyjny), która w swoim ciele definiuje konstruktor przyjmujący liczby całkowite. Druga kolekcja (intList) jako typ ogólny przyjmuje liczby całkowite (typ bezpośredni). Następnie za pomocą pętli for dodajemy do list odpowiednie wartości, które następnie za pomocą instrukcji foreach są wyświetlane w nastepujący sposób:

Poszerzmy teraz powyższy przykład o interfejs IComparable. Jest to jeden z rodzajów interfejsów kolekcji, dzięki któremu możliwe jest porównywanie dwóch obiektów przechowywanych w kolekcji, a co się z tym wiąże ich sortowanie. Klasa List (podobnie jak wszystkie kolekcje) udostępnia metodę Sort(), która umożliwia posortowanie obiektów obsługujących IComparable.

public class LiczbyCalkowite : IComparable<LiczbyCalkowite>
{
private int _liczba;
 
public LiczbyCalkowite(int liczba)
{
this._liczba = liczba;
}
 
public int Liczba
{
get { return _liczba; }
set { _liczba = value; }
}

public override string ToString()
{
return _liczba.ToString();
}
 
//za porownanie odpowiedzialna jest klasa LiczbyCalkowite, ktora
//wykorzystuje domyslna metode CompareTo liczb calkowitych
public int CompareTo(LiczbyCalkowite lcl)
{
return this._liczba.CompareTo(lcl._liczba);
}
}
 
class Glowna
{
static void Main()
{
string s = "";
List<LiczbyCalkowite> numList = new List<LiczbyCalkowite>();
List<int> intList = new List<int>();
 
//gerowanie liczb losowych
Random r = new Random();
 
//umieszczamy na liscie losowo wygenerowane liczby
for (int i = 0; i < 4; i++)
{
numList.Add(new LiczbyCalkowite(r.Next(20) + 5));
intList.Add(r.Next(20) * 5);
}
//wyswietlamy zawartosci listy numList
foreach (LiczbyCalkowite c in numList)
{
s += c.ToString() + " ";
}
System.Console.WriteLine("Lista pierwsza zawiera: {0}", s);
s = "";
//wyswietlamy zawratosc listy intList
foreach (int i in intList)
{
s += i.ToString() + " ";
}
System.Console.WriteLine("Lista druga zawiera: {0}", s);
 
//sortujemy liste pierwsza
numList.Sort();
//wyswietlamy posortowane elementry listy numList
s = "";
for (int i = 0; i < numList.Count; i++)
{
s += numList[i].ToString() + " ";
}
System.Console.WriteLine("Lista pierwsza posortowana: {0}", s);
 
//sortujemy liste druga
intList.Sort();
//wyswietlamy posortowane elementry listy intList
s = "";
for (int i = 0; i < intList.Count; i++)
{
s += intList[i].ToString() + " ";
}
System.Console.WriteLine("Lista druga posortowana: {0}", s);
}
}

W powyższym przykładzie nasza klasa LiczbyCalkowite obsługuje interfejs IComparable<LiczbyCalkowite>. Jest to interfejs kolekcji, który porównuje dwa obiekty przechowywane w niej, co pozwala na ich posortowanie. Aby w pełni obsługiwać ten interfejs klasa LiczbyCalkowite musi udostępniać metodę CompareTo() w następujący sposób:

public int CompareTo(LiczbyCalkowite lcl)
{
return this._liczba.CompareTo(lcl._liczba);
}

W powyższym fragmencie kodu metoda CompareTo(), której opis znajduje się właśnie w interfejsie IComparable przyjmuje jako parametr obiekt lcl typu LiczbyCalkowite. Wiadomo, że jest to obiekt tego typu, ponieważ kolekcja jest bezpieczna ze względu na typ. Metoda ta została tak zaprojektowana, że porównuje wartość aktualnego obiektu LiczbyCalkowite z obiektem przekazazywanym jako parametr. Jeśli ich różnica jest mniejsza od 0, wówczas metoda zwraca liczbę –1, gdy oba obiekty są sobie równe, metoda zwraca 0. Natomiast w przypadku, gdy aktualny obiekt jest mniejszy od tego, który przychodzi jako parametr, wówczas metoda CompareTo() zwraca liczbę 1. W naszym przykładzie porównywana jest składowa _liczba a do jej porównania posłużyliśmy się wbudowaną metodą CompareTo() porównującą wartości dwóch liczb całkowitych.

Drugim wartym komentarza fragmentem kodu jest utworzenie obiektu klasy Random:

Random r = new Random();

Aby pokazać prawidłowość sortowania przez nasz program, wygenerowaliśmy za pomocą obiektu klasy Random losowe liczby z przedziału 0 – 20 (Metoda Next() jest przeciążona i umożliwia przekazanie liczby całkowitej o największej potrzebnej wartości – w naszym przypadku 20).

Po skompilowaniu i uruchomieniu otrzymamy następujące wyniki:

Innym wartym napisania kilka słów interfejsem kolekcji jest IEnumerable<T>. Zawiera on tylko jedną metodę GetEnumerator(), która zwraca implementację typu IEnumerator<T>.

public class Imiona : IEnumerable<string>
{
string[] strings = new string[5];
int licznik = 0;

public IEnumerator<string> GetEnumerator()
{
foreach (string s in strings)
{
yield return s;
}
}
 
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
 
public Imiona(params string[] param)
{
foreach (string s in param)
{
strings[licznik++] = s;
}
}
}
 
class Glowna
{
static void Main(string[] args)
{
Imiona i = new Imiona("Wiola", "Paweł", "Sylwester", "Urszula", "Maria");

foreach (string srg in i)
{
System.Console.WriteLine("Ulubione imiona to: {0}", srg + ".");
}
}
}

W klasie głównej po utworzeniu obiektu „i” klasy Imiona oraz przypisaniu mu odpowiednich wartości używamy pętli foreach, która wykorzystuje interfejs IEnumerable<T> wywołując metodę GetEnumerator(). Metoda ta zwraca wersję interfejsu IEnumerator dla odpowiednich łańcuchów:

public IEnumerator<string> GetEnumerator()
 
Pętla przechodzi przez kolejne łańcuchy znaków w tablicy i tworzy kolejne wartości:
 
foreach (string s in strings)
{
yield return s;
}

W powyższym fragmencie kodu użylismy słówka: yield, które ułatwia nam działanie licznika iteracji poprzez tworzenie kolejnych jego wartości. W ten sposób otrzymamy następujące wyniki:

Kolejnym godnym uwagi interfejsem jest IDictionary<K, V>. Kolekcją, która obsługuje ten interfejs jest kolekcja oparta na parach: klucz – wartość. Przykładem takiej kolekcji są tytułowe słowniki.

Słownik to kolekcja, która zawiera wartości powiązane z kluczami. Przykładem słowników jest kolekcja stolic państw europejskich. Możemy je przechowywać w tablicy np.

string[] stolice = new string[48];

Tablica stolice została zadeklarowana tak, aby mogła pomieścic 48 stolic państw Europy. Gdybyśmy teraz chcieli sprawdzić stolicę Polski, musielibyśmy pamiętać, że to 34-te państwo europejskie w kolejności alfabetycznej. A więc poniższy fragment kodu wyświetli nam stolicę naszego kraju:

string stolicaPolski = stolice[34];

Ale taki dostęp do nazw stolic jest bardzo niewygodny. O wiele lepszym rozwiązaniem jest zastosowanie kolekcji słownikowej, w której wykorzystamy parę klucz - wartość. Jako klucz posłużymy się nazwą państwa europejskiego, a jako wartość – nazwą stolic.

W poniższej tabelce znajdują się najważniejsze metody i właściwości słowników:

Metoda lub właściwość działanie
Count Właściwość, która zwraca liczbę elementów słownika<
Keys Właściwość, która zwraca kolekcję kluczy słownika
Values Właściwość, która zwraca kolekcję wartości słownika
Add() Metoda, która dodaje element o określonym kluczu oraz wartości
Clear() Metoda, która usuwa ze słownika wszystkie elementy
Remove() Metoda, która usuwa ze słownika element o okreslonym kluczu

Typem klucza może być typ dowolny (łańcuchy znaków, liczby całkowite, obiekty etc.). Podobnie jest z typami wartości.

Jak już wiemy, słowniki dziedziczą interfejs IDictionary<K, V>, gdzie K to klucz, a V – wartość. Interfejs ten dostarcza nam właściwość Item, dzięki której możemy na podstawie klucza pobrać odpowiednią dla niego wartość.

Napiszmy prosty program, w którym dodamy do słownika kilka ptaków, a następnie skorzystamy z właściwości Item:

public class Ptaki
{
static void Main()
{
//tworzymy slownik
Dictionary<string, string> dic = new Dictionary<string, string>();
//dodajemy do slownika elementy
dic.Add("001", "jastrząb");
dic.Add("002", "orzeł");
dic.Add("003", "kania");
//wyswietlamy konkretny element
System.Console.WriteLine("Jednym z ptaków drapieżnych jest: {0}", dic["003"] +".");
//usuwamy wszystkie elementy ze slownika
dic.Clear();
//dodajemy do slownika nowy element
dic.Add("004", "gołębie");
//wyswietlamy konkretny element
System.Console.WriteLine("Najlepszymi ptakami na świecie są: {0}", dic["004"] +".");
}
}

Po utworzeniu słownika dodajemy 3 pary: klucz – wartość (zarówno typem klucza jak i typem wartości jest typ podstawowy: string). Na podstawie klucza wyświetlana jest odpowiednia wartość. W powyższym programiku użylismy również metody Clear(), która usuwa ze słownika wszystkie dodane wcześniej elementy. Po uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

Tematem dzisiejszego artykułu były interfejsy kolekcji. Omówiliśmy i pokazaliśmy na przykładach sposoby ich użycia (IComparable<T>, IEnumerable<T>, IList<T> oraz IDictionary<T>).

Niniejszy temat jest również powtórką z kolekcji, o których to była już mowa na łamach portalu CentrumXP. Przypomnieliśmy sobie również klasę List<T>, którą bardzo często stosujemy w naszych programach. Powtórka z iteracji za pomocą instrukcji foreach będzie jak najbardziej przydatna, gdy będziemy chcieli użyć kolejno wszystkich elementów tablicy bądż kolekcji.

Za tydzień opowiemy sobie szerzej o łańcuchach znaków.