V minulém díle jsme si poměrně podrobně povykládali o datových typech, třídách, dědičnosti, metodách a o všem možné. Měli byste být schopni zodpovědět následující otázky:
- Jaký je rozdíl mezi hodnotovým a referenčním typem? Jaký je rozdíl mezi třídou a strukturou?
- Co je předávání parametrů hodnotou a referencí?
- Co je přetěžování?
- Co je zapečetěná a virtuální metoda?
- K čemu slouží abstraktní třídy?
- Co znamená, když je metoda statická?
Rozhraní
Jak již název tohoto dílu říká, dnes se budeme věnovat rozhraním. K čemu vlastně slouží?
.NET Framework, na rozdíl třeba od C++, nepodporuje vícenásobnou dědičnost. Každá třída může dědit nejvýše z jedné třídy. V .NETu existuje právě jedna třída, která nemá žádného předka, a tou je třída System.Object. Každý datový typ dědí právě z této třídy.
Vícenásobná dědičnost podporována tedy není, což znamená, že jedna třída nemůže dědit z více dalších tříd. Velmi často ale můžeme chtít s jednou třídou (anebo hierarchií tříd) pracovat na různých místech a využívat různé části jejich funkcionality. Nebo můžeme chtít stejným způsobem pracovat s několika třídami, které od sebe navzájem nedědí.
Proto máme k dispozici rozhraní. Rozhraní (anglicky interface) si představte jako seznam metod, vlastností, událostí atd., který musí obsahovat každá třída, která toto rozhraní implementuje.
Uveďme praktický příklad – v .NETu existuje rozhraní IComparable (rozhraní se typicky pojmenovávají s velkým I na začátku), které mohou implementovat všechny typy, jejichž instance mezi sebou můžeme porovnávat. Toto rozhraní předepisuje jednu jedinou metodu, a to CompareTo, která porovná předanou hodnotu s hodnotou, na níž tuto metodu voláme, a vrátí číslo –1, 0 nebo 1 podle toho, jestli je předaná hodnota menší, rovna nebo větší hodnotě instance, na níž je metoda volána.
Každé rozhraní samozřejmě může implementovat libovolné množství metod, vlastností atd.
Deklarace rozhraní
V jazycích VB.NET a C# deklarujeme rozhraní velmi podobně, jako bychom deklarovali abstraktní třídu s abstraktními metodami. Deklarovaným členům nedáváme žádné modifikátory viditelnosti (Public, Private atd.) ani žádná jiná klíčová slova (abstract, virtual resp. MustOverride a Overridable). Rozhraní též nemůže deklarovat statické členy, figuruje pouze na instancích tříd.
Public Interface IVector
Function GetLength() As Double
Sub AddVector(ByVal vector As IVector)
ReadOnly Property X() As Double
ReadOnly Property Y() As Double
End Interface
public interface IVector
{
double GetLength();
void AddVector(IVector vector);
double X { get; }
double Y { get; }
}
Na výše uvedené ukázce jsme deklarovali rozhraní IVector, které předepisuje vlastnosti X a Y typu Double, které jsou jen pro čtení, metodu GetLength, jež vrátí délku vektoru, a metodu AddVector, která do aktuální instance přičte zadaný vektor.
Je nutné si uvědomit, že rozhraní je jenom seznam členů. Neobsahuje žádný kód, jen deklarace. Rozhraní neumožňuje předepisovat proměnné.
Implementace rozhraní
Samotné rozhraní je nám naprosto k ničemu. Potřebujeme třídu, která by jej implementovala. Napíšeme dvě různé implementace vektoru ve 2D prostoru, jedna bude zadána přímo souřadnicemi X a Y, druhá bude zadána délkou a úhlem. Ukážeme si, že díky našemu rozhraní můžeme s oběma těmito třídami, které od sebe nedědí ani nemají mimo System.Object společného předka, pracovat jednotně.
Public Class Vector1
Implements IVector
Private _x, _y As Double
Public Sub AddVector(ByVal vector As IVector) Implements IVector.AddVector
_x += vector.X
_y += vector.Y
End Sub
Public Function GetLength() As Double Implements IVector.GetLength
Return Math.Sqrt(_x * _x + _y * _y)
End Function
Public ReadOnly Property X() As Double Implements IVector.X
Get
Return _x
End Get
End Property
Public ReadOnly Property Y() As Double Implements IVector.Y
Get
Return _y
End Get
End Property
End Class
Public Class Vector2
Implements IVector
Private _length, _angle As Double
Public Sub AddVector(ByVal vector As IVector) Implements IVector.AddVector
Dim _x, _y As Double
_x = X + vector.X
_y = Y + vector.Y
_length += vector.GetLength
_angle = Math.Atan2(_x, _y)
End Sub
Public Function GetLength() As Double Implements IVector.GetLength
Return _length
End Function
Public ReadOnly Property X() As Double Implements IVector.X
Get
Return Math.Sin(_angle) * _length
End Get
End Property
Public ReadOnly Property Y() As Double Implements IVector.Y
Get
Return Math.Cos(_angle) * _length
End Get
End Property
End Class
public class Vector1 : IVector
{
private double x, y;
public double GetLength()
{
return Math.Sqrt(x * x + y * y);
}
public void AddVector(IVector vector)
{
x += vector.X;
y += vector.Y;
}
public double X
{
get { return x; }
}
public double Y
{
get { return y; }
}
}
public class Vector2 : IVector
{
private double length, angle;
public double GetLength()
{
return length;
}
public void AddVector(IVector vector)
{
double x = X + vector.X, y = Y + vector.Y;
length += vector.GetLength();
angle = Math.Atan2(x, y);
}
public double X
{
get { return Math.Sin(angle) * length; }
}
public double Y
{
get { return Math.Cos(angle) * length; }
}
}
Na výše uvedené ukázce máme třídy Vector1 a Vector2. Všimněte si nejprve syntaxe, jakou rozhraní impementujeme. Ve VB.NET je použito klíčové slovo Implements, v C# to vypadá jako klasická dědičnost. Pokud by třída implementovala víc rozhraní (což klidně může, zároveň může dědit z jedné třídy), uvádíme v C# nejprve třídu a pak seznam rozhraní, přičemž tyto názvy oddělujeme čárkami. Ve VB.NET bychom použili klíčové slovo Inherits pro třídu a Implements pro všechna rozhraní, opět je oddělujeme čárkou.
Ve VB.NET každý člen, který pochází z rozhraní, má na konci Implements IVector.něco, čímž jasně říkáme, že následující metoda implementuje tuto metodu z rozhraní. Nemusí se vůbec jmenovat stejně. V C# se naproti tomu u jednotlivých metod a vlastností rozhraní většinou neuvádí, je nutné dodržet názvy a datové typy argumentů, pokud jde o metody.
V případě, že třída má dědit více rozhraní, může se stát, že dvě rozhraní definují položku stejného názvu a například i stejného počtu argumentů. Ve VB.NET je to vyřešeno automaticky klauzulí Implements, prostě uděláme dvě metody s různými názvy a přes Implements je namapujeme na konkrétní metody rozhraní.
V C# se ale metody mapují podle stejných názvů a může vzniknout problém. Proto v C# rozlišujeme implicitní a explicitní implementaci rozhraní. Implicitní jsme již viděli – rozhraní a příslušná metoda či vlastnost je určeno podle názvu automaticky. V případě, kdy by měla nastat pochybnost, to můžeme vyřešit takto:
double IVector.GetLength()
{
return length;
}
V tomto případě jsme napsali implementaci metody GetLength. Nelze zde použít modifikátor viditelnosti, takže tato metoda bude vidět jen, pokud je výraz, na němž jej voláme, typu IVector. Pokud není, metodu neuvidíme, museli bychom nadeklarovat druhou s modifikátorem třeba public, která by tuto zavolala, abychom docílili stejného efektu, jako ve VB.NET.
V praxi bychom tedy, v případě explicitní implementace metody GetLength v C# museli proměnnou typu Vector1 nebo Vector2 přetypovat na IVector, anebo místo konkrétního typu používat proměnnou typu IVector, abychom mohli zavolat GetLength. Anebo do tříd Vector1 i Vector2 uvést kromě výše uvedené ještě veřejnou metodu GetLength:
public double GetLength()
{
return ((IVector)this).GetLength();
}
To už se ale dostáváme k tomu, jak rozhraní používat.
Používáme rozhraní
K čemu nám tedy rozhraní jsou, jsme ukázali v předešlé kapitole. Díky rozhraní IVector můžeme do jedné proměnné uložit jak instance tříd Vector1, tak i tříd Vector2. A to i přesto, že tyto třídy nedědí jedna od druhé, ani nemají společného předka (tedy resp. mají, System.Object, ale ten zase nemá metody, které by s nimi mohly nějak rozumně pracovat, například zjistit délku).
Vnitřní implementace metod našich dvou vektorů je jiná, jeden si pamatuje X a Y souřadnice a délku počítá, druhý si zase pamatuje délku a úhel a počítá z nich souřadnice X a Y.
Dim v1, v2 As IVector
v1 = New Vector1()
v2 = New Vector2()
v1.AddVector(v2)
Console.WriteLine(v2.X)
IVector v1, v2;
v1 = new Vector1();
v2 = new Vector2();
v1.AddVector(v2);
Console.WriteLine(v2.X);
Oba vektory ale mají navenek stejné rozhraní – vlastnosti X a Y, metody GetLength a AddVector. V případě, že budeme mít kód, který bude využívat rozhraní IVector, kdokoliv může napsat svoji implementaci vektoru implementující toto rozhraní, a ať už si bude informace o vektoru ukládat jak a kam chce, kód bude fungovat (pokud tedy dodržíme stejnou funkčnost metod a vlastností).
Další vymoženosti
Rozhraní nemusí implementovat jen třídy a referenční typy obecně, právě naopak. I hodnotové typy mohou implementovat rozhraní, a také to dělají, například již zmiňované IComparable s metodou CompareTo. Schválně se podívejte, co můžete zavolat na Integeru.
Rozhraní mohou od sebe dědit (a dost se to používá). Vícenásobná dědičnost rozhraní je povolena, takže jedno rozhraní může dědit z více různých rozhraní. To už se v praxi vídá méně často, já jsem to nepoužil pokud vím nikdy, ale jde to. Dědičnost rozhraní se chová naprosto intuitivně, pokud víte, principy jsou stejné jako u tříd.
Vestavěná rozhraní
V .NET Frameworku je mnoho různých rozhraní, která si nyní představíme. Nebudeme se zaobírat detaily, na většinu z nich podrobněji narazíme v následujících dílech, ale ta základní je jistě dobré znát.
IEnumerable je rozhraní, které deklaruje metodu GetEnumerator, jež vrací objekt implementující IEnumerator, který má metody MoveNext, Reset a vlastnost Current. Vlastnost Current vrací aktuální prvek, metoda MoveNext přepne na následující prvek (a vlastnost Current bude pak vracet ten) a ještě vrátí, zda-li sada obsahuje další prvky. Reset se posune na začátek. Celá tato maškaráda slouží pro procházení jakýchkoliv kolekcí údajů a spoléhá na ni i vestavěná konstrukce v jazycích VB.NET a C#, a sice cyklus For Each (resp. foreach), který si představíme příště, až se budeme bavit o kolekcích. Víceméně vše, co obsahuje sadu nějakých objektů či hodnot, implementuje IEnumerable, aby bylo možné je procházet cyklem.
IComparable jsme již zmínili, slouží pro definici funkce pro porovnávání objektů. Výhodou je, že s ním počítají různé funkce Sort a umí podle této hodnoty řadit.
IEquatable je něco podobného, obsahuje metodu Equals vracející true nebo false podle toho, jestli se dva objekty rovnají nebo ne.
ICustomFormatter je rozhraní, které podporuje konverzi na řetězec s možností definice formátu. Implementováno je třeba typem DateTime, který reprezentuje datum a čas, aby bylo možné datum formátovat s použitím různých stylů a národních nastavení.
ICollection reprezentuje kolekci dat a dědí z IEnumerable. Kromě možnosti sekvenčního procházení od začátku do konce obsahuje metody pro přidávání a odebírání prvků z kolekce.
O dalších vestavěných rozhraních si povíme něco v dalších dílech tohoto seriálu.
Závěrem
V tomto díle jsme si krátce představili rozhraní. Neprobíral jsem všechny detaily a neukazoval úplně všechny možnosti, co s rozhraními jdou, nerozepisoval jsem všechna omezení, která nám stejně řekne kompilátor, když je porušíme. Není důležité si je pamatovat a umět z hlavy.
Popovídali jsme si i o tom, k čemu se rozhraní dají použít. Můžeme je využít jako náhražku za absenci vícenásobné dědičnosti, dále pro definici společných metod ve třídách, které spolu jinak “nekamarádí”. Dále se rozhraní hojně používají pro implementaci různých návrhových vzorů, které se běžně používají a je velmi příhodné je znát. Také samotný .NET Framework používá mnoho mechanismů, která staví právě na použití rozhraní. Je proto třeba znát základní rozhraní, protože na ně narážíme téměř na každém kroku.