なぜ、多態性の理解が重要なのか、その理由

この記事では、当社の新人エンジニア研修の参考にC#を解説します。

前回は「オブジェクト指向基礎② 継承」について学びました。今回は多態性(たたいせい)について解説します。この用語を初めて聞く方も多いでしょう。多態性は、英語でポリモーフィズム(polymorphism)です。poly(色々な)morph(変化をする)ism(性質)という意味です。いかにも難しそうですが、オブジェクト指向の理解には必須の知識ですし、仕組みがわかれば大変便利で有用な技術です。しっかりと理解しましょう。

多態性を一言でざっくり説明すると、同じ名前のメソッドに色々な動作をさせるための仕組みです。

例えば、親クラスと全く同じメソッドを子クラスにも持たせて違う動作をさせたり、一つのクラスに同じ名前で引数が違うメソッドをいくつも作ったりすることができるのです。まずは、概要を見てみましょう。

1. 多態性の概要と分類

多態性を大きく分類すると、次の二種類になります。

  1. 実行時の多態性 - 子クラス毎の同名メソッドがそれぞれ異なる動作をする - メソッドのオーバーライド -
  2. コンパイル時の多態性 - 一つのクラスが持つ複数の同名メソッドが違う引数を取る - メソッドのオーバーロード

以下、それぞれを詳しく解説していきます。

1. メソッドのオーバーロード (Overload)

オーバーロードとは、「同じメソッド名で、引数の数や型が異なる複数のメソッド」 を定義することです。

例: Console.WriteLine

namespace Chap13;
using System;

public class Example05 {
    public static void run() {
        Console.WriteLine("Hello");  // 引数はstring
        Console.WriteLine('A');      // 引数はchar
        Console.WriteLine(256);      // 引数はint
        Console.WriteLine(3.14);     // 引数はdouble
    }
}

WriteLine() はオーバーロードされており、string, char, int, double など様々な型を引数として受け取れるようになっています。

新人エンジニア研修
インプットの幅を広げるオーバーロード

自前でオーバーロード

static void Method() {
    Console.WriteLine("引数なしメソッド");
}

static void Method(int i) {
    Console.WriteLine("int型の引数: " + i);
}

static void Method(double d) {
    Console.WriteLine("double型の引数: " + d);
}

引数の型・数・並び順が異なれば、同じメソッド名Methodで定義可能です。

戻り値の型が違うだけではオーバーロードにはなりません。必ず引数シグネチャが違う必要があります。


<実行結果>

abc

なぜ型パラメータが必要?

同じList<...>でも、格納する要素の型がどんな型かを指定しないとコンパイラが判断できません。誤った型を混在させることを防ぎ、型安全を確保するため、<T> で要素型を宣言します。

2. ジェネリクス(Generics)

C#では「ジェネリクス」という機能で「再利用可能な型引数つきのクラス・メソッド」を設計できます。
List<T>はその代表例で、Tには参照型でも値型(intなど)でも指定可能です。

例: 整数型リスト

namespace Chap13;
using System;
using System.Collections.Generic;

public class Example02 {
    public static void run() {
        List<int> list = new List<int>();
        list.Add(1);
        list.Add(2);
        list.Add(3);

        // 合計を求める
        Console.WriteLine(list[0] + list[1] + list[2]); // 6
    }
}

3. LinkedList<T>との使い分け

新人エンジニア研修
List<T>LinkedList<T>の関係

C#には LinkedList<T> という連結リスト構造もあります。
LinkedList<T> はリスト先頭への要素の追加・削除が高速ですが、ランダムアクセスが遅いなど特徴があります。

List<T> は内部実装が配列の動的拡張であり、「先頭要素を削除」すると配列コピーが入るので遅め、逆に末尾への追加は高速、ランダムアクセスも高速です。

namespace Chap13;
using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Example03 {
    public static void run() {
        const int N = 10000;
        
        // List<T>に1万件追加
        var list1 = new List<int>();
        for (int i = 0; i < N; i++) {
            list1.Add(i);
        }

        var stopwatch = Stopwatch.StartNew();
        // 先頭要素を連続削除
        for (int i = 0; i < N/2; i++) {
            list1.RemoveAt(0);
        }
        stopwatch.Stop();
        Console.WriteLine($"List 先頭削除 {N/2}回: {stopwatch.ElapsedMilliseconds} ms");

        // LinkedList<T>に1万件追加
        var list2 = new LinkedList<int>(list1);

        stopwatch.Restart();
        // 先頭要素を連続削除
        for (int i = 0; i < list2.Count/2; i++) {
            list2.RemoveFirst();
        }
        stopwatch.Stop();
        Console.WriteLine($"LinkedList 先頭削除 {N/2}回: {stopwatch.ElapsedMilliseconds} ms");
    }
}

実行すると、List<T>の先頭削除が遅い、LinkedList<T>は先頭削除は高速といった結果になる傾向があると思います。(環境によります)

新人エンジニア研修
List<T>LinkedList<T>の違い
  1. List<T>: ランダムアクセス高速 / 末尾追加高速 / 先頭削除・挿入遅め
  2. LinkedList<T>: 先頭・中間の削除・挿入が部分的に高速 / ランダムアクセス苦手

4. IList<T>ICollection<T>

C#のList<T>は「動的配列」の具象クラスですが、インターフェースとして IList<T>ICollection<T> を使う場合があります。これは「柔軟に差し替えられるようにしたい」ときなどに便利です。

新人エンジニア研修
C#のコレクション系クラスとインターフェース
IList<string> list = new List<string>();
// あとで new LinkedList<string>() などに付け替えやすい

Removeメソッドを安全に使えるかどうかが異なります。
一般的には「コレクションをforeach中に変更しない」が原則で、変更したいなら別の方法(例えば forループ、または .Where(...) で抽出)を推奨します。

  1. C#のジェネリクスT を指定することで、型安全かつパフォーマンス良くデータを扱える
  2. LinkedList<T>など他のコレクションもあり、用途に応じて使い分ける
  3. インターフェース (IList<T> など) で変数を宣言すれば差し替え可能
  4. foreach中の要素削除は注意(InvalidOperationException)

これで、コレクションフレームワークの基礎が学べました。
C#でもList<T>を中心に覚えれば「要素数を動的に管理する」作りができ、ロジックを組む上で強力なツールとなります。追加でDictionary<TKey,TValue>, HashSet<T>など他のコレクションも学んでみるとさらに便利さが分かるでしょう。


<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ List<T>は配列のように要素数を自由に伸縮できるListは、要素数に応じて動的に伸縮する配列である。要素の追加、削除、検索など、便利なメソッドを多く持つ。

□ ジェネリクスとは、クラスやメソッドに型パラメータを与えることで、様々な型に対応する汎用的な設計を可能にする機能である。C#では<>記号を使用して型を指定する。

□ C#のコレクション系クラスとインターフェースとは、オブジェクトの集合を効率的に操作するために設計されたクラス群を指す。C#ではSystem.CollectionsとSystem.Collections.Generic名前空間に含まれるクラスが該当する。

最後までお読みいただき、ありがとうございます。