Testowy plik

Transkrypt

Testowy plik
Rozdzia³ 12 Dziedziczenie
189
Rozdzia³ 12
Dziedziczenie
Rozdzia³ 12 Dziedziczenie
Czêœæ II Zrozumieæ jêzyk C#
Szacunkowy czas
60 min.
W tym rozdziale dowiesz siê, jak:
n Utworzyæ klasê pochodn¹, która dziedziczy po klasie bazowej.
n Wywo³aæ konstruktor klasy bazowej z konstruktora klasy pochodnej.
n Sterowaæ ukrywaniem i przes³anianiem metod za pomoc¹ s³ów kluczowych new,
n
n
n
n
n
n
n
virtual i override.
Ograniczaæ dostêpnoœæ klasy w hierarchii dziedziczenia za pomoc¹ s³owa kluczowego protected.
Tworzyæ interfejs zawieraj¹cy tylko nazwy metod.
Implementowaæ interfejs w strukturze lub klasie, pisz¹c treœæ metod.
Zawrzeæ wspólne funkcje implementacji w klasie abstrakcyjnej.
Zadeklarowaæ, ¿e z klasy nie mo¿na wywodziæ nowych klas, za pomoc¹ s³owa kluczowego sealed.
Napisaæ klasê implementuj¹c¹ wiele interfejsów.
Zrealizowaæ metody interfejsu, u¿ywaj¹c techniki jawnej implementacji interfejsu.
Co to jest dziedziczenie?
Ogólnie rzecz bior¹c, programiœci czêsto nie rozumiej¹, czym jest dziedziczenie. Po czêœci
bierze siê to st¹d, ¿e samo s³owo „dziedziczenie” ma wiele znaczeñ. Jeœli ktoœ zapisuje ci
coœ w testamencie, mówimy, ¿e to dziedziczysz. Mówimy te¿, ¿e dziedziczysz po³owê
genów po matce, a po³owê po ojcu. Oba te znaczenia nie maj¹ nic wspólnego z dziedziczeniem w programowaniu. Tutaj chodzi przede wszystkim o klasyfikacjê, o zwi¹zek
miêdzy klasami. Kiedy by³eœ w szkole, zapewne uczy³eœ siê o ssakach i dowiedzia³eœ
siê, ¿e pies jest ssakiem. W Visual C# móg³byœ to zobrazowaæ, tworz¹c dwie klasy –
jedn¹ o nazwie ssak, drug¹ o nazwie pies – i deklaruj¹c, ¿e pies dziedziczy po ssaku.
Dziedziczenie wskazywa³oby, ¿e miêdzy tymi klasami istnieje zwi¹zek, i podkreœla³o, ¿e wszystkie psy s¹ ssakami.
Wa¿ne Dziedziczenie to relacja miêdzy klasami, a nie obiektami. Deklaruj¹c istnienie zwi¹zku na poziomie klasy, gwarantujemy, ¿e zachodzi on miêdzy wszystkimi
obiektami klasy. Dziedziczenie jest ma³o elastyczne.
190
Czêœæ II Zrozumieæ jêzyk C#
Podstawowa sk³adnia
W tym podrozdziale omówimy podstawow¹ sk³adniê zwi¹zan¹ z dziedziczeniem,
któr¹ musisz znaæ, ¿eby tworzyæ klasy dziedzicz¹ce po innych klasach. Poznawszy tê
sk³adniê bêdziesz móg³ j¹ rozpoznaæ, kiedy zobaczysz j¹ w kodzie lub dokumentacji.
Klasy bazowe i pochodne
Aby zadeklarowaæ, ¿e klasa dziedziczy po innej klasie, u¿ywamy nastêpuj¹cej
sk³adni:
class KlasaPochodna : KlasaBazowa
(
...
}
Klasa pochodna dziedziczy po klasie bazowej. Oznacza to, ¿e klasa pochodna jest
klas¹ bazow¹. Klasa mo¿e pochodziæ od najwy¿ej jednej klasy. Innymi s³owy, nigdy
nie pochodzi od dwóch lub wielu klas.
Wskazówka Programuj¹cy w C++ powinni zauwa¿yæ, ¿e nie mo¿na jawnie okreœliæ, czy dziedziczenie jest publiczne, prywatne czy chronione. Dziedziczenie w C#
jest zawsze publiczne. Programuj¹cy w Javie powinni zwróciæ uwagê na brak s³owa
kluczowego w rodzaju extends.
Klasa System.Object to podstawa wszystkich innych klas. Innymi s³owy, wszystkie
klasy niejawnie dziedzicz¹ po klasie System.Object. Jeœli zadeklarujesz klasê w taki
sposób:
class Token
{
public Token(string name)
{
...
}
...
}
kompilator przepisze j¹ tak (móg³byœ napisaæ to jawnie, ale lepiej tego nie robiæ):
class Token : System.Object
{
public Token(string name)
{
...
}
...
}
Rozdzia³ 12 Dziedziczenie
191
Wywo³ywanie konstruktorów klasy bazowej
Konstruktor klasy pochodnej musi wywo³ywaæ konstruktor klasy bazowej. W tym
celu u¿ywa siê s³owa kluczowego base. Nie ma tu ¿adnej niejednoznacznoœci, poniewa¿ klasa mo¿e mieæ najwy¿ej jedn¹ klasê bazow¹. Oto przyk³ad:
class IdentifierToken : Token
{
public IdentifierToken(string name)
: base(name) // wywo³uje Token(name)
{
...
}
...
}
Jeœli nie wywo³asz jawnie konstruktora klasy bazowej w konstruktorze klasy pochodnej, kompilator spróbuje po cichu wstawiæ wywo³anie domyœlnego konstruktora klasy bazowej. W poprzednim przyk³adzie kompilator przepisa³by poni¿szy fragment:
class Token
{
public Token(string name)
{
...
}
...
}
w nastêpuj¹cy sposób:
class Token : System.Object
{
public Token(string name)
: base()
{
...
}
...
}
To dzia³a, poniewa¿ System.Object ma publiczny konstruktor domyœlny. Jednak¿e nie
wszystkie klasy go maj¹, a wówczas brak wywo³ania konstruktora klasy bazowej spowoduje b³¹d kompilacji:
class IdentifierToken : Token
{
public IdentifierToken(string name)
// b³¹d; klasa bazowa Token nie ma
// publicznego konstruktora domyœlnego
{
...
}
...
}
192
Czêœæ II Zrozumieæ jêzyk C#
Metody new
Je¿eli klasa bazowa i pochodna deklaruj¹ dwie metody o takiej samej sygnaturze (sygnatur¹ metody jest jej nazwa oraz liczba i typ parametrów), to metody te nie s¹ ze
sob¹ zwi¹zane. Jeœli spróbujesz skompilowaæ poni¿szy przyk³ad, kompilator wygeneruje komunikat ostrzegawczy, informuj¹c, ¿e IdentifierToken.Name() ukrywa Token.Name():
class Token
{
...
public string Name() { ... }
}
class IdentifierToken : Token
{
...
public string Name() { ... }
}
Komunikat przypomina, ¿e ze wzglêdu na tak¹ sam¹ sygnaturê i brak zwi¹zku miêdzy dwoma metodami definicja metody Name w klasie IdentifierToken ukrywa metodê
Name w klasie Token. Z regu³y tego rodzaju zbie¿noœæ jest (w najlepszym razie)
Ÿród³em nieporozumieñ, wiêc powinieneœ zmieniæ nazwy metod, aby unikn¹æ konfliktu. Jeœli jednak jesteœ pewien, ¿e dwie metody powinny mieæ tê sam¹ sygnaturê,
mo¿esz wyeliminowaæ komunikat ostrzegawczy, u¿ywaj¹c s³owa kluczowego new:
class Token
{
...
public string Name() { ... }
}
class IdentifierToken : Token
{
...
new public string Name() { ... }
}
U¿ycie s³owa kluczowego new w niczym nie zmienia faktu, ¿e metody nie s¹ ze sob¹
zwi¹zane i ¿e jedna ukrywa drug¹. Wy³¹cza tylko komunikat ostrzegawczy. W rezultacie s³owo kluczowe new informuje: „Jestem profesjonalnym programist¹ i wiem, co
robiê”.
Aby zrozumieæ, na czym polega ukrywanie, rozwa¿ poni¿szy przyk³ad i zastanów
siê, która metoda Name zostanie wywo³ana:
static void Method(Token t)
{
Console.WriteLine(t.Name());
}
static void Main()
{
IdentifierToken variable = new IdentifierToken("variable");
Method(variable);
}
W tym przyk³adzie metoda Main deklaruje zmienn¹ typu IdentifierToken i inicjuje j¹
obiektem typu IdentifierToken. Zmienna jest nastêpnie przekazywana jako argument
Rozdzia³ 12 Dziedziczenie
193
metodzie Method. Co wa¿ne, metoda Method deklaruje swój parametr jako Token, a nie
IdentifierToken. Jest to dozwolone, poniewa¿ ka¿dy IdentifierToken to Token i mo¿na
podstawiæ go za Token. Metoda Method wywo³uje nastêpnie metodê Name tego parametru. Jako istoty ludzkie mo¿emy spojrzeæ na kod i stwierdziæ, ¿e w tym konkretnym przypadku parametr – choæ jego typ okreœlono jako Token – w rzeczywistoœci
odnosi siê do obiektu typu IdentifierToken. Mo¿na by siê wiêc spodziewaæ, ¿e
wywo³anie metody Name oznacza wywo³anie IdentifierToken.Name. Tak jednak nie
jest: oznacza wywo³anie Token.Name. Prawdopodobnie nie tego sobie ¿yczysz. Na
szczêœcie mo¿na sprawiæ, ¿e to wywo³anie bêdzie dzia³a³o zgodnie z oczekiwaniami.
Metody wirtualne
Aby wywo³anie dzia³a³o jak nale¿y, musisz u¿yæ polimorfizmu. Polimorfizm dos³ownie
oznacza „wielokszta³tnoœæ”. W programowaniu tym terminem okreœlamy mo¿liwoœæ
zaimplementowania tej samej metody wiêcej ni¿ jeden raz. Dziêki polimorfizmowi
mo¿esz po³¹czyæ dotychczas niezwi¹zane metody Name klasy bazowej i pochodnej
oraz zadeklarowaæ, ¿e s¹ dwoma implementacjami tej samej metody. Aby zmieniæ
metodê w polimorficzn¹, musisz u¿yæ s³owa kluczowego virtual. Na przyk³ad:
class Token
{
...
public virtual string Name() { ... }
}
S³owo kluczowe virtual oznacza, ¿e jest to pierwsza implementacja metody o nazwie
Name. Natomiast brak tego s³owa kluczowego (jak w poprzednim przyk³adzie) oznacza, ¿e jest to jedyna implementacja metody o nazwie Name.
Domyœlnie metody C# nie s¹ wirtualne (tak jak w C++, ale inaczej ni¿ w Javie, gdzie
metody domyœlnie s¹ wirtualne).
Metody przes³aniaj¹ce
Jeœli klasa bazowa deklaruje, ¿e metoda jest wirtualna, klasa pochodna mo¿e u¿yæ
s³owa kluczowego override, aby zadeklarowaæ inn¹ implementacjê tej metody. Na
przyk³ad:
class IdentifierToken : Token
{
...
public override string Name() { ... }
}
Deklaruj¹c metody polimorficzne za pomoc¹ s³ów kluczowych virtual i override, musisz przestrzegaæ kilku wa¿nych regu³:
n Nie mo¿esz zadeklarowaæ metody prywatnej ze s³owem kluczowym virtual lub
override. Jeœli to zrobisz, spowodujesz b³¹d kompilacji. Prywatne naprawdê znaczy
prywatne.
n Obie metody musz¹ byæ identyczne, tzn. mieæ tê sam¹ nazwê, te same typy parametrów i ten sam typ zwrotny (C# nie obs³uguje kowariancji typów zwrotnych).
194
Czêœæ II Zrozumieæ jêzyk C#
n Dwie metody musz¹ mieæ tê sam¹ dostêpnoœæ. Jeœli jedna metoda jest publiczna,
druga tak¿e musi byæ publiczna. (Programiœci C++ powinni zwróciæ na to uwagê.
W C++ metody mog¹ mieæ ró¿n¹ dostêpnoœæ).
n Mo¿esz przes³oniæ tylko metodê wirtualn¹. Jeœli metoda klasy bazowej nie jest wirtualna i spróbujesz j¹ przes³oniæ, spowodujesz b³¹d kompilacji. Tak byæ powinno;
to klasa bazowa powinna decydowaæ, czy jej metody mog¹ byæ przes³aniane.
n Jeœli klasa pochodna nie deklaruje metody z wykorzystaniem s³owa kluczowego
override, to nie przes³ania metody klasy bazowej. Innymi s³owy, staje siê implementacj¹ zupe³nie innej metody, która przypadkiem ma tê sam¹ nazwê. Tak jak poprzednio, spowoduje to ostrze¿enie podczas kompilacji, które mo¿esz wy³¹czyæ
opisanym ju¿ s³owem kluczowym new.
n Metoda przes³aniaj¹ca jest niejawnie wirtualna i sama mo¿e zostaæ przes³oniêta
w kolejnej klasie pochodnej. Nie mo¿esz tego jednak jawnie deklarowaæ za pomoc¹
s³owa kluczowego virtual. Chodzi o to, ¿e metoda nie ma ¿adnych kwalifikatorów,
jeœli jest zwyk³¹, niepolimorficzn¹ i nieprzes³aniaj¹c¹ metod¹; w przeciwnym razie
ma tylko jeden kwalifikator (ta regu³a obowi¹zuje niemal zawsze).
Po zadeklarowaniu metody w klasie bazowej jako wirtualnej, a metody w klasie pochodnej jako przes³aniaj¹cej, mo¿emy ponownie zaj¹æ siê poprzednim przyk³adem:
static void Method(Token t)
{
Console.WriteLine(t.Name());
}
static void Main()
{
IdentifierToken variable = new IdentifierToken("variable");
Method(variable);
}
Tym razem, kiedy kompilator kompiluje wywo³anie metody Name na parametrze t
(zadeklarowanym jako Token), zauwa¿a, ¿e metoda Token.Name jest wirtualna. Z tego
wzglêdu nie wywo³uje bezpoœrednio Token.Name (co robi³ poprzednio), ale „najbardziej pochodn¹” implementacjê metody Token.Name w rzeczywistym obiekcie, do
którego odnosi siê t. W tym przypadku t odnosi siê do obiektu klasy IdentifierToken,
która przes³ania metodê Token.Name; zapis t.Name spowoduje wiêc, zgodnie z oczekiwaniami, wywo³anie metody IdentifierToken.Name.
Dostêp chroniony
S³owa kluczowe public i private definiuj¹ skrajne rodzaje dostêpnoœci: sk³adowe publiczne klasy s¹ dostêpne dla ka¿dego, a sk³adowe prywatne – dla nikogo (oczywiœcie, z wyj¹tkiem samej klasy). Te dwie skrajnoœci wystarczaj¹, kiedy klasy pozostaj¹
w izolacji. Jak jednak wiedz¹ wszyscy doœwiadczeni programiœci jêzyków obiektowych, izolowane klasy nie mog¹ rozwi¹zywaæ skomplikowanych problemów. Sekret
kryje siê w umiejêtnoœci ³¹czenia klas ze sob¹, w p³ynnym przejœciu od programowania na ma³¹ skalê do programowania na du¿¹ skalê. Dziedziczenie jest potê¿nym mechanizmem ³¹czenia klas, a miêdzy klas¹ pochodn¹ i jej klas¹ bazow¹ (ale nie
Rozdzia³ 12 Dziedziczenie
195
odwrotnie) zachodzi szczególny, œcis³y zwi¹zek. Odzwierciedla go s³owo kluczowe
protected (chroniony):
n Klasa pochodna ma dostêp do chronionych sk³adowych klasy bazowej. Innymi
s³owy, wewn¹trz klasy pochodnej chronione sk³adowe klasy bazowej s¹ publiczne.
n Jeœli klasa nie jest klas¹ pochodn¹, nie ma dostêpu do chronionych sk³adowych klasy. Innymi s³owy, wewn¹trz klasy nie bêd¹cej klas¹ pochodn¹ chronione sk³adowe
innej klasy s¹ prywatne.
C# daje programistom pe³n¹ swobodê deklarowania metod i pól jako chronionych.
Jednak¿e wiêkszoœæ podrêczników programowania obiektowego zaleca, ¿eby pola
by³y œciœle prywatne. Pola publiczne naruszaj¹ hermetycznoœæ, poniewa¿ daj¹
wszystkim u¿ytkownikom klasy bezpoœredni, niekontrolowany dostêp do pól. Pola
chronione s¹ hermetyczne dla u¿ytkowników klasy, poniewa¿ s¹ dla nich niedostêpne. Pozwalaj¹ jednak na naruszenie hermetycznoœci przez klasy pochodne.
Wskazówka Dostêp do chronionej sk³adowej klasy bazowej mo¿esz uzyskaæ nie
tylko w klasie pochodnej, ale tak¿e w klasie wywiedzionej z klasy pochodnej. Chroniona sk³adowa klasy bazowej pozostaje chroniona w klasie pochodnej i w klasach
wywiedzionych z klasy pochodnej.
Tworzenie interfejsów
Dziedziczenie po klasie jest przydatne, ale znacznie wa¿niejsze jest dziedziczenie po
interfejsach. Interfejsy to rzeczywisty powód, dla którego dziedziczenie istnieje. Interfejsy s¹ bardzo wa¿ne; pozwalaj¹ ca³kowicie oddzieliæ nazwê metody od jej implementacji. Do tej pory nie mogliœmy tego zrobiæ (metoda wirtualna musi mieæ treœæ).
Takie oddzielenie daje ogromne mo¿liwoœci.
Interfejsy œciœle oddzielaj¹ „co” od „jak”. Interfejs okreœla tylko nazwê metody, nie
troszcz¹c siê o sposób jej realizacji. Interfejs informuje, jak obiekt powinien byæ u¿ywany, a nie jak jest zaimplementowany w danej chwili. Bardzo podobnie funkcjonuj¹ ludzie; codziennie u¿ywamy wielu urz¹dzeñ, nie zastanawiaj¹c siê, jak dzia³aj¹. Na
przyk³ad sposób dzia³ania telefonów jest dla ciebie nieistotny, poniewa¿ nie wytwarzasz ich, tylko u¿ywasz. Wystarczy, ¿e wiesz, do czego s³u¿y telefon i jak sprawiæ, ¿e
bêdzie on pe³ni³ swoj¹ funkcjê. Wystarczy, ¿e znasz interfejs.
Sk³adnia
Aby zadeklarowaæ interfejs, u¿ywasz s³owa kluczowego interface zamiast class lub
struct. W interfejsie deklarujesz metody tak samo jak w klasie lub strukturze, ale nigdy nie u¿ywasz modyfikatora dostêpnoœci (public, private, protected), a treœæ metody
zastêpujesz œrednikiem. Na przyk³ad:
interface IToken
{
string Name();
}
196
Czêœæ II Zrozumieæ jêzyk C#
Wskazówka Dokumentacja Microsoft .NET Framework zaleca, aby nazwy interfejsów poprzedzaæ du¿¹ liter¹ I. To ostatni bastion notacji wêgierskiej w C#.
Ograniczenia
Nale¿y zapamiêtaæ, ¿e interfejs nigdy nie zawiera implementacji. Nic. Nul. Zero.
Ograniczenia s¹ naturaln¹ konsekwencj¹ tego faktu:
n Nie mo¿esz tworzyæ instancji interfejsu. Gdyby to by³o mo¿liwe, co oznacza³oby
wywo³anie jednej z jego metod, które – z definicji – s¹ tylko nazwane, a nie zaimplementowane?
n W interfejsie nie mo¿e byæ ¿adnych pól. Pole jest implementacj¹ atrybutu obiektu.
Pola s¹ niedopuszczalne, nawet pola statyczne.
n W interfejsie nie mo¿e byæ konstruktorów. Konstruktor zawiera instrukcje s³u¿¹ce
do inicjowania nowo utworzonej instancji obiektu, a nie wolno tworzyæ instancji interfejsu.
n W interfejsie nie mo¿e byæ destruktora. Destruktor zawiera instrukcje s³u¿¹ce do
niszczenia instancji obiektu, ale ¿eby zniszczyæ obiekt, musisz najpierw go utworzyæ, czego nie mo¿esz zrobiæ – bo to interfejs.
n Nie mo¿esz u¿ywaæ modyfikatora dostêpnoœci. Wszystkie metody interfejsu s¹
z za³o¿enia publiczne. Interfejs reprezentuje sposób u¿ycia.
n Nie mo¿esz zagnie¿d¿aæ ¿adnych typów (wyliczeñ, struktur, klas, interfejsów lub
delegacji) w interfejsie.
n Interfejs nie mo¿e pochodziæ od struktury lub klasy. Struktury i klasy zawieraj¹ implementacjê; gdyby interfejs móg³ po nich dziedziczyæ, dziedziczy³by implementacjê.
Implementowanie interfejsu
Aby zaimplementowaæ interfejs, deklarujesz klasê (lub strukturê), która dziedziczy
po interfejsie i implementuje wszystkie jego metody. Na przyk³ad:
class Token : IToken
{
...
public string Name()
{
...
}
}
Kiedy implementujesz interfejs, musisz upewniæ siê, ¿e ka¿da metoda dok³adnie odpowiada swojemu prototypowi w interfejsie:
n Metoda musi byæ jawnie zadeklarowana jako publiczna, poniewa¿ metoda interfejsu jest niejawnie publiczna.
n Typy zwrotne musz¹ do siebie dok³adnie pasowaæ.
n Nazwy metod musz¹ do siebie dok³adnie pasowaæ.
Rozdzia³ 12 Dziedziczenie
197
n Parametry (jeœli s¹) musz¹ do siebie dok³adnie pasowaæ (³¹cznie z modyfikatorami
ref i out, choæ nie params).
Jeœli jest jakaœ ró¿nica, kompilacja nie powiedzie siê, poniewa¿ metoda interfejsu nie
bêdzie zaimplementowana. Domyœlnie metoda implementuj¹ca metodê interfejsu nie
jest wirtualna (by³a ju¿ o tym mowa). Jeœli chcesz przes³oniæ implementacjê metody
w kolejnej klasie pochodnej, musisz zadeklarowaæ metodê jako wirtualn¹ (o tym równie¿ wspomnieliœmy). Na przyk³ad:
class Token : IToken
{
...
public virtual string Name()
{
...
}
}
class IdentifierToken : Token
{
...
public override string Name()
{
...
}
}
Zgadza siê to z zasad¹, ¿e s³owo kluczowe virtual deklaruje pierwsz¹ implementacjê
metody (w kolejnych klasach pochodnych mog¹ byæ inne), a metoda niewirtualna jest
jedyn¹ implementacj¹.
Klasa mo¿e rozszerzaæ inn¹ klasê oraz implementowaæ interfejs. W takim przypadku
C# nie odró¿nia klasy bazowej od interfejsu bazowego za pomoc¹ s³ów kluczowych,
jak robi to np. Java. C# u¿ywa notacji pozycyjnej. Najpierw pisze siê nazwê klasy bazowej, potem przecinek, wreszcie nazwê interfejsu bazowego. Na przyk³ad:
interface IToken
{
...
}
class DefaultTokenImpl
{
...
}
class IdentifierToken : DefaultTokenImpl, IToken
{
...
}
Klasy abstrakcyjne
Niemal zawsze okazuje siê, ¿e dany interfejs jest implementowany przez wiele klas
lub struktur. Na przyk³ad interfejs IToken móg³by byæ implementowany przez piêæ
klas, po jednej na ka¿dy typ leksemu w pliku Ÿród³owym C#: IdentifierToken, KeywordToken, LiteralToken, OperatorToken i PunctuatorToken (móg³byœ równie¿ zdefiniowaæ
klasy komentarza i odstêpu). W takiej sytuacji czêœci klas pochodnych maj¹ zwykle
198
Czêœæ II Zrozumieæ jêzyk C#
wspóln¹ implementacjê. Na przyk³ad w poni¿szych dwóch klasach powielenie kodu
jest oczywiste:
class IdentifierToken : IToken
{
public IdentifierToken(string name)
{
this.name = name;
}
public virtual string Name()
{
return name;
}
...
private string name;
}
class StringLiteralToken : IToken
{
public StringLiteralToken(string name)
{
this.name = name;
}
public virtual string Name()
{
return name;
}
...
private string name;
}
Powielenie kodu to znak ostrzegawczy; powinieneœ przepisaæ kod tak, aby unikn¹æ
duplikacji. Mo¿na to jednak zrobiæ dobrze albo Ÿle. Z³y sposób polega na upchniêciu
cech wspólnych w interfejsie. Jest to niepo¿¹dane, bo wówczas musia³byœ zmieniæ interfejs w klasê (poniewa¿ interfejsy nie mog¹ zawieraæ implementacji). Interfejsy wymyœlono nie bez powodu; zostaw interfejs w spokoju. Aby unikn¹æ duplikacji, nale¿y
utworzyæ now¹ klasê, która bêdzie zawieraæ wspóln¹ implementacjê. Na przyk³ad:
class DefaultTokenImpl : IToken
{
public DefaultTokenImpl(string name)
{
this.name = name;
}
public string Name()
{
return name;
}
...
private string name;
}
class IdentifierToken : DefaultTokenImpl, IToken
{
public IdentifierToken(string name)
: base(name)
{
}
...
}
Rozdzia³ 12 Dziedziczenie
199
class StringLiteralToken : DefaultTokenImpl, IToken
{
public StringLiteralToken(string name)
: base(name)
{
}
...
}
Jest to rozwi¹zanie dobre, ale jeszcze nie doskona³e: mo¿esz teraz tworzyæ instancje
klasy DefaultTokenImpl, co raczej nie ma sensu. Klasa DefaultTokenImpl ma tylko zapewniaæ wspóln¹ implementacjê; istnieje tylko po to, aby po niej dziedziczyæ (albo nie
– ka¿da klasa pochodna sama o tym decyduje). Klasa DefaultTokenImpl jest abstrakcyjna, w takim samym sensie jak interfejs. Tworzenie instancji interfejsu jest zawsze niemo¿liwe, ale chc¹c zadeklarowaæ, ¿e nie wolno tworzyæ instancji klasy, musisz o tym
jawnie poinformowaæ za pomoc¹ s³owa kluczowego abstract. Na przyk³ad:
abstract class DefaultTokenImpl
{
public DefaultTokenImpl(string name)
{
this.name = name;
}
public string Name()
{
return name;
}
private string name;
}
Zwróæ uwagê, ¿e nowa klasa DefaultTokenImpl nie implementuje interfejsu IToken.
Mog³aby, ale s³u¿y nie do tego. Klasa abstrakcyjna okreœla wspóln¹ implementacjê,
natomiast interfejs okreœla sposób u¿ycia. Zwykle lepiej oddzieliæ te dwa aspekty i pozwoliæ klasom nieabstrakcyjnym (takim jak StringLiteralToken) samodzielnie decydowaæ o sposobie implementowania interfejsów:
n Mog¹ dziedziczyæ po DefaultTokenImpl oraz IToken; w takim przypadku DefaultTokenImpl.Name staje siê implementacj¹ metody IToken.Name. Oznacza to, ¿e metoda
DefaultTokenImpl.Name musi byæ publiczna. Mo¿esz zadeklarowaæ konstruktor DefaultTokenImpl jako chroniony, ale metoda Name musi pozostaæ publiczna, jeœli ma
byæ implementacj¹ IToken.Name w klasie pochodnej.
n Mog¹ nie dziedziczyæ po DefaultTokenImpl; w takim przypadku bêd¹ musia³y same
implementowaæ metodê IToken.Name. Rezygnacja z dziedziczenia mo¿e mieæ ró¿ne
przyczyny (choæby prosty fakt, ¿e klasa mo¿e mieæ co najwy¿ej jedn¹ klasê bazow¹).
Metody abstrakcyjne
Tylko klasy abstrakcyjne mog¹ deklarowaæ metody abstrakcyjne. Oznacza to, ¿e
w klasie abstrakcyjnej wolno oznaczyæ metodê s³owem kluczowym abstract i zamiast
treœci metody napisaæ œrednik (jak w interfejsie). Metody abstrakcyjne s¹ szczególnie
przydatne wtedy, gdy klasa abstrakcyjna implementuje interfejs, poniewa¿ klasa abstrakcyjna – tak jak zwyk³e klasy – musi „implementowaæ” wszystkie metody interfej-
200
Czêœæ II Zrozumieæ jêzyk C#
su, po którym dziedziczy. Na przyk³ad poni¿sza klasa DefaultTokenImpl kompiluje siê
tylko dlatego, ¿e „implementuje” obie metody dziedziczone po swoim interfejsie:
interface IToken
{
int Line();
string Name();
}
abstract class DefaultTokenImpl : IToken
{
public abstract int Line();
public string Name()
{
return name;
}
public DefaultTokenImpl(string name)
{
this.name = name;
}
private string name;
}
Metoda abstrakcyjna jest niejawnie wirtualna, wiêc w klasach pochodnych musi byæ
implementowana przez metody przes³aniaj¹ce. Na przyk³ad:
class LiteralToken : DefaultTokenImpl
{
public override int Line()
{
...
}
}
Klasy zamkniête
U¿ywanie dziedziczenia nie jest ³atwe. Nie ma siê czemu dziwiæ; jeœli tworzysz interfejs albo klasê abstrakcyjn¹, œwiadomie piszesz coœ, z czego w przysz³oœci bêdziesz
wywodzi³ nowe klasy. Problem w tym, ¿e trudno przewidzieæ przysz³oœæ. ¯eby utworzyæ elastyczn¹ i ³atw¹ w u¿yciu hierarchiê interfejsów, klas abstrakcyjnych oraz
zwyk³ych klas, potrzeba umiejêtnoœci, wysi³ku i znajomoœci problemu, który próbujesz rozwi¹zaæ. Innymi s³owy, jeœli nie tworzysz klasy z zamiarem u¿ycia jej jako klasy bazowej, jest ma³o prawdopodobne, ¿e bêdzie dobrze pe³ni³a tak¹ funkcjê. Na
szczêœcie C# pozwala u¿yæ s³owa kluczowego sealed, aby zapobiec u¿ywaniu klasy
jako klasy bazowej. Na przyk³ad:
sealed class LiteralToken : DefaultTokenImpl, IToken
{
...
}
Jeœli inna klasa zadeklaruje LiteralToken jako klasê bazow¹, wyst¹pi b³¹d kompilacji.
Klasa zamkniêta nie mo¿e deklarowaæ metod wirtualnych. S³owo kluczowe virtual deklaruje, ¿e jest to pierwsza implementacja metody, która bêdzie przes³aniana w klasach
pochodnych, a z klasy zamkniêtej nie mo¿na wywodziæ nowych klas. Zauwa¿ te¿, ¿e
struktura jest niejawnie zamkniêta; nie da siê wywodziæ z niej nowych struktur.
Rozdzia³ 12 Dziedziczenie
201
Metody zamkniête
Za pomoc¹ s³owa kluczowego sealed mo¿na równie¿ zadeklarowaæ metodê jako zamkniêt¹. Oznacza to, ¿e klasa pochodna nie mo¿e przes³aniaæ metody. Zamkn¹æ mo¿na
tylko metodê przes³aniaj¹c¹; zwi¹zek miêdzy interfejsem oraz s³owami kluczowymi
virtual, override i sealed jest nastêpuj¹cy:
n Interfejs wprowadza nazwê metody.
n Metoda wirtualna (virtual) to pierwsza implementacja metody.
n Metoda przes³aniaj¹ca (override) to kolejna implementacja metody.
n Metoda zamkniêta (sealed) to ostatnia implementacja metody.
Rozszerzanie hierarchii dziedziczenia
W poni¿szym æwiczeniu zapoznasz siê z niewielk¹ hierarchi¹ interfejsów i klas, które
wspólnie tworz¹ prosty szkielet.
Wskazówka Szkielet ró¿ni siê od biblioteki tym, ¿e biblioteki mo¿na u¿ywaæ tylko
bezpoœrednio, w œciœle okreœlony sposób, natomiast ze szkieletu mo¿na korzystaæ poœrednio, na wiele ró¿nych sposobów, tworz¹c klasy dziedzicz¹ce po rozwa¿nie zaprojektowanych interfejsach szkieletu.
Szkielet jest aplikacj¹ Microsoft Windows, która symuluje czytanie pliku Ÿród³owego
C# i klasyfikowanie jego treœci na leksemy (np. identyfikatory, s³owa kluczowe, operatory itd.). W drugim æwiczeniu utworzysz klasê, która dziedziczy po interfejsach
szkieletu oraz wyœwietla leksemy pliku Ÿród³owego w polu wzbogaconego tekstu,
u¿ywaj¹c kolorowej sk³adni.
Hierarchia dziedziczenia
1. Uruchom Microsoft Visual Studio .NET.
2. Otwórz projekt CSharp w folderze \Visual C# Step by Step\Chapter 12\CSharp.
Otworzy siê projekt CSharp.
3. Otwórz plik Ÿród³owy SourceFile.cs w okienku Code.
Klasa SourceFile zawiera prywatne pole tablicowe o nazwie tokens:
private
{
new
new
new
new
...
};
IVisitableToken[] tokens =
KeywordToken("using"),
WhitespaceToken(" "),
IdentifierToken("System"),
PunctuatorToken(";"),
Ta tablica zawiera sekwencjê obiektów, które implementuj¹ interfejs IVisitableToken. Symuluj¹ one leksemy prostego pliku Ÿród³owego z programem „hello
world”. Pe³na wersja tego projektu analizowa³aby plik Ÿród³owy o podanej na-
202
Czêœæ II Zrozumieæ jêzyk C#
zwie i tworzy³a leksemy dynamicznie. Klasa SourceFile zawiera te¿ publiczn¹ metodê o nazwie Accept, która przyjmuje jeden parametr typu ITokenVisitor. Treœæ
metody Accept iteruje przez leksemy, wywo³uj¹c ich metody Accept:
public void Accept(ITokenVisitor visitor)
{
foreach(IVisitableToken token in tokens)
{
token.Accept(visitor);
}
}
Dziêki temu parametr visitor odwiedza kolejno ka¿dy leksem.
4. Otwórz plik Ÿród³owy IVisitableToken.cs w okienku Code.
Interfejs IVistableToken dziedziczy po dwóch innych interfejsach, IVisitable oraz
IToken:
interface IVisitableToken: IVistable, IToken
{
}
5. Otwórz plik Ÿród³owy IVisitable.cs w okienku Code.
Interfejs IVisitable deklaruje metodê Accept:
interface IVistable
{
void Accept(ITokenVisitor visitor)
}
Ka¿dy obiekt w tablicy tokens w klasie SourceFile jest dostêpny za poœrednictwem
interfejsu IVisitableToken. Interfejs IVisitableToken dziedziczy metodê Accept. Oznacza to, ¿e ka¿dy leksem musi implementowaæ metodê Accept.
6. Otwórz plik Ÿród³owy Source.cs w okienku Code i znajdŸ klasê IdentifierToken.
Klasa IdentifierToken dziedziczy po abstrakcyjnej klasie DefaultTokenImpl oraz po
interfejsie IVisitableToken. Implementuje metodê Accept w nastêpuj¹cy sposób:
void IVisitable.Accept(ITokenVisitor visitor)
{
visitor.VisitIdentifier(ToString());
}
Inne klasy leksemów wygl¹daj¹ podobnie.
7. Otwórz plik Ÿród³owy ITokenVisitor.cs w okienku Code.
Interfejs ITokenVisitor zawiera po jednej metodzie na ka¿dy typ leksemu.
Ca³a ta hierarchia interfejsów, klas abstrakcyjnych i zwyk³ych klas dzia³a tak, ¿e
mo¿esz napisaæ klasê, która implementuje interfejs ITokenVisitor, utworzyæ instancjê tej klasy i przekazaæ j¹ jako parametr metody Accept klasy SourceFile. Na
przyk³ad:
class MyVisitor : ITokenVisitor
{
public void VisitIdentifier(string token)
{
...
Rozdzia³ 12 Dziedziczenie
}
203
}
public void VisitKeyword(string token)
{
...
}
...
static void Main()
{
SourceFile source = new SourceFile();
MyVisitor visitor = new MyVisitor();
source.Accept(visitor);
}
W rezultacie ka¿dy leksem pliku Ÿród³owego wywo³a odpowiedni¹ metodê obiektu
visitor. Mo¿esz utworzyæ ró¿ne klasy wizytatorów, które bêd¹ wykonywaæ ró¿ne
zadania podczas odwiedzania poszczególnych leksemów. Na przyk³ad:
n Wizytator wyœwietlaj¹cy, który wyœwietla plik Ÿród³owy w polu wzbogaconego
tekstu.
n Wizytator drukuj¹cy, który przekszta³ca znaki tabulacji w spacje i wyrównuje nawiasy klamrowe.
n Wizytator pisowni, który sprawdza pisowniê ka¿dego identyfikatora.
n Wizytator doradczy, który sprawdza, czy identyfikatory publiczne zaczynaj¹ siê
du¿¹ liter¹ i czy interfejsy zaczynaj¹ siê du¿¹ liter¹ I.
n Wizytator z³o¿onoœci, który monitoruje g³êbokoœæ zagnie¿d¿enia nawiasów klamrowych w kodzie.
n Wizytator zliczaj¹cy, który sumuje liczbê wierszy w ka¿dej metodzie, liczbê sk³adowych ka¿dej klasy oraz liczbê wierszy w ka¿dym pliku Ÿród³owym.
W poni¿szym æwiczeniu utworzymy klasê ColorSyntaxVisitor, która wyœwietla plik
Ÿród³owy w polu wzbogaconego tekstu, u¿ywaj¹c kolorowej sk³adni (np. s³owa kluczowe s¹ wyœwietlane na niebiesko).
Pisanie klasy ColorSyntaxVisitor
1. W Solution Explorerze kliknij dwukrotnie pozycjê Form1.cs, aby wyœwietliæ formê
Color Syntax w oknie Designer View. Forma zawiera przycisk Open, który otwiera
plik przeznaczony do podzia³u na leksemy, oraz pole wzbogaconego tekstu, które
wyœwietla leksemy.
204
Czêœæ II Zrozumieæ jêzyk C#
2. Otwórz plik Ÿród³owy Form1.cs w okienku Code i znajdŸ dwa prywatne pola o nazwach codeText oraz Open:
private System.Windows.Forms.RichTextBox codeText;
private System.Windows.Forms.Button Open;
W³aœnie te dwa pola implementuj¹ pole wzbogaconego tekstu oraz przycisk, które
przed chwil¹ widzia³eœ na formie.
3. W okienku Code znajdŸ metodê Form1.Open_Click.
Ta metoda jest wywo³ywana po klikniêciu przycisku Open. Musisz j¹ napisaæ tak,
¿eby wyœwietla³a leksemy w polu wzbogaconego tekstu. Zmieñ metodê Form1.
Open_Click w nastêpuj¹cy sposób:
private void Open_Click(object sender, System.EventArgs e)
{
SourceFile source = new SourceFile();
ColorSyntaxVisitor visitor =
new ColorSyntaxVisitor(codeText);
source.Accept(visitor);
}
4. Otwórz plik Ÿród³owy ColorSyntaxVisitor.cs w okienku Code.
Klasa ColorSyntaxVisitor jest czêœciowo zaimplementowana. Dziedziczy po interfejsie ITokenVisitor i zawiera ju¿ dwa pola oraz konstruktor, który inicjuje referencjê do docelowego pola wzbogaconego tekstu. Twoim zadaniem bêdzie
zaimplementowaæ metody dziedziczone po interfejsie ITokenVisitor oraz wyœwietliæ leksemy w docelowym polu wzbogaconego tekstu.
5. W okienku Code dopisz metodê Write do klasy ColorSyntaxVisitor:
private void Write(string token, Color color)
{
target.AppendText(token);
target.Select(index, index + token.Length);
index += token.Length;
target.SelectionColor = color;
}
Ta metoda do³¹cza leksem do pola wzbogaconego tekstu, wyœwietlaj¹c go w okreœlonym kolorze. Bêd¹ j¹ wywo³ywaæ pozosta³e metody.
Rozdzia³ 12 Dziedziczenie
205
6. W okienku Code napisz pozosta³e metody klasy ColorSyntaxVisitor.
U¿yj koloru Color.Blue dla s³ów kluczowych, ColorGreen – dla litera³ów ³añcuchowych, a Color.Black – dla pozosta³ych leksemów.
void ITokenVisitor.VisitComment(string token)
{
Write(token, Color.Black);
}
void ITokenVisitor.VisitIdentifier(string token)
{
Write(token, Color.Black);
}
void ITokenVisitor.VisitKeyword(string token)
{
Write(token, Color.Blue);
}
void ITokenVisitor.VisitOperator(string token)
{
Write(token, Color.Black);
}
void ITokenVisitor.VisitPunctuator(string token)
{
Write(token, Color.Black);
}
void ITokenVisitor.VisitStringLiteral(string token)
{
Write(token, Color.Green);
}
void ITokenVisitor.VisitWhiteSpace(string token)
{
Write(token, Color.Black);
}
7. Z menu Build wybierz polecenie Build Solution.
W razie potrzeby popraw b³êdy i ponownie zbuduj program.
8. Z menu Debug wybierz polecenie Start Without Debugging.
Pojawi siê forma Color Syntax.
9. Kliknij przycisk Open na formie.
W polu wzbogaconego tekstu uka¿e siê kod ze s³owami kluczowymi wyœwietlonymi na niebiesko, a litera³ami ³añcuchowymi – na zielono.
206
Czêœæ II Zrozumieæ jêzyk C#
10. Zamknij formê.
Wrócisz do Visual Studia .NET.
Praca z wieloma interfejsami
Klasa mo¿e mieæ najwy¿ej jedn¹ klasê bazow¹, ale dowolnie du¿o interfejsów. Klasa
musi implementowaæ wszystkie metody odziedziczone po wszystkich interfejsach.
Klasy abstrakcyjne i zamkniête musz¹ przestrzegaæ tych samych regu³ dziedziczenia
co zwyk³e klasy. Interfejs nie mo¿e dziedziczyæ po ¿adnej klasie (to wprowadzi³oby
do niego implementacjê), ale mo¿e dziedziczyæ po dowolnie wielu interfejsach (bo nie
wprowadza to ¿adnej implementacji).
Sk³adnia
Jeœli interfejs, struktura lub klasa dziedziczy po wiêcej ni¿ jednym interfejsie, interfejsy umieszcza siê na podzielonej przecinkami liœcie. Jeœli klasa ma równie¿ klasê bazow¹, interfejsy nale¿y wymieniæ po klasie bazowej. Na przyk³ad:
class IdentifierToken : DefaultTokenImpl, IToken, IVisitable
{
...
}
Jawna implementacja interfejsu
Istnieje drugi sposób implementowania metody interfejsu przez klasê lub strukturê.
W tym przypadku nazwê metody jawnie kwalifikuje siê nazw¹ interfejsu i nie u¿ywa
modyfikatora dostêpnoœci. Jeœli np. interfejs IVisitable wygl¹da tak:
interface IVisitable
{
void Accept(IVisitor visitor);
}
mo¿na zaimplementowaæ go w taki sposób:
class IdentifierToken : DefaultTokenImpl, IToken, IVisitable
{
...
void IVisitable.Accept(IVisitor visitor)
{
...
}
}
Rozdzia³ 12 Dziedziczenie
207
Nazywamy to jawn¹ implementacj¹ interfejsu (ang. Explicit Interface Implementation,
EII). Metoda EII jest w rezultacie prywatna w implementuj¹cej j¹ strukturze lub klasie. Na przyk³ad:
IdentifierToken token = new IdentifierToken("token");
token.Accept(visitor); // b³¹d kompilacji: metoda niedostêpna
Mo¿na jednak wywo³aæ metodê przez jawnie nazwany interfejs, u¿ywaj¹c rzutowania. Na przyk³ad:
IdentifierToken token = new IdentifierToken("token");
((IVisitable)token).Accept(visitor); // w porz¹dku
Zatem metoda EII nie jest ani ca³kowicie prywatna, ani ca³kowicie publiczna, co wyjaœnia brak kwalifikatora dostêpnoœci. Metody EII s¹ przydatne z co najmniej dwóch
powodów.
Po pierwsze, tworz¹ jeszcze œciœlejszy podzia³ miêdzy interfejsem, który okreœla, jak
nale¿y u¿ywaæ obiektu (typem u¿ycia), a klas¹, która okreœla, jak obiekt jest tworzony
i implementowany (klas¹ kreacji). Metody EII s¹ prywatnymi metodami klasy, wiêc
¿eby je wywo³aæ, trzeba spojrzeæ na obiekt przez jego interfejs.
Po drugie, metody EII rozwi¹zuj¹ potencjalny problem konfliktu nazw miêdzy wieloma interfejsami. Przypuœæmy, ¿e interfejsy IToken oraz IVisitable zawieraj¹ metodê
o nazwie ToString:
interface IToken
{
...
string ToString();
}
interface IVisitable
{
...
string ToString();
}
Czy ci siê to podoba, czy nie, poni¿sza klasa deklaruje jedn¹ metodê ToString, która
jest implementacj¹ obu powy¿szych metod:
class IdentifierToken: IToken, IVisitable
{
...
public string ToString()
{
...
}
}
EII pozwala utworzyæ dwie ró¿ne implementacje metody ToString, po jednej na ka¿d¹
metodê interfejsu. Na przyk³ad:
class IdentifierToken: IToken, IVisitable
{
...
string IToken.ToString()
{
...
}
208
Czêœæ II Zrozumieæ jêzyk C#
string IVisitable.ToString()
{
...
}
}
Zwróæ uwagê, ¿e metoda EII nie jest wirtualna (i nie mo¿e byæ). Nie da siê jej równie¿
przes³oniæ w klasie pochodnej.
Podsumowanie kombinacji s³ów kluczowych
W poni¿szej tabeli wymieniono poprawne (tak) i niepoprawne (nie) kombinacje s³ów
kluczowych.
interface
abstract class
class
sealed class
struct
abstract
nie
tak
nie
nie
nie
new
tak(1)
tak
tak
tak
nie(2)
override
nie
tak
tak
tak
nie(3)
private
nie
tak
tak
tak
tak
protected
nie
tak
tak
tak
nie(4)
public
nie
tak
tak
tak
tak
sealed
nie
tak
tak
tak
nie
virtual
nie
tak
tak
nie
nie
(1) Interfejs mo¿e rozszerzaæ inny interfejs i wprowadzaæ now¹ metodê o takiej samej
sygnaturze.
(2) Struktura niejawnie dziedziczy po klasie System.Object zawieraj¹cej metody, które struktura mo¿e ukryæ.
(3) Struktura niejawnie dziedziczy po klasie System.Object zawieraj¹cej metody wirtualne.
(4) Struktura jest niejawnie zamkniêta i nie mo¿na po niej dziedziczyæ.
Jeœli chcesz przejœæ do nastêpnego rozdzia³u
l Nie wy³¹czaj Visual Studia .NET i zacznij czytaæ rozdzia³ 13.
Jeœli chcesz opuœciæ Visual Studio .NET
l Z menu File wybierz polecenie Exit.
Jeœli zobaczysz okno dialogowe Save (Zapisz), kliknij przycisk Yes (Tak).
Rozdzia³ 12 Dziedziczenie
209
Krótki przewodnik
Aby
Wykonaj nastêpuj¹ce czynnoœci
Utworzyæ klasê pochodn¹
klasy bazowej
Napisz nazwê nowej klasy, dwukropek i nazwê
klasy bazowej. Na przyk³ad:
Wywo³aæ konstruktor
klasy bazowej
Zadeklarowaæ metodê
wirtualn¹
Zadeklarowaæ interfejs
Zaimplementowaæ
interfejs
class Derived : Base
{
...
}
Podaj listê parametrów konstruktora klasy
bazowej przed treœci¹ konstruktora klasy
pochodnej. Na przyk³ad:
class Derived : Base
{
...
public Derived(int x) : Base(x)
{
...
}
...
}
Podczas deklarowania metody u¿yj s³owa
kluczowego virtual. Na przyk³ad:
class Animal
{
public virtual string Talk()
{
...
}
}
U¿yj s³owa kluczowego interface. Na przyk³ad:
interface IDemo
{
string Name();
string Description();
}
Zadeklaruj klasê, u¿ywaj¹c tej samej sk³adni co
w przypadku dziedziczenia, a nastêpnie
zaimplementuj wszystkie funkcje sk³adowe
interfejsu. Na przyk³ad:
class Test : IDemo
{
public string IDemo.Name()
{
...
}
public string IDemo.Description()
{
...
}
}

Podobne dokumenty