連載

C#入門

第4回 継承とインターフェイス

(株)ピーデー
川俣 晶
2001/05/10


機能を置き換える継承

 継承の機能は、クラスに新しい機能を増やすことばかりではない。機能を置き換える、という使い方も可能だ。例えば、あるクラスを継承するときに、そのクラスが元々持っているメソッドの中身をごっそり入れ替えてしまうということもできる。入れ替えるぐらいなら、継承などしなければよいと思うかもしれないが、実際にやっていると、特定の機能だけ入れ替えたいという場面に遭遇するものである。つまり、大多数の機能は継承して、一部の機能だけ入れ替えたいというニーズに対応するのがこの機能である。

 ここでは上記のサンプルソースを少しだけ修正して、これを実現してみよう。上記のサンプルソースでは、クラスTaroにgetTaroNameというメソッドを付けてみたが、内容的にはgetName(「名前をください」)と言ってもよいものである。そこで、クラスPersonのgetNameメソッドを、クラスTaroで継承する際に、中身を置き換えるようにしてみよう。

 具体的なソースコードは以下のようになる。

  1: namespace ConsoleApplication5
  2: {
  3:   using System;
  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:   public class Class1
 22:   {
 23:     public static int Main(string[] args)
 24:     {
 25:       Person person = new Person();
 26:       Console.WriteLine( person.getName() );
 27:       Taro taro = new Taro();
 28:       Console.WriteLine( taro.getName() );
 29:       Person someone = new Taro();
 30:       Console.WriteLine( someone.getName() );
 31:       return 0;
 32:     }
 33:   }
 34: }
クラスの継承によって、機能の置き換えを実現するサンプル・プログラム

 ここで注目すべきは、15行目である。メソッドの定義に、何やら見慣れないような、見慣れたようなキーワードが追加されていることが分かるだろう。“new”というキーワードは、オブジェクトを作成するときにいつも使うキーワードと綴りは同じである。しかし、ここではまったく違う意味を持つのである。この“new”は、「継承したクラスにある同名のメソッドの中身を置き換えますよ」ということを示すキーワードである。ここで、JavaC++の経験がある人なら、「おや?」と思うだろう。これらの言語では、何も書かなくても、すでにあるメソッドを置き換えることができたからだ。だが、継承を多用すると、どのクラスのどのメソッドが呼び出されるのか分かりにくくなる。偶然にも同じ名前のメソッドを作ってしまった結果、呼び出したいメソッドが呼ばれずバグになるというケースもある。そこでC#では、既存のクラスのメソッドを置き換えることを意図する場合と、まったく新規にメソッドを定義する場合で違う構文で記述することを強制するようになっているわけである。これにより、タイプする手間は少々増えるが、その代わりに、潜在的にバグが入り込む隙が減っていると言える。これが、C#がJavaやC++よりも先進的と言われる理由の1つである。

 さて、このサンプル・プログラムの実行結果は以下のようになる。

プログラムの実行結果
C#では、継承するクラスと同名のメソッドを置き換えるときには、それを明示的に示す“new”キーワードを指定する。
  PersonクラスのgetNameメソッドの実行結果。
  Taroクラスで置き換えられたgetNameメソッドの実行結果。
  Taroクラスのインスタンスを作成し、それへの参照をPerson型の変数に格納して、そのgetNameメソッドを実行した結果。この場合には、TaroクラスのgetNameメソッドではなく、PersonクラスのgetNameメソッドが実行されていることが分かる。

 結果の最初の1行目は、ソースの25〜26行目のコードによるものである。Personクラスのインスタンスを作成してメソッドを呼び出しているわけだが、当然、この結果にはTaroクラスは何の影響も及ぼさない。つまり、PersonクラスのgetNameメソッドが実行されている。結果の2行目は、ソースの27〜28行目のコードによるものである。同じgetNameメソッドを呼び出しても、今度がTaroクラスのgetNameが呼び出され、PersonクラスのgetNameは無視されていることが分かるだろう。

 結果の最後の1行は、ちょっと理解しにくいと思う。これは、ソースの29〜30行目のコードによるものであるが、ここでは、Taroクラスのインスタンスを作成している(“new Taro( )”)にもかかわらず、それへの参照をPerson型の変数に格納してしまっている。実は、あるオブジェクトへの参照を、スーパークラスのインスタンスへの参照として扱うことは、言語仕様的に認められている。この機能は、抽象度の高い機能を実装する際には不可欠なものと言え、.NET Frameworkのクラス・ライブラリのなかにいくつも実例を見ることができる。例えば、ビットマップ、アイコン、カーソル、メタファイルのクラスは、すべてImageクラスから継承され、作られている。そのため、Imageクラスを処理する機能を作成すると、それを用いて、ビットマップ、アイコン、カーソル、メタファイルのどのオブジェクトでも処理できるという寸法である。

 さて、この結果は、Taroクラスのインスタンスを作成したにもかかわらず、結果としてPersonクラスのgetNameメソッドが呼び出されたことを意味している。このことは、newキーワードを用いたメソッドの置き換えは万能ではないことを示している。つまり、Person型の変数を経由してアクセスすると、たとえ対象となるオブジェクトがTaro型であろうと、コンパイラはPerson型として処理してしまうと言うことである。そのため、newキーワードを用いて宣言されたメソッドは、オブジェクトにアクセスするときに使用されるデータ型の違いによって、呼び出されたり、呼び出されなかったりする、と言うわけである。

