第4章 継承とインターフェイス:連載 改訂版 C#入門(1/3 ページ)
継承とインターフェイスは、OOP言語であるC#を学ぶ上で、避けては通れない重要な基本機能である。これらはなぜ必要で、何の役に立つのか?
本記事は、(株)技術評論社が発行する書籍『新プログラミング環境 C#がわかる+使える』から許可を得て転載したものです。同書籍に関する詳しい情報については、本記事の最後に掲載しています。
オブジェクト指向には「継承」と呼ばれる特徴的な機能が存在する。継承とは、あるクラスの機能を含む新しいクラスを作成する機能である。これにより、多様なクラスを素早く作成しながら、基本機能を複数のクラスで共有させることができる。また、継承に似た機能として「インターフェイス」がある。インターフェイスには、クラスが持つべきメソッドなどの種類を定める機能がある。これにより、同じインターフェイスを実装したクラスは、同じ呼び出し方法で利用可能になる。本章では、この2つの機能について解説する。
4-1 機能を追加する継承
継承はオブジェクト指向における重要な機能の1つである。継承とは、あるクラスの持つ機能をそっくり引き継いで、さらに機能を付け加えた新しいクラスを作る機能をいう。
さっそく具体的なサンプル・ソースを見てみよう。List 4-1のサンプル・ソースはごく基本的な継承機能を記述したソース・コードである。
1: using System;
2:
3: namespace Sample001
4: {
5: public class Person
6: {
7: public string getName()
8: {
9: return "私には名前がありません。";
10: }
11: }
12:
13: public class Taro : Person
14: {
15: public string getTaroName()
16: {
17: return "私の名前は太郎です。";
18: }
19: }
20:
21: class Class1
22: {
23: [STAThread]
24: static void Main(string[] args)
25: {
26: Person person = new Person();
27: Console.WriteLine( person.getName() );
28: Taro taro = new Taro();
29: Console.WriteLine( taro.getName() );
30: Console.WriteLine( taro.getTaroName() );
31: }
32: }
33: }
このソース・コードには3つのクラスが定義されている。1つは5行目から始まるPersonクラス。もう1つは13行目から始まるTaroクラス。最後は21行目から始まるClass1クラスであるが、これはただ単にMainメソッドを持つために存在しているだけなので、何の役割も果たしていない。この点については、本章のサンプルにすべて共通していることなので、Class1クラスの存在はMainメソッドを除き無視していただきたい。
さて、List 4-1のポイントは、13行目である。この「public class Taro : Person」が意味していることは、以下のとおりだ。まず、publicは、これが外部からアクセスを許されたクラスであることを示すが、このサンプルではそれほど大きな意味はない。classは、クラスを宣言するキーワードであり、宣言されるクラスの名前はTaroである。そして、その後にあるコロン(:)記号が、ほかのクラスからの継承を行うという意図を示すために記述される。具体的にどのクラスから継承されるかは、この後にクラス名を記して指定する。ここでは、Personというキーワードを書くことで、Personクラスから継承していることを示している。
Personクラスを継承したTaroクラスは、Personクラスの持つ機能とTaroクラスの持つ機能の双方を利用できる。その証拠に、28行目で作成したTaroクラスのインスタンスからは、Personクラスで定義したgetNameメソッドと、Taroクラスで定義したgetTaroNameメソッドの双方がどちらも利用できることが、29行目と30行目から分かるだろう。
実際に実行した結果はFig.4-1のとおりだ。1行目はPersonクラスのgetName()メソッドの結果、2行目はTaroクラスのgetName()メソッドの結果である。TaroクラスはPersonクラスの機能を引き継いでいるので同じ文字列が表示される。3行目は、Taroクラスで追加されたgetTaroName()の結果である。これはPersonクラスにはなく、Taroクラスにしかないメソッドである。
さて、ここで用語を説明しよう。あるクラスを継承して別のクラスを作ったとき、最初のクラスをスーパークラスという。逆に、あるクラスから別のクラスが作られたとき、作られたクラスはサブクラスという。分かりにくいかもしれないが、これもそういうものでしかないので、暗記してしまえば難しくはない。Fig.4-2を参照されたい。
4-2 機能を置き換える継承
継承は機能を増やすことだけに使用できるわけではない。機能を置き換える、という使い方も可能だ。例えば、あるクラスを継承するときに、そのクラスがもともと持っているメソッドの中身をごっそり入れ替えてしまうということもできる。入れ替えるくらいなら、継承などしなければよいと思うかもしれないが、実際にやっていると、特定の機能だけ入れ替えたいという場面に遭遇するものである。つまり、大多数の機能は継承して、一部の機能だけ入れ替えたいというニーズに対応するのが、この機能である。
ここでは、List 4-1のサンプル・ソースを少し修正して、これを実現してみよう。上記のサンプル・ソースではクラスTaroにgetTaroNameというメソッドを付けてみたが、内容的には「getName(名前をください)」といってもよいものである。そこで、クラスPersonのgetNameメソッドを、クラスTaroで継承する際に中身を置き換えるようにしてみよう。
具体的なソース・コードはList 4-2のようになる。
1: using System;
2:
3: namespace Sample002
4: {
5: public class Person
6: {
7: public string getName()
8: {
9: return "私には名前がありません。";
10: }
11: }
12:
13: public class Taro : Person
14: {
15: public new string getName()
16: {
17: return "私の名前は太郎です。";
18: }
19: }
20:
21: class Class1
22: {
23: [STAThread]
24: static void Main(string[] args)
25: {
26: Person person = new Person();
27: Console.WriteLine( person.getName() );
28: Taro taro = new Taro();
29: Console.WriteLine( taro.getName() );
30: Person someone = new Taro();
31: Console.WriteLine( someone.getName() );
32: }
33: }
34: }
ここで注目すべきは、15行目である。メソッドの定義に、何やら見慣れないような見慣れたようなキーワードが追加されていることが分かるだろう。newというキーワードはオブジェクトを作成するときにいつも使うキーワードとつづりは同じである。しかし、ここではまったく違う意味を持つのである。このnewは、「継承したクラスにある同名のメソッドの中身を置き換えますよ」ということを示すキーワードである。ここで、JavaやC++の経験がある人なら、「おや?」と思うだろう。これらの言語では、何も書かなくても既存のメソッドを置き換えることができたからだ。だが、継承を多用すると、どのクラスのどのメソッドが呼び出されるのか分かりにくくなる。偶然にも同じ名前のメソッドを作ってしまった結果、呼び出したいメソッドが呼ばれずバグになるというケースもある。そこで、C#では、既存のクラスのメソッドを置き換えることを意図する場合と、まったく新規にメソッドを定義する場合では、異なる構文での記述を強制するようになっているわけである。これにより、タイプする手間は少々増えるが、その代わりに、潜在的にバグが入り込む隙が減っているといえる。
さて、実行結果はFig.4-3のようになる。
Fig.4-3の最初の1行は、List 4-2の26〜27行目のコードによるものである。Personクラスのインスタンスを作成してメソッドを呼び出しているわけだが、当然、この結果にはTaroクラスは何の影響も及ぼさない。つまり、PersonクラスのgetNameメソッドが実行されているのである。
2行目は、28〜29行目のコードによるものである。同じgetNameメソッドを呼び出しても、今度はTaroクラスのgetNameが呼び出され、PersonクラスのgetNameは無視されていることが分かるだろう。
最後の1行は、ちょっと理解しにくいと思う。これは、30〜31行目のコードによるものであるが、ここでは、Taroクラスのインスタンスを作成している(new Taro())にもかかわらず、それへの参照をPerson型の変数に格納してしまっている。実は、あるオブジェクトへの参照を、スーパークラスのインスタンスへの参照として扱うことは、言語仕様的に認められている。この機能は、抽象度の高い機能を実装する際には不可欠なものといえ、.NET Frameworkのクラス・ライブラリの中にいくつも実例を見ることができる。例えば、ビットマップ、アイコン、カーソル、メタファイルのクラスは、すべてSystem.Drawing.Imageクラスから継承され、作られている。そのため、System.Drawing.Imageクラスを処理する機能を作成すると、それを用いて、ビットマップ、アイコン、カーソル、メタファイルのどのオブジェクトでも処理できるというわけである。
Fig.4-3の出力は、Taroクラスのインスタンスを作成したにもかかわらず、結果としてPersonクラスのgetNameメソッドが呼び出されたことを意味している。このことは、newキーワードを用いたメソッドの置き換えは万能ではないことを示している。つまり、Person型の変数を経由してアクセスすると、例え対象となるオブジェクトがTaro型であろうと、コンパイラはPerson型として処理してしまうということである。つまり、newキーワードを用いて宣言されたメソッドは、オブジェクトにアクセスするときに使用されるデータ型の違いによって、呼び出されたり、呼び出されなかったりする、というわけである。
Copyright© Digital Advantage Corp. All Rights Reserved.