Ogólnie jest lepiej

Nowoczesne języki programowania, takie jak C#, z jednej strony mają zapewnić jak największe bezpieczeństwo projektowanych aplikacji - co osiąga się, stosując mechanizmy zarządzania kodem - a z drugiej - być tak potężne, elastyczne, wygodne i szybkie, jak C++, z którego niemal wszystkie się w jakimś stopniu wywodzą.

Nowoczesne języki programowania, takie jak C#, z jednej strony mają zapewnić jak największe bezpieczeństwo projektowanych aplikacji - co osiąga się, stosując mechanizmy zarządzania kodem - a z drugiej - być tak potężne, elastyczne, wygodne i szybkie, jak C++, z którego niemal wszystkie się w jakimś stopniu wywodzą.

Przejmując od programisty kontrolę pamięci, zniechęcając go do korzystania ze wskaźników i innych sposobów bezpośredniego odwoływania się do pamięci, a przede wszystkim korzystając z warstwy uruchomieniowej nowoczesne języki siłą rzeczy ograniczają swoje możliwości wpływania na najdrobniejsze szczegóły implementacji programu.

Rysunek 1. Strona MSDN2 zawiera kilka interesujących artykułów wprowadzających w zagadnienie typów ogólnych.

Rysunek 1. Strona MSDN2 zawiera kilka interesujących artykułów wprowadzających w zagadnienie typów ogólnych.

Mamy zatem sprzeczność w samych założeniach takich języków, jak C# i Java. Ale sprzeczność w sytuacji, gdy umiemy dobrze zdefiniować tezę i antytezę, jest zalążkiem rozwoju, bo prowadzi do syntezy. Najlepszy przykład to wprowadzone do C# 2.0 i Java 5.0 typy parametryczne, które są kompromisem między możliwościami potężnej biblioteki STL do C++ a potrzebą zagwarantowania bezpieczeństwa, będącą jednym z głównych założeń C# i Java.

Typy parametryczne, nazywane w oficjalnej dokumentacji C# typami ogólnymi (generic types), pozwalają na definiowanie klas i struktur, które operują na typie wskazanym przez programistę dopiero w poleceniu tworzącym obiekt. W ten sposób można przygotować duże partie kodu do wielokrotnego wykorzystania z różnymi typami parametrów. Typy parametrów mogą być ograniczone do pewnych grup, np. do wybranej klasy, czy struktur, typów mających konstruktor domyślny lub implementujących wskazany interfejs.

W trakcie programowania typów ogólnych szybko ujawniają się braki w zbiorze interfejsów platformy .NET - nie ma ona na przykład interfejsu wyróżniającego zbiór struktur, na których można wykonywać operacje arytmetyczne, a bez takiego ograniczenia korzystanie z operatorów dodawania czy odejmowania nie jest możliwe. To w zasadzie ogranicza użyteczność typów ogólnych do gromadzenia i porządkowania danych, zmniejszając możliwości manipulowania nimi. I nic dziwnego, bo typy ogólne wprowadzono, aby usprawnić działanie kolekcji w .NET 2.0.

Najlepszym sposobem, aby zrozumieć, czym są typy ogólne, i przede wszystkim, aby docenić ich użyteczność jest przykład, który pokazuje typowy sposób ich zastosowania - a jak już wiemy, ich typowym zastosowaniem w C# 2.0 są kolekcje. Przygotujmy zatem typ o nazwie Para, zawierający dwa pola typu określonego przez parametr T. Obiekty typu Para można będzie porównywać. Aby zdecydować o ich kolejności, porównujemy najpierw pierwsze pola obu obiektów i dopiero gdy się okaże, że są równe, o kolejności decyduje drugi argument. Dzięki sparametryzowaniu klasy Para bez żadnych modyfikacji będziemy mogli wykorzystać ten typ do przechowywania zarówno liczb, jak i na przykład łańcuchów. Od typu mającego pełnić funkcję parametru będziemy wymagali jedynie implementacji interfejsu IComparable<T>, a więc tego, żeby elementy tego typu mogły być ze sobą porównywane. Sam typ Para również będzie implementował interfejs IComparable, co pozwoli do sortowania zbioru obiektów Para wykorzystać metody zaimplementowane w klasach kolekcji, np. Array.Sort.