より根本的に機能を置き換える継承

 実際にプログラムを作成していると、newキーワードによるメソッドの置き換えでは生ぬるいと思うようになってくる。せっかく置き換えても、スーパークラスのデータ型で宣言された変数を通すと利用されない、というような制限があっては使いにくいことこの上ない。置き換えた以上は、どんなデータ型を通じて利用しようと、置き換えたメソッドが呼び出されてほしい! と思うのは当然の成り行きだろう。

 そのためには、newキーワードで置き換えるのではなく、virtualキーワードとoverrideキーワードを用いて、別の方法で置き換える機能が用意されている。

 実際に記述した例は以下のとおりである。

  1: namespace ConsoleApplication5
  2: {
  3:   using System;
  4: 
  5:   public class Person
  6:   {
  7:     public virtual string getName()
  8:     {
  9:       return "私には名前がありません。";
 10:     }
 11:   }
 12: 
 13:   public class Taro : Person
 14:   {
 15:     public override string getName()
 16:     {
 17:       return "私の名前は太郎です。";
 18:     }
 19:   }
 20: 
 21:   public class Class1
 22:   {
 23:     public static int Main(string[] args)
 24:     {
 25:       Person person = new Person();
 26:       Console.WriteLine( person.getName() );
 27:       Taro taro = new Taro();
 28:       Console.WriteLine( taro.getName() );
 29:       Person someone = new Taro();
 30:       Console.WriteLine( someone.getName() );
 31:       return 0;
 32:     }
 33:   }
 34: }
virtualキーワードとoverrideキーワードを使って、より完全に機能の置き換えを行うサンプル・プログラム

 このサンプルソースのポイントは、7行目と15行目のメソッド宣言にある。7行目のメソッド宣言には、“virtual”というキーワードが増えている。そして、15行目にはnewではなく、“override”というキーワードが書き込まれている。違いとしてはそれだけである。ところがこれを実行すると結果が違う。

プログラムの実行結果
virtualキーワードとoverrideキーワードを使用すれば、スーパークラスのデータ型変数を通しても、置き換えた後のメソッドが実行される。
  Personクラスのget Nameメソッドの実行結果。
  Taroクラスで置き換えられたgetNameメソッドの実行結果。
  このようにvirtual/overrideキーワードを使用すれば、たとえスーパークラスのデータ型変数を通しても、Taroクラスで置き換えたgetNameメソッドが実行される。

 つまり、実行結果のの結果が、1つ前のサンプルソースではPersonクラスのgetNameの結果であったものが、この例ではTaroクラスの結果となっている。つまり、virtualとoverrideというキーワードを用いると、オブジェクトにアクセスする変数の型とは関係なく、置き換えたメソッドが呼び出されるようになったわけである。

 ここでポイントになるのは、置き換える側だけでなく、置き換えられる側にも常にvirtualというキーワードを付けねばならない点だ。この方法で置き換えられるかもしれないメソッドには、あらかじめvirtualキーワードを付けておく必要がある。それをやっておかないと、overrideキーワードを付けることができない。

 さて、ここで気になることは、メソッドを置き換える方法に、どうして2種類の方法が存在するのか、と言うことだ。その理由は処理効率にある。virtualキーワードによる置き換えは、強力で使いやすいが、いったいどんなメソッドが呼び出されるか、実行してみないと分からないと言うことも多い。それに対して、newキーワードによる置き換えは、変数のデータ型によって呼び出されるメソッドが分かるため、コンパイラにも容易に呼び出し先が推測可能である。これにより、さまざまな最適化機能を適用することができ、処理速度を上げることが可能になるのである。そのため、特に必要とされない限り、virtualキーワードは使わないのが普通である。しかし、機能的な要請によりvirtualキーワードが不可欠というケースも存在するので、virtualキーワードは不要というわけでもないのである。


 INDEX
  C#入門 第4回 継承とインターフェイス
   1.機能を追加する継承
 2.機能を置き換える継承
   3. 抽象クラスからの継承
 
「C#入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間