連載

C#入門

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

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


抽象クラスからの継承

 ここまでのサンプルソースは、すべてPersonクラスを継承してTaroクラスを定義している。ここでの想定は、Personクラスは抽象的な人間を代表させるクラスで、Taroクラスは具体的な太郎さんについての情報を持つクラスとしたいわけである。そして、実際に使うとなれば、Personクラスを継承するのはTaroクラスだけでなく、他にHanakoクラスやJiroクラスも作りたいと考えることになる。そうすると、PersonクラスがgetName、つまり「名前をください」というメソッドを持つ意味はない。Personクラスだけでは特定の名前が決まらないからだ。しかし継承の活用という観点から言うと、PersonクラスにgetNameというメソッドがあるおかげで、Taroクラス、Hanakoクラス、Jiroクラスというクラスの種類に関係なく、getNameメソッドを呼び出すプログラムが書けるようになる。

 これを要約すれば、PersonクラスのgetNameメソッドはほしいけれど、中身はなくても構わないと言うことである。実際に、抽象的な何かを実現するクラスを作る場合には、このような機能に対する要望はよく出てくる。

 実際に記述してみると、以下のようになる。

  1: namespace ConsoleApplication5
  2: {
  3:   using System;
  4: 
  5:   public abstract class Person
  6:   {
  7:     public abstract string getName();
  8:   }
  9: 
 10:   public class Taro : Person
 11:   {
 12:     public override string getName()
 13:     {
 14:       return "私の名前は太郎です。";
 15:     }
 16:   }
 17: 
 18:   public class Class1
 19:   {
 20:     public static int Main(string[] args)
 21:     {
 22:       // 次の行はコンパイルエラーになる。
 23:       // "抽象クラスまたはインターフェイス
 24:       // 'ConsoleApplication5.Person' の
 25:       // インスタンスを作成できません。"
 26:       // Person person = new Person();
 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: }
抽象クラスの使用例

 このソースコードのポイントは、5行目と7行目に追加された“abstract”キーワードである(abstractは「抽象」の意)。12行目の“override”キーワードは1つ手前のサンプルソースと同じである。7行目では、具体的な中身のないメソッドであると言うことを示すためにこのabstractキーワードが記述されている。これにより、実際にメソッドの中身は書かなくてもエラーにならなくなる。逆に、abstractキーワードを指定した場合には、中身を書き込むとエラーになってしまう。

 5行目の“abstract”キーワードは、7行目のメソッド宣言とは違う意味を持つ。class宣言と一緒に用いられるabstractキーワードは、これが抽象クラスであると言うことを示す。抽象クラスとは、中身のない定義が含まれるクラスのことを言う。そのため、抽象クラスは継承には使えるが、このクラスから直接インスタンスを作成することはできない。実際に、26行目のコメントの中身(抽象クラスからインスタンスを生成するコード)をソースコードに書き込むと、「抽象クラスまたはインターフェイス 'ConsoleApplication5.Person' のインスタンスを作成できません。」というエラーになり、コンパイルできない。

 処理結果は以下のようになるが、基本的には、virtualとoverrideキーワードを使った場合と同じである。ただ、Personクラスのインスタンスは作成できないため、出力される行数が1つ少なくなっている。

プログラムの実行結果
Personを抽象クラスとして定義した例。実行結果自体は、前出のvirualとoverrideを使用したサンプル・プログラムのそれに等しい。ただし、抽象クラスからはインスタンスを作成できないので、前出サンプルの1行目に相当する結果はない。
  TaroクラスのgetNameメソッドの実行結果。
  Person型の変数を経由した、TaroクラスのgetNameメソッドの実行結果。

インターフェイスの実装

 実は1つ手前のサンプルソース(抽象クラス)と、ほとんど同じことができる方法がもう1つある。それは、クラスとは異なるインターフェイスという機能を利用する方法である。インターフェイスは中身のないメソッドの定義などを含むもので、機能的には抽象クラスと似ている。しかし、抽象クラスは、あくまで中身のないメソッドを含むクラスであって、クラスそのものに変わりはない。つまり、クラスに許されるあらゆる記述が可能である。そのなかには、中身のあるメソッドも含まれる。それに対して、インターフェイスは、いっさい中身を持たず、メソッドなどの宣言だけを持つ。それだけなら、中身のいっさいない抽象クラスを用いれば、インターフェイスの代用になりそうなものだが、実際はそうもいかない。なぜなら、継承する際には、スーパークラスとなれるクラスは1個に限られるが、インターフェイスはいくつでも受け入れることができるからだ。いくつもの機能や役割を1個のオブジェクトに持たせるためには、インターフェイスは不可欠な存在なのである。

 それでは、インターフェイスを用いて書き直した例を以下に示す。

  1: namespace ConsoleApplication5
  2: {
  3:   using System;
  4: 
  5:   public interface IPerson
  6:   {
  7:     string getName();
  8:   }
  9: 
 10:   public class Taro : IPerson
 11:   {
 12:     public string getName()
 13:     {
 14:       return "私の名前は太郎です。";
 15:     }
 16:   }
 17: 
 18:   public class Class1
 19:   {
 20:     public static int Main(string[] args)
 21:     {
 22:       // 次の行はコンパイルエラーになる。
 23:       // "抽象クラスまたはインターフェイス
 24:       // 'ConsoleApplication5.IPerson' の
 25:       // インスタンスを作成できません。"
 26:       // IPerson person = new IPerson();
 27:       Taro taro = new Taro();
 28:       Console.WriteLine( taro.getName() );
 29:       IPerson someone = new Taro();
 30:       Console.WriteLine( someone.getName() );
 31:       return 0;
 32:     }
 33:   }
 34: }
インターフェイスを使用するサンプル・プログラム

 このソースコードのポイントは、5行目のインターフェイスの宣言と、12行目のメソッド宣言である。インターフェイスの宣言では、classキーワードのかわりにinterfaceキーワードを用いる。しかし、その際に、abstractキーワードなどは必要ない。また7行目のメソッドの宣言にも、abstractキーワードは必要ない。インターフェイスであるというだけで、実行すべき中身がないのは明らかだからだ。また、12行目でも、特にoverrideのようなキーワードは必要とされていない。インターフェイスに含まれるメソッドを実装するのは、それ以外に選択の余地がないので(必ずメソッドを実装する必要があるので)、キーワードを添えて区別する意味がないからだ。もし、ここでgetNameメソッドがなければ、エラーになりコンパイルできない。

 当然のことながら、インターフェイスからインスタンスを生成することはできない。そのため、26行目のコメントに書かれたようなコードは書き込んでもコンパイルエラーになる。

 このソースコードでは、意図的に、1つだけキーワード名を変えてある。他のサンプルソースでは、Personと記述していたものを、この例だけは“IPerson”としている。これは、インターフェイス名には先頭にIの1文字を添えるという慣習があるためだ。けして、言語仕様の一部ではないので、付けなくてもコンパイルは通る。しかし、プログラムの分かりやすさという観点からは、この慣習に合わせて使うのがよいだろう。

 これを実行した結果は以下のようになる。

プログラムの実行結果
実行結果は前出のサンプルとまったく同様である。いずれも、Taroクラスで実装されたメソッドが実行されている。

どう継承を使っていくべきか?

 従来のオブジェクト指向プログラミングの世界では、継承の活用が重要とされてきた。再利用可能な部品から継承して必要な機能を持ったクラスを作り出すことが重要とされ、再利用可能な部品も使いやすいように細分化されていった。その結果として訪れたものは、膨大なクラスの海に溺れる混沌の世界である。いくら再利用可能な部品があろうと、どこに何があるのか分からねばないのと同じである。しかも、あるオブジェクトのあるメソッドの呼び出しが、具体的にどのクラスのメソッドを呼び出しているのか、ソースコードを見るだけでは容易に分からないという問題も起きる。

 このことから、過度に継承に依存するオブジェクト指向は、実用的ではないと言える。そのような状況を踏まえて、C#はオブジェクト指向ではなく、コンポーネント指向言語であると名乗っているようである。もちろん、オブジェクト指向言語としての機能は備えているし、それを利用して.NET Frameworkのクラス・ライブラリは構築されている。しかし、過度に複雑な継承関係を使うことは、C#的ではない。そのためにC#には、インターフェイスや、いずれ解説するデレゲートなどの機能が存在するのである。

 より望ましいC#によるクラス設計の方向性は以下のようになる。

  • インターフェイスなどの機能で足りる目的に継承は使わない
  • 継承するより、オブジェクトをメンバ変数としてクラス内に持つ
  • 継承関係で依存するクラスは少なければ少ないほど見通しがよくなる
  • 継承を使う場合でも、使いすぎないように心がける

まとめ

 今回のメインテーマである継承は、どちらかと言えば、あまり多用してほしくない機能である。また、一般的なC#プログラマであれば、自分で継承するコードを一度も書くことなくプログラムを完成させてしまうケースがないとも言い切れない。にもかかわらず、かなりの行数を取って解説した理由は、それを知らねば.NET Frameworkを活用できないという理由による。つまり、自分で書く頻度は低いかもしれないが、クラス・ライブラリの活用で重要な意味を持つ機能、と認識してもよいだろう。

 けして継承は不要な機能ではない。ただ、過度に依存すると、スパゲッティのように容易に解きほぐせないプログラムになる危険をはらんでいるだけなのである。

 さて、クラスとメソッドの宣言のやり方はだいぶ深まってきたが、そうなると次はそれらを利用して処理されるべきデータが気になるところだ。次回はC#のデータ型について解説したいと考えている。

 それでは次回もLet's See Sharp!End of Article


 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 記事ランキング

本日 月間