Definiowanie sparametryzowanej klasy

Rysunek 2. Dodawanie do projektu pliku zgodnego z szablonem Class.

Rysunek 2. Dodawanie do projektu pliku zgodnego z szablonem Class.

Uruchamiamy Visual C# 2005. Zacznijmy od utworzenia projektu typu Windows Application o nazwie Pary. Klasę Para zdefiniujemy w osobnym pliku - naciskamy [Ctrl Shift A], w oknie Add New Item (rys. 2) wybieramy szablon Class i w pole Name wpisujemy nazwę pliku Para.cs. Następnie klikamy przycisk Add. Po powrocie do głównego okna Visual C# w pliku Para.cs definiujemy klasę Para według wzoru z poniższego listingu:

using System;

using System.Collections.Generic;

using System.Text;

namespace Pary {

public class Para<T> {

private T pierwszy = default(T);

private T drugi = default(T);

public Para(T pierwszy, T drugi) {

this.pierwszy = pierwszy;

this.drugi = drugi;

}

public override string ToString() {

return pierwszy.ToString() + "\t" +

drugi.ToString();

}

}

}

Zgodnie z powyższą definicją, parametryzowana klasa Para<T> zawiera dwa prywatne pola typu określonego przez parametr T, o nazwach pierwszy i drugi. Klasa ta zawiera również konstruktor pozwalający na inicjowanie prywatnych pól, oraz metodę ToString, która zastępuje metodę odziedziczoną po niejawnie określonej klasie bazowej System.Object.

W wierszu definiującym pola użyte zostało słowo kluczowe default w nowym dla C# kontekście. Wyrażenie to zwraca wartość domyślną typu podanego w argumencie. Dla int i double będzie to 0 i 0.0, a dla string - pusty łańcuch. Inicjacje te umieściłem raczej w celu pokazania nowego użycia słowa default niż z rzeczywistej potrzeby, bo ostateczna wartość jest polom nadawana w konstruktorze.

W wypadku klas, po zdefiniowaniu dowolnego konstruktora konstruktor bezargumentowy (domyślny) nie jest automatycznie tworzony. Inaczej jest w wypadku struktur, w których taki konstruktor zawsze powstaje.

Rysunek 3. Zbiory obiektów typu Para&lt;int&gt;, Para&lt;double&gt; i Para&lt;string&gt;.

Rysunek 3. Zbiory obiektów typu Para<int>, Para<double> i Para<string>.

Zdefiniujmy najprostsze kolekcje złożone z elementów Para<T>. Zmieńmy kartę na Form1.cs [Design] (widok projektowania formy Form1) i umieśćmy w formie przycisk Button. Następnie kliknijmy go dwukrotnie, aby utworzyć domyślną metodę zdarzeniową i umieśćmy w niej polecenia z poniż-szego listingu. Tworzone są w nim trzy tablice złożone z obiektów typu Para<int>, Para<double> oraz Para<string>. Po zdefiniowaniu poniższej metody warto zapisać projekt.

Warto wspomnieć, że parametryzować można nie tylko całe klasy lub struktury, ale również poszczególne metody.

private void

button1_Click(object sender, EventArgs e)

{

Random r = new Random();

//int

Para<int>[] pi = new Para<int>[10];

for (int i = 0; i < pi.Length; i++)

pi[i] = new Para<int>(r.Next(10),r.Next(10));

string si = "";

foreach (Para<int> para in pi)

si += para.ToString() + "\n";

MessageBox.Show("Para<int>\n" + si);

//double

Para<double>[] pd = new Para<double>[10];

for (int i = 0; i < pi.Length; i++)

pd[i] = new Para<double>(r.NextDouble(), r.NextDouble());

string sd = "";

foreach (Para<double> para in pd)

sd += para.ToString() + "\n";

MessageBox.Show("Para<double>\n" + sd);

//string

Para<string>[] ps = new Para<string>[12];

ps[0] = new Para<string>("Śliwa", "Magdalena");

ps[1] = new Para<string>("Liktoras", "Maria");

...

string ss = "";

foreach (Para<string> para in ps)

ss += para.ToString() + "\n";

MessageBox.Show("Para<string>\n" + ss);

}


Zobacz również