Jazyky C# a C++ Když je něco jinak... Programovací jazyk C# je natolik podobný jazyku C++, že to programátora zvyklého na C++ snadno svede k nepozornosti. Výsledkem pak může být nejedno nemilé překvapení, neboť některá pravidla jsou v jazyce C# výrazně odlišná. Zde si povšimneme několika úskalí hrozících kvůli těmto rozdílům při práci s metodami. Přetížené metody Podobně jako C++, i C# dovoluje přetěžovat metody. Postup při určení, kterou metodu má překladač zavolat, je v každém z jazyků poněkud jiný. To nejspíš nikoho nepřekvapí, neboť se liší i systém jejich datových typů. Nicméně základní myšlenka je v obou jazycích stejná: Sestaví se množina "kandidátů", tedy metod se stejným identifikátorem a se stejným počtem formálních parametrů, a mezi nimi se vybere metoda, jejíž signatura (v C++ bychom řekli prototyp) nejlépe odpovídá typům skutečných parametrů. (O tom budeme hovořit jako o pravidlu "lepší shody".) Jistě víte, že v C++ není možné rozlišovat přetížené metody na základě toho, zda se jejich parametry předávají hodnotou nebo odkazem. V C# však rozlišovat přetížené metody podle způsobu předávání parametrů lze, neboť ten je součástí signatury. To znamená, že v jedné třídě můžeme mít vedle sebe např. metody void f(int i); // Parametr předávaný hodnotou void f(ref int i); // Parametr předávaný odkazem void f(out int i); // Výstupní parametr Vysvětlení, proč je to v C# možné, je jednoduché: Modifikátory ref, resp. out, jež označují parametry předávané odkazem, resp. výstupní parametry, zapisujeme také před skutečné parametry při volání. Chceme-li např. zavolat metodu f() a jako výstupní parametr jí předat proměnnou x, napíšeme f(out x);. V C#, stejně jako v C++, lze definovat také metody s proměnným počtem parametrů. V C++ k tomu slouží výpustka (...), v C# modifikátor params. Tento modifikátor ovšem v C# k rozlišení přetížených metod použít nelze. Zastínění metody V C#, stejně jako v C++, představuje třída obor viditelnosti identifikátorů. Jestliže v C++ deklarujeme v odvozené třídě stejný identifikátor jako v předkovi, zastíníme tím identifikátor z předka; v C# platí podobné pravidlo pro většinu identifikátorů, v případě metod je však situace složitější. Ukážeme si příklad, kdy se v důsledku toho program v C# chová jinak než analogická konstrukce v C++. Nejprve deklarujeme třídu A, jež bude obsahovat veřejně přístupnou metodu f() typu void bez parametrů: class A // C# { public void f(){/* ... */} } (Podobně jako v C++ i v C# platí, že pokud v nějaké třídě nedeklarujeme žádný konstruktor, doplní do ní překladač veřejně přístupný konstruktor bez parametrů; proto si můžeme dovolit deklaraci konstruktoru ve třídě A vynechat.) Dále deklarujeme třídu B jako potomka třídy A. Ve třídě B deklarujeme opět metodu f(), tentokrát ovšem s jedním parametrem typu int: class B: A // C# { new public void f(int i){} static void Main(string[] args) { B b = new B(); b.f(); } } V metodě Main() vytvoříme instanci třídy B a zavoláme metodu f() bez parametrů; v C# to je v pořádku, neboť metoda f(int i), deklarovaná ve třídě B, není zastíněna metodou f() deklarovanou v předkovi. Na druhé straně analogický program v C++, class A // C++ {public: void f(){} }; class B: public A {public: void f(int i){} }; void main() { C *b = new B; b->f(); } se nepodaří přeložit, neboť překladač ohlásí něco jako "příliš málo parametrů při volání metody f()". V C# platí, že deklarujeme-li v odvozené třídě metodu s identifikátorem f, zastíníme tím metodu předka se stejnou signaturou. Metody se stejným identifikátorem, ale s rozdílnou signaturou, zůstanou viditelné. Proto tím, že jsme ve třídě B deklarovali metodu f(int i), jsme nezastínili metodu f()zděděnou od třídy A, neboť se liší jejich signatury. Zděděné a vlastní metody Z předchozího příkladu by se mohlo zdát, že zděděné metody mají v C# stejné postavení jako metody definované v odvozené třídě. To však není pravda; podívejme se na následující příklad: using System; // C# class A { public void f(int i) { Console.WriteLine("A.f(int)"); } }; class B: A { public void f(long i) { Console.WriteLine("B.f(long)"); } }class Program { static void Main(string[] args) { B b = new B(); b.f('a'); } } V předkovi, ve třídě A, jsme definovali metodu f(int i), zatímco v potomkovi metodu f(long i). V metodě Main() vytvoříme instanci třídy B a zavoláme metodu f() se skutečným parametrem typu char. (To lze, neboť v C# je stejně jako v C++ k dispozici implicitní konverze typu char na celočíselné typy.) Podle pravidla o "lepší shodě" bychom mohli očekávat, že se zavolá zděděná metoda f(int i). Výstup tohoto programu nás však přesvědčí, že se volá metoda f(long i) definovaná v odvozené třídě B. Stejná metoda se zavolá i v případě, že metodu f() zavoláme příkazem b.f(1); i když se v tomto případě typ skutečného parametru přesně shoduje s typem formálního parametru zděděné metody. Na druhé straně, změníme-li deklarace metod f() takto: class A // C# { public void f(long i) {Console.WriteLine("A.f(long)");} }; class B: A { public void f(short i) {Console.WriteLine("B.f(short)");} } a zavoláme-li metodu f() příkazy int a = 1; b.f(a); zavolá se zděděná metoda, neboť konverze typu int na short nemůže v C# proběhnout implicitně. Modifikátor new Už jsme si řekli, že definujeme-li v odvozené třídě složku se stejným identifikátorem jako v předkovi, zděděnou složku zastíníme. To ve skutečnosti nepotřebujeme příliš často. Proto zavádí jazyk C# modifikátor new, kterým překladači vysvětlujeme, že nejde o překlep a že si zastínění zděděné složky opravdu přejeme. Vynecháme-li tento modifikátor, ohlásí překladač varování; podobně to dopadne, uvedeme-li tento modifikátor v deklaraci, která nic nezastiňuje. Modifikátor new nesmíme v C# použít spolu s modifikátorem override. Můžeme ho však použít spolu s modifikátorem virtual, tj. v odvozené třídě smíme deklarovat např. metodu public new virtual void f() {/* ... */} Takováto deklarace přeruší hierarchii virtuálních metod a znamená novou implementaci, nezávislou na implementaci z předka. (Nejspíš nepůjde o nijak často používanou konstrukci, ale nějaký důvod, proč to tvůrci jazyka zavedli, asi bude.) Přístupová práva V C++ se při vyhledávání přetížených metod neuplatňují přístupová práva: Nejprve se zjistí, která metoda nejlépe odpovídá typům skutečných parametrů při volání, a pak se určí, zda ji lze zavolat (zda to dovolují přístupová práva). V C# se přístupová práva uplatňují už při vyhledávání "kandidátů". Podívejme se na příklad: V předkovi, ve třídě A, deklarujeme veřejně přístupnou metodu f() bez parametrů. V potomkovi, ve třídě B, ji zastíníme chráněnou metodou se stejnou signaturou. Ve třídě Program, v metodě Main(), vytvoříme instanci třídy B a zavoláme pro ni metodu f(): using System; // C# class A { public void f() { Console.WriteLine("A.f"); } }class B: A { new protected void f() { Console.WriteLine("B.f"); } }class Program { static void Main() { B b = new B(); b.f(); } } Tento program zavolá metodu f() zděděnou po třídě A. Metoda f(), deklarovaná ve třídě B, není přístupná, a proto ji překladač při výběru kandidátů nebere v úvahu. Podívejme se ještě na analogický program v C++: #include // C++ class A { public: void f() { cout << "A.f"; } }; class B: public A { protected: void f() { cout << "B.f"; } }; void main() { B *b = new B; b->f(); } Pokus o překlad tohoto programu skončí chybou - překladač oznámí, že metoda B::f() není dostupná, neboť přístupová práva vzal v úvahu až po určení metody, kterou je třeba volat. Filozofii jazyka C++, pokud jde o aplikaci přístupových práv, lze shrnout do věty, že "změna přístupových práv nesmí změnit chování programu". (Tak to formuloval B. Stroustrup v jedné z knih o C++.) Jazyk C# se řídí spíše filozofií "co není přístupné, o tom nevím, to neberu v úvahu". Změna přístupových práv v tomto jazyce proto může vést k poměrně zásadní změně v chování programu: V závislosti na použitém modifikátoru se budou volat různé metody! Není jazyk jako jazyk Tolik prozatím k nejmarkantnějším odlišnostem C# a C++, pokud jde o zacházení s metodami. Tím ovšem nejsou vyčerpána všechna nebezpečí vyplývající ze shodné či podobné syntaxe a rozdílné sémantiky programových konstrukcí v různých jazycích. K jazyku C#, v němž takové záměny hrozí poměrně často, a k jeho porovnání s jinými programovacími jazyky se proto ještě někdy vrátíme. Miroslav Virius, autor@chip.cz