C#でクラスを作ろう(11)/ポリモーフィズム(多態性)

3月 17, 2020

このシリーズについて

C言語やC++言語などを学んではいるけどクラスをあまり作ったことが無い、という方を対象にしています。
このシリーズでは、C#でクラスを作るための基本的な構文を解説しています。C++やJavaなどと共通している概念も多いですが、サンプルコードは基本的にC#で解説します。ところどころ、C++特有の概念を解説することもあります。

クラスを継承して仮想メソッドを定義すると何がうれしいのか。それは、プログラムコードは同じでも、オブジェクトの実体に合わせて適切にメソッドの振る舞いを変化させられるというポリモーフィズム(多態性)を実現できるところにあります。

スポンサーリンク

メソッドの振る舞いを拡張する

//親クラス
class Person
{
    //人物を表わすクラス
    //中略...
    public virtual void SelfIntroduction()
    {
        //自己紹介をする
        Console.WriteLine($"私は{this.Name}です");
    }
}
  
 
//メインロジック
Person ps = new Person();
//ps.Nameなどのプロパティをここで設定
 
if (firstMeeting)  //もし初対面なら
{
    for (int i = 0; i < partnerCount; i++)  //相手側の人数
    {
        //相手側1人1人に自己紹介をする
        ps.SelfIntroduction();
    }
}

いつも出てくるPersonクラスの例です。とりあえずPersonクラスが定義されていて、Person型変数psがあるとします。

メインロジックでは「初対面の相手と会う場合は相手側の人数の回数だけ自己紹介をする」というコードを記述しています。

ここで重要なのは、メインロジックを書いているときはSelfIntroductionメソッドの中身を気にしなくていいということです。中身がどうであれ、SelfIntroductionメソッドは「自己紹介をする」という動作であるということだけ知っていれば十分です。

メインロジックを書いている側(=クラスの外側)は、クラスの内部の詳細を知る必要はありません。このように、クラスの外側と内側で関心事を分離し、プログラミング中の脳の消費リソースを抑えるようにすることで、ミスやバグを減らすことができます。

さて、Personクラスを継承したStudentクラスを作り、仮想メソッドSelfIntroductionをオーバーライドします。

//子クラス
class Student : Person
{
    //小学生を表わすクラス。人物を表わす親クラスPersonを継承している
    //中略...
    public override void SelfIntroduction()
    {
        //自己紹介をする
        Console.WriteLine($"私は{this.Name}です");
        Console.WriteLine($"小学{this.Grade}年生です");
    }
}

オブジェクトの実体がStudentであるときは、SelfIntroductionメソッドはこのオーバーライドされたStudentクラスのバージョンのほうが呼ばれます。

仮想メソッドのオーバーライドは、上記のように親クラスの動作を完全に書き換えることができます。しかしこの例では、「私は○山×郎です」と自己紹介する部分は親クラスと同じです。そのような場合、次のような書き方をすることもできます。

public override void SelfIntroduction()
{
    //自己紹介をする
    base.SelfIntroduction();  //親クラスのバージョンのSelfIntroductionを呼ぶ
    Console.WriteLine($"小学{this.Grade}年生です");
}

baseキーワードを使うことによって、親クラスのバージョンの同名メソッドを呼ぶことができます。このような形でオーバーライドするということの意味は、「親クラスの動作を行ったあと、このStudentクラス特有の動作を行う」ということです。これは、子クラス側で親クラスのメソッドの振る舞いを拡張していると言えるでしょう。

しかしこれには注意が必要です。メソッドによっては、子クラス独自の機能を書いた後に最後にbase.XXX()とすべき場合もあります。どのタイミングでbaseバージョンのメソッドを呼ぶべきかは、親クラスを作った人に聞くか、親クラスの仕様書を読むしかありません。1人で全部作っている場合はあまり気になりませんが、親クラス側と子クラス側でお互いのことをよく知っていなければならないということに注意しましょう。

とにかくこのようにして仮想メソッドが子クラス側でオーバーライドされました。ではメインロジックはどうなるか、見てみましょう。

//メインロジック
Student st = new Student();
//st.Nameなどのプロパティをここで設定
Person ps = st;  //アップキャストする
 
if (firstMeeting)  //もし初対面なら
{
    for (int i = 0; i < partnerCount; i++)  //相手側の人数
    {
        //相手側1人1人に自己紹介をする
        ps.SelfIntroduction();
    }
}

Student型のインスタンスstを生成したりプロパティを設定するところは多少の変更が必要ですが、Person型変数psにアップキャストしたあとは、メインロジックは全く同じです。にもかかわらず、ps.SelfIntroduction()のところではStudent型バージョンのSelfIntroductionメソッドが呼ばれます。

これが仮想メソッドの強力な一面です。メインロジックを全く変えることなく、psの中に何が入っているかによって、元のバージョンのメソッドか拡張されたバージョンのメソッドかが自動的に選択されて、振る舞いを変化させることができます。多様に振る舞い(状態)が変化するこの性質を、ポリモーフィズム(多態性)と呼びます。

ポリモーフィズム

ポリモーフィズムは、プログラミング言語の種類に拠らない概念です。そのため、学術的な文献を紐解くと、上記のようなC#でのポリモーフィズムとは微妙に違う解説が書かれていることがあります。また、他の言語では、全然別の機能をポリモーフィズムと呼ぶ場合もあります。そのあたりは自分の使っている言語に合わせて適切に解釈していきましょう。「学術的な本当の意味はこうだ」というところにはあまり固執しないほうがいいと思います。

複数の子クラスを親クラスのリストでまとめる

//何らかの共通部分
abstract class FileCopyItem
{
    public string SourceFilePath { get; set; }  //コピー元のファイルパス
    public abstract void DoFileCopy();  //抽象メソッド
}
 
//ローカルコピーを担うクラス
class LocalFileCopyItem : FileCopyItem
{
    public string DestinationFilePath { get; set; }  //コピー先のファイルパス
    public override void DoFileCopy()
    {
        //ファイルコピーの実行
        File.Copy(this.SourceFilePath, this.DestinationFilePath);
    }
}
 
//FTP送信を担うクラス
class FtpFileCopyItem : FileCopyItem
{
    public string DestinationURL { get; set; }       //コピー先のURL
    public string FtpUserName { get; set; }          //FTP接続のユーザー名
    public string FtpPassword { get; set; }          //FTP接続のパスワード
    public override void DoFileCopy()
    {
        //FTP送信の実行
        WebClient wc = new WebClient();
        wc.Credentials = new NetworkCredential(this.FtpUserName, this.FtpPassword);
        wc.UploadFile(this.DestinationURL, this.SourceFilePath);
    }
}

似た機能を持つクラスの共通部分を抜き出して親クラスとした場合の例です。抽象メソッドDoFileCopyは、それぞれの子クラスで適切にオーバーライドされています。

この抽象メソッドDoFileCopyがポリモーフィズムによって強力に働く例を紹介します。

//ファイルコピー元の配列
string[] srcPath = new string[]
{
    "file1.txt",
    "file2.txt",
    "file3.txt",
    "file4.txt"
};
//ファイルコピー先の配列
string[] destPath = new string[]
{
    @"C:\",
    @"C:\SomeFolder\",
    @"ftp://example.com/htdocs/",
    @"ftp://example.com/htdocs/SomeDirectory/"
};
 
//4つのFileCopyItemを生成する
FileCopyItem[] itemList = new FileCopyItem[4];
for (int i = 0; i < 4; i++)
{
    if (destPath[i].Contains("ftp://"))
    {
        var item = new FtpFileCopyItem();  //FtpFileCopyItemのインスタンス
        item.SourceFilePath = srcPath[i];
        item.DestinationURL = destPath[i];
        itemList[i] = item;  //暗黙のアップキャストが行われる
    }
    else
    {
        var item = new LocalFileCopyItem();  //LocalFileCopyItemのインスタンス
        item.SourceFilePath = srcPath[i];
        item.DestinationFilePath = destPath[i];
        itemList[i] = item;  //暗黙のアップキャストが行われる
    }
}
 
//ループを回して4つのファイルコピーを実行
for (int i = 0; i < 4; i++)
{
    itemList[i].DoFileCopy();
}

4つのファイルコピー元とファイルコピー先を表わす配列があったとします。4つそれぞれに対してFileCopyItemオブジェクトを生成するわけですが、コピー先に"ftp://"の文字が含まれていたときはFtpFileCopyItemオブジェクトを生成し、そうでないときはLocalFileCopyItemオブジェクトを生成します。

どちらの子クラスも抽象クラスFileCopyItemを継承しているので、FileCopyItem型の配列itemListに代入するときに暗黙のアップキャストが行われます。この配列itemListの中にはいろいろな実体が入っていますが、プログラムコードで表現される限りにおいては、抽象基本クラスFileCopyItemの配列でしかありません。

そしてこの配列をループで回してファイルコピーを実行するわけですが、単純にitemList[i].DoFileCopy();と書くだけでOKです。この簡単なコードだけで、ローカルコピーを行うのかFTP送信を行うのかが自動的に振り分けられます。

ループというのはプログラムの至る所で現れますが、ループで回す配列内の各要素についてそれぞれ振る舞いを変化させたい場合でも、ポリモーフィズムをうまく使えばループ内のコードは最小限になります。

もしポリモーフィズムを使わない場合は、ループの中で条件分岐を行うハメになり、ループの中のコードが大きく膨れ上がってしまいます。そうなると「ループの中」と「ループを扱うメインロジック側」で関心事が複雑に絡み合ってしまうので、ミスやバグの原因となりかねません。

この例ではサンプルコードを簡単にするために、コピー元配列とコピー先配列をコード中に固定値で書いていますが、実際のプログラミングではそれらをユーザーからの入力から取得したり、データベースから取得したりすることになります。そしてその個数も4個とか決まっておらず、可変です。そのように実行時にしか何が行われるか不明な場合でも、ポリモーフィズムによって自動的にクラスが規定した適切な振る舞いが行われるようになります。

オブジェクトが自律的に振る舞うオブジェクト指向

仮想メソッドを使ったポリモーフィズムという手法は、別に何もこんなことをしなくてもifとかswitchで分岐させれば同等の機能の実現は可能です。

それなのにこのようなポリモーフィズムという手法が近年のプログラミング言語に取り入れられているのは、オブジェクト指向という考え方に拠るところが大きいです。

プログラミングにおけるメインロジックは、「自己紹介をする」「ファイルをコピーする」というような日本語で表現できる動作をそのまま「SelfIntroduction」とか「DoFileCopy」とかで簡潔に書き下すことができれば、ミスやバグを減らせます。

日本語で表現できるくらいの単位の動作を組み合わせて書き下していくことこそが、「プログラミングを使ってやりたいこと」の本質です。条件分岐とか文字列の一致検査というようなプログラミング言語特有のテクニック的なことは、できれば頭の中から追いやりたいものです。

そのように関心事を分離するためには、各オブジェクトが自律的に振る舞うようになっていなければなりません。自律的に振る舞うオブジェクトさえあれば、あとはほとんど日本語で表現できる動作をそのままメインロジックに書けばいいだけになります。このような考え方をオブジェクト指向と呼びます。

学術的な文献に書かれている「オブジェクト指向」は宗教論争に発展するほど百家争鳴ですが、あまり頭でっかちにならず、「クラスを使うというよりは、クラスが振る舞いを自分で決める」という感じになるように仮想メソッドなどを使いながらプログラムを書いてみてください。最初は構文や仕様にとまどうかもしれませんが、慣れてくると「ああ、このやり方のほうがミスが少なくてラクだ」と思うはずです。きっと。

スポンサーリンク