オブジェクト指向言語の利点

前回,オブジェクトというのは大雑把には「データと,それに関連する関数」をまとめたものであり,「オブジェクトの作成やオブジェクトに対するメソッドを呼び出しを基本としてプログラムを構成する」のが(C#などにおける)オブジェクト指向プログラミングであると述べた.そして,実際に例や課題を通してオブジェクト指向プログラミングを体験してもらった.

では,そのようなアプローチにどのような利点があるだろうか.ひとつはカプセル化である.アクセス指定子を適切に設定することにより,メソッドやデータをプログラムの他のパートから隠蔽することができる.また,クラスを利用することによって,「データ」と「それに関連する関数」という明らかに関連の深いものを,自然に局所にまとめることが可能となる.これにより,クラスを開発する側はプログラムの他の部分を気にすることなく内部実装を変更することができるようになるし,クラスやそのクラスのオブジェクトを使う側は内部の詳細を気にすることなしにそのクラスやそのオブジェクトを利用することができる.こうした,部品毎に分けてプログラムを開発できることはモジュラリティと呼ばれ,実用的なプログラミング言語が兼ね備えているべき能力の一つである.

Note

クラスやオブジェクトが(オブジェクト指向)言語のモジュラリティを支える全てではない.実際に,クラスではうまくまとめきれない関心事をまとめるためにアスペクト指向言語などの言語が提案されている.また,後半学ぶF#のような(典型的な)関数プログラミング言語では得意とする抽象化が異なる(「visitorパターン」や「expression problem」について調べてみよ).

また継承によるコードの再利用性や,その継承を利用したポリモーフィズム(「多態性」や「多相性」など.同じコードで異なる型のデータを扱えること)も C#のようなクラスべースのオブジェクト指向言語の利点である.

今回の演習では,それを詳しくみることにする.

継承(inheritance)

継承は,元となるクラスB(基底クラス,親クラスなど)からアクセスレベルがprivateでない(protected,internalや publicなど)なフィールドやメソッド等をすべて受け継いだクラスD(派生クラス,子クラスなど)を作成する機能である.その際において以下が可能となる.

Note

仮想メソッドはインスタンスによってどのような動作をするかが異なるため,たとえば,

class C { public virtual void f() { ... } }
class P { 
  public static void C g() { ... } 
  public static void h() { C x = g(); x.f();  } 
}

というコードにおいて,x.f()で呼ばれるメソッドは最初の行で定義したものとは限らない.たとえば,どこかでCの派生クラスでf()をオーバライドするようなものDが作成され,g()が返すのはDのインスタンスかもしれない.

その意味では,クラスは,アクセス制御を無視したとしても,たとえばC言語における構造体の定義とその型の引数を第一引数としてとるような関数をまとめたものとは異なる.典型的な実装においては,インスタンスはフィールドの値をまとめたもののほか仮想メソッドの実体への参照を持つ.

例:電話機

例を通して継承を説明しよう.以下のPhoneクラスを考える.

class Phone 
{
    // 電話番号
    protected string number; 

    public Phone(string n) 
    {
       number = n; 
    }
    public void Call() 
    {
       Console.WriteLine("Called!");
    }
    // 仮想メソッド
    public virtual void Info() 
    {
       Console.WriteLine("Phone: " + number);
    }
}

これを元に,電話番号の他にメールアドレスも持つ MobilePhone クラスを作成してみる. C#では継承は,クラス定義の際にクラス名の横に: 基底クラスとすることで行える.

// MobilePhoneはPhoneの派生クラス
class MobilePhone : Phone
{
    protected string email; 

    // ": base(n)" の部分は基底クラスのコンストラクタを呼ぶ.
    // ": base(n)"の省略は ": base()" と同じ意味である.
    // そのため,もしここで当該部分を省略するとエラーになる.
    // (Phone()というコンストラクタは定義されていないため.)
    public MobilePhone(string n, string e) : base(n) 
    {
       email = e; 
    }

    // MobilePhoneはPhoneに含まれないメソッドを含んでもよい.
    public void Mail() 
    {
       Console.WriteLine("Mailed!");
    }   

    // 仮想メソッドをオーバライド
    public override void Info() 
    {
       // 基底クラスの(すわなち,オーバライドされる前の) Info() を呼ぶ
       base.Info();

       Console.WriteLine("EMail: " + email); 
    }       
}

以下のように,MobilePhoneのインスタンスに対しては基底クラスのメソッドも,MobilePhoneのメソッドも呼び出すことができる.

MobilePhone mp = new MobilePhone("090-0000-0000", "nobody@example.com"); 
mp.Call(); 
mp.Mail(); 
mp.Info(); 

上を(適当なMainメソッドを用意するなどして)実行すると,以下となる.

Called!
Mailed! 
Phone: 090-0000-0000
EMail: nobody@example.com 

前述したように,MobilePhoneのインスタンスはPhoneのインスタンスでもある.そのため,以下のようなコードも記述できる.

Phone p = new MobilePhone("090-0000-0000", "nobody@example.com"); 
p.Call(); 
// p.Mail(); はエラーになる.MailはPhoneのメソッドではないので
p.Info(); 

実行結果は以下となる.

Called!
Phone: 090-0000-0000
EMail: nobody@example.com 

ここで注目すべきことは二つある.一つは

Phone p = ... 
p.Call(); 
p.Info(); 

というコードはpに代入されるものがPhoneのインスタンスであろうがMobilePhoneのインスタンスであろうが動作する.このように同じコードで異なる型のデータを扱えることをポリモーフィズムと言う.

もう一つはInfo()は仮想メソッドであるのでp.Info()の挙動は実際にpに代入されるインスタンスにより変わりうる.現時点ではPhoneの派生クラスはMobilePhoneしかないが,別の派生クラスを定義したりMobilePhoneをさらに派生したりすることも可能である.

実際に以下のコードを実行することで動作を確認してみよう.

using System;

class Phone 
{
    protected string number; 

    public Phone(string n) 
    {
       number = n; 
    }
    public void Call() 
    {
       Console.WriteLine("Called!");
    }
    public virtual void Info() 
    {
       Console.WriteLine("Phone: " + number);
    }
}
class MobilePhone : Phone 
{
    protected string email; 

    public MobilePhone(string n, string e) : base(n) 
    {
       email = e; 
    }

    public void Mail() 
    {
       Console.WriteLine("Mailed!");
    }   

    public override void Info() 
    {
       base.Info();

       Console.WriteLine("EMail: " + email); 
    }       
}
class Program 
{
    static void CallAndInfo(Phone p) 
    {
       p.Call(); 
       p.Info(); 
    }

    static void Main(string[] args) 
    {
       CallAndInfo( new Phone("090-0000-0000") ); 
       Console.WriteLine("--------------------");
       CallAndInfo( new MobilePhone("090-0000-0000", "nobody@example.com" ));
    }
}

実行結果は以下となる.

Called!
Phone: 090-0000-0000
--------------------
Called!
Phone: 090-0000-0000
EMail: nobody@example.com

object

C#にはobjectという,全ての型の祖先となる型が存在する.objectの興味深い仮想メソッドの一つにToString()がある.

class Phone
{
    // ...
    public override string ToString() 
    {
        return "Phone {number = " + number + "}";  
    }
}
class MobilePhone 
{
    // ...
    public override string ToString() 
    {
        return "MobilePhone {number = " + number + ", email = " + email + "}";  
    }
}
class Program 
{
    public static void Main()
    {
        Console.WriteLine(new Phone("090-0000-0000"));
        // Phone {number = 090-0000-0000 }
        Console.WriteLine(new MobilePhone("090-0000-0000", "nobody@example.com"));
        // MobilePhone {number = 090-0000-0000, email = nobody@example.com}
    }
}

以上のコードがユーザ定義型のオブジェクトに対しても動作するのは,Console.WriteLine(object)が中でToString()を呼んでいるためである.

オーバロード(overload)

C#ではクラスは引数の型や数の異なる複数の同名のメソッドやコンストラクタを持つ (オーバロード) ことができる.たとえば,前回Counterクラスに以下のコンストラクタやメソッドを追加することができる.

public Counter() : this(0) { }
public Counter( Counter c ) : this(c.GetValue()) { } 

public void Inc(int c) 
{
    count += c; 
}

オーバロードも(広義の)ポリモーフィズムを実現する言語機能の一つである.

たとえば,+演算子は多数オーバロードされていて,たとえばそれにより通常の加算だけでなく,文字列同士の連接やToString()を経由した文字列の連接が行える.

Console.WriteLine("Hello" + " " + "World!"); // Hello World!
int i = 0; 
Console.WriteLine("i = " + i);               // i = 0 
double d = 2.3; 
Console.WriteLine(d + ".");                  // 2.3.

最初のWriteLineでは,引数としてstring2つを受けとる+演算子(2つとも)が使用されている.二個目の例では,第一引数としてstring,第二引数としてobjectを受けとる+演算子が,最後の例では,第一引数としてobject,第二引数としてstringを受けとる+演算子が使用されている.

Note

継承によるポリモーフィズムは部分型ポリモーフィズム(subtype polymorphism)と呼ばれるものの一種であるが,オーバロードによるポリモーフィズムはアドホックポリモーフィズム(ad-hoc polymorphism)と呼ばれるものの一種である.

キャスト

C#では,数値型をより広い型や浮動小数点数型に変換したり,派生クラスのインスタンスを基底クラスのインスタンスに変換したりするのに陽な操作は不要である.

byte b = 0xbe; 
int i = b; 
double d = i; 

Phone p = new MobilePhone("090-0000-0000", "nobody@example.com" ); 

一方で,その逆をするにはキャストが必要である.

() 

デフォルトだと,整数型をより小さい整数型にキャストするときには上位ビットが切り詰められ,同じサイズの整数型にキャストするときは同じビット表現がキャスト先の型として解釈されることになる.また,(byte) 0x1ffなどのように,定数がキャストされるときにキャスト先の型の収まらない場合はコンパイルエラーとなる.

int  i = 0x1ff;           
byte b = 0xff;
Console.WriteLine((byte)  i); // 255
Console.WriteLine((sbyte) b); // -1

また,クラスについてはキャスト元がキャスト先の型を持ちえない場合は例外(後の回で詳しく紹介する.捕捉しなければ実行時エラーとなる)が発生する.

object o = new Phone("090-0000-0000");
// 以下はキャスト成功
((Phone) o).Call(); // Called! 
// 以下は例外発生
((MobilePhone) o).Mail(); 

なお,暗黙の型変換が行える場合にもキャストを用いて陽に行ってもよい.

int  i = 0; 
long l = (long) i; 

練習問題

Caution

練習問題の解答は提出しない.講義中にトライしわからない点を質問するのに使うとよい.

上述のMobilePhoneクラスをさらに継承し,以下のコンストラクタ・メソッドを持つクラスSmartPhoneを作成しなさい.

コンストラクタ・メソッド 実装すべき挙動
public SmartPhone(string n, string e, string o) 電話番号n,メールアドレスe,持ち主oを受けとるコンストラクタ.
public void Touch() 単に標準出力にTouched!と出力する.
public override void Info() 電話番号,メールアドレスに加え,持ち主の情報も標準出力に出力する.

また,MobilePhoneの例にならい,適切なMainメソッドをProgramクラスに実装し動作確認せよ.特に,SmartPhoneのインスタンスが,MobilePhoneのインスタンスとしてもPhoneのインスタンスとしても使えることがわかるようにせよ.

Hint

上記の仕様は他にフィールドをもってはいけないとは書いていない.上記を達成するためにはどんなフィールドが必要になるかを考えてみよう.