Novinky v .NET 2 a VS 2005 (Whidbey). Část I. Vylepšení v .NET jazycích, díl 1 – Generics

   Tak jako celá vývojářská komunita, i my netrpělivě očekáváme příchod nové verze .NET 2 a vývojového prostředí MS Visual Studio 2005 známého pod označením „Whidbey“. Abychom věděli, co nás čeká a abychom s tím mohli seznámit i Vás, vyzkoušeli jsme betaverzi číslem 2. Dnes se seznámíme s první novinkou z řady vylepšení, která pro nás Microsoft připravil uvnitř programovacích jazyků C# a VB.NET.

Generics

   Hned první „vychytávka“ stojí za pozornost. Značně totiž eliminuje práci s  typem object, který je nechvalně známý svým použitím zejména v různých kolekcích typu Stack, ArrayList apod., kdy během kompilace není znám typ, který bude do kolekce přidáván. To vede jednak k tomu, že kompilátor neodhalí možné chyby v typech (a ty se projeví až za běhu) , jednak k tomu, že je nutno provádět boxing a unboxing proměnných z typu object a naopak (což snižuje výkon aplikace).

Jednoduchá ukázka nevýhodnosti typu object je demonstrována v následujícím příkladu, který ve výsledku vede k chybě vyvolané za běhu aplikace:

// using System.Collections;
 
//implementace zásobníku objektů Employee

Stack employees = new Stack();

// parametr je typu object
// je použita implicitní konverze na object

employees.Push(
   new Employee() );

// návratový typ je object
// musíme provést explicitní konverzi

Employee employee =
   (Employee) employees.Pop();

//implementace zásobníku objektů Integer
Stack sizes = new Stack();

// Boxing
sizes.Push( 42 );

// Unboxing
int size1 = (int) sizes.Pop();

// aplikačně nekorektní kód, který však kompilátor zkopmiluje
sizes.Push( 77 );
sizes.Push( new Employee() );

// nyní nastane výjimka InvalidCastException
int size2 = (int) sizes.Pop();

   Řešením je v .NET 2 použití zmíněných generics – tedy jakýchsi šablon kódu, které umožňují specifikaci typu v okamžiku použití šablony.
Šablona se deklaruje za použití speciálního operátoru T, který vyjadřuje typ, a pomocí “špičatých” závorek (deklarace však může být i složitější, viz. poslední příklad). Při použití šablony se T nahrazuje za konkrétní typ objektu.
Výsledkem je typová kontrola kódu již v průběhu kompilace a eliminace boxingu a unboxingu proměnných na typ objekt. A jak tedy vypadá implementace naší kolekce za použití generics ?

// using System.Collections.Generic;

Stack< Employee > employees =
   new Stack< Employee >();

// parametr je typu Employee
// neprovádí se žádná konverze

employees.Push(
   new Employee() );

// návratový typ je Employee
// není nutná konverze

Employee employee =
   employees.Pop();
Stack< int > sizes =
   new Stack< int >();

// boxing se neprovádí
sizes.Push( 42 );

// unboxing se neprovádí
int size1 = sizes.Pop();

sizes.Push( 77 );

// chyba, která je odhalena již kompilátorem
sizes.Push( new Employee() );

// výběr z kolekce proběhne vždy korektně
int size2 = sizes.Pop();

Jak vytvořit vlastní třídu s použitím generics

V případě, že potřebujeme vytvořit vlastní třídu implementující generics, postupujeme obdobně jako při programování běžné třídy, připojíme však navíc deklarace pro substituci datového typu.

// deklaraci třídy doplníme o operátor <T> reprezentující typ konkrétní instance třídy
public class MyStack< T >
{
   private T“ frames;
   private int pointer = 0;

   public MyStack( int size )
   {
      frames = new T` size `;
   }

   public void Push( T frame )
   {
      frames` pointer++ ` =
         frame;
   }
public T Pop()
   {
      return
         frames` –pointer `;
   }
}

// v deklaraci instance třídy uvedeme typ
MyStack< int > s =
   new MyStack< int >( 7 );

//kompilátor dovolí vkládat pouze typově korektní hodnoty
for ( int f = 0; f < 7; ++f )
   s.Push( f );

// vypíše ‘6 5 4 3 2 1 0 ‘
for ( int f = 0; f < 7; ++f )
   Console.Write(
      s.Pop() + " " );

Jak jsme si ukázali, mohou nám generics usnadnit práci s kolekcemi a zbavit nás nepříjemností s typem object. Je však třeba mít na paměti omezení generics, kterými jsou:

  • Omezení tříd – typy použitých objektů musí být potomkem specifikovaného typu
  • Omezení rozhraní – typy použitých objektů musí implementovat sepcifikovaná rozhraní
  • Omezení konstruktoru – typy použitých objektů musí mít veřejný defaultní konstruktor

Výše uvedená omezení jsou patrná na jednoduchém příkladu:

//třída MyList definuje dva různé typy, proto jsou zapsány jako K,V
// a navíc obsahují deklaraci dědičmosti resp. implementovaných rozhraní
class MyList< K, V >
   where K : IComparable,
             IFormattable
   where V : ValueBase, new()
{
   // …
}

class ValueBase {}

class Widget : ValueBase {}

class Thing : ValueBase
{
   public Thing( int i ) {}
}

// OK – integer implementuje IComparable a IFormattable a Widget je potomkem ValueBase a zároveň má i veřejný defaultní konstruktor Widget()

MyList<int, Widget> list1 =
   new MyList<int, Widget>();

// Chyba – string není potomkem ValueBase
MyList<int, string> list2 =
   new MyList<int, string>();

// Chyba – Thing nemá veřejný defaultní konstruktor, resp. kompilátor ho
//nevygeneruje protože je uveden jiný konstruktor

MyList<int, Thing> list3 =
   new MyList<int, Thing>();

// Chyba – Point neimplementuje IComparable a IFormattable
MyList<Point, Widget> list4 = new MyList<Point,Widget>();

Jak dnešní úvodní díl našeho seriálu naznačuje, rozhodně je na co se těšit. A to je teprve malý závdavek. Příště se podíváme rovnou na několik novinek najednou – čekají nás např. partial classes nebo anonymní delegáti.

Autor příspěvku: Petr Hradec