連載:C# 2.0入門

第2回 ジェネリック

株式会社ピーデー 川俣 晶
2007/06/29
Page1 Page2 Page3 Page4

ジェネリックとは何か?

 前回は、C# 2.0がC# 1.xとはまったく異質な言語であることを示したが、今回のテーマである「ジェネリック(Generic)」にはそのような大胆さはない。あくまで、C# 1.xプログラミングを行っているときに日常的に感じる不満を解消する新機能として理解することができる。

 それではC# 1.xプログラミングの不満とは何だろうか。

 具体的にはいろいろあると思うが、恐らく以下の点については、ほとんどのC# 1.xプログラマーが不満と感じているだろう。

  • コレクションの要素にアクセスする際、キャストが要求される

 具体的にいえば、以下のようなことである。

using System;
using System.Collections;

class Program
{
  static void Main(string[] args)
  {
    ArrayList list = new ArrayList();
    list.Add("Hello!");

    Console.WriteLine(((string)list[0]).ToUpper()); // キャスト必要
    // 出力:HELLO!
  }
}
リスト1 C# 1.xでコレクションを使用した例

 このプログラムでは、可変サイズの1次元リスト・コレクションを実現するArrayListクラスに文字列を入れてから取り出している。ここで、何事もなく文字列をコレクションに入れることができるのに、取り出す際には

(string)list[0]

というキャストが必要とされている。そして、このキャストはいくつもの面倒を引き連れて来る。

 まず、書かねばならない文字が増えて面倒である。本質的に必要のない文字がソース・コード上に増えれば、それだけ可読性も悪くなり、メンテナンス性も落ちる。だが、これはやって来る災厄の中では序の口にすぎない。

 キャストを用いると、それだけ実行速度が低下する。キャストは重い処理なのである。ちなみに、C#ではキャストよりもas演算子を使う方が高速なのだが、as演算子は変換できないときにnullという値になるため、そのまま例外を投げてくれるキャストと比較していまひとつ扱いにくい。

 そして、キャストを用いると、型変換の妥当性のチェックをコンパイル時に行うことができなくなり、それは実行時に遅延される。軽量言語の信奉者の中には、「型は実行時にチェックされるから問題ない」と主張する者もいるが、本質的にこれは何が問題であるかを見失った発言といえる。型のチェックが実行時まで遅延されるということは、チェックが「遅延」されるという問題ではなく、型のチェックが「個別の値」に対して行われるという問題なのである。

 より具体的に見てみよう。キャストを用いると、まず以下のような状況が発生する。

  • テスト実行の際、すべての行が実行されるとは限らない。つまり、実行されなかった行に書かれたキャストの妥当性はチェックされない

 これは、テスト支援ツールなどを用いて、カバレッジ(網羅)率100%を目指せば解消できるかもしれない。

 しかし、以下のような問題は解消できない。

  • たまたまテスト実行の際に与えられた値に対してキャストが妥当であることは確認されるが、実行時にあり得るすべての値に対して、キャストが妥当であるかはチェックされていない

 このような問題は、小さなプログラムでもバグを生むことがあるし、規模の大きなプログラムでは相当のリスクになる。例えば、コレクションに思いも寄らない型の値が入っていて、うまくいくはずのキャストが失敗する……といった状況は、仕様変更の多いプロジェクトや、意思疎通が不十分な開発プロジェクトでは珍しくもないだろう。

 このような問題は、できるだけ多くの型チェックをコンパイル時に行うことで回避できるが、C# 1.xでは限界がある。

 もちろん、それが不可能と思うならば、大きな不満にはならないだろう。しかし、歴代のマイクロソフト社製開発ツールを使ってきた技術者であれば、C#よりも古くから使われてきたC++ではそれが可能であることを知っているはずである。C++の「template」と呼ばれる機能は、まさに汎用のコレクション・クラスからキャストを追放する手段として使用できたのだ。つまり、不可能ではないことが明らかであるがゆえに、この問題はC# 1.xでの大きな不満としてそこにあったと思う。

特集:C#&VBジェネリック超入門

 そして、C# 2.0はその問題を解消する新機能を追加した。ただし、それはtemplate機能ではなく、ジェネリックという似て非なる機能である。

 この機能を使うと、リスト1からキャストを追放できることを確認するサンプル・コードを作成した(リスト2)。

using System;
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    List<string> list = new List<string>();
    list.Add("Hello!");

    Console.WriteLine(list[0].ToUpper()); // キャスト不要
    // 出力:HELLO!
  }
}
リスト2 C# 2.0でコレクションを使用した例

 このとおり、変数listを宣言する際に、

List<string>

のように、stringという型を明示することにより(記述方法の詳細は後述)、ソース・コード上からキャストは消滅した。

新しいコレクションの紹介

 リスト1で使用されているListクラスは、従来のArrayListクラスに相当するコレクション・クラスである。ちなみに、Listクラスが属する名前空間は「System.Collections」ではなく「System.Collections.Generic」である。

 このことから分かるとおり、ジェネリックなコレクションを使ううえでは、「名前が変わっている」「名前空間が変わっている」という2つのポイントを踏まえねばならない。

 このうち、名前空間の方は基本的に1種類しかないので、さほど迷うことはないだろう。特にVisual Studio 2005でソース・ファイルを作成するとデフォルトで、

using System.Collections.Generic;

が書き込まれたファイルが作成されることも多いので、いちいち意識せずとも対応できるだろう。

 問題は、コレクション・クラス本体の名前が変わっている点である。素早く把握できるように、主要なコレクション・クラスについての対応関係を表にまとめておく。

従来のコレクション ジェネリックのコレクション 機能
ArrayList List 可変サイズの1次元リスト
Hashtable Dictionary キー/値ペアのコレクション
Queue Queue 先入れ先出しコレクション
SortedList SortedList キーでソートされたキー/値ペアのコレクション
Stack Stack 後入れ先出しコレクション
コレクション・クラスの対応表
ジェネリックのコレクション・クラスは.NET Framework 2.0でクラス・ライブラリに追加されたもの。もちろんこれには従来のコレクション・クラスも含まれている。

 ちなみに、Stackクラスのように従来のものと名前が同じクラスが存在することに注意が必要である。これらは別の名前空間に属する異なるクラスである。そのため、ジェネリック使用の有無のほかにも相違が存在するケースがある。その中で、特に大きな問題は後で説明を行う。

 さて、ArrayListからListへの名前の変更は、よく使われるクラスの名前がより短くなったということで使い勝手が良くなったといえるだろう(もっとも、ArrayListからList<string>になったと思うなら、短くなったとはいいにくいが……)。

 一方、HashtableからDictionaryへの変化は、原型をまったくとどめない名前の変化といえる。これは、名前からの類推が通用せず、新しい名前を暗記するしかないだろう。

 話はまだ終わらない。実は、ジェネリックのコレクションには、従来のコレクションにはない機能も増えているからだ。ここでは、そのような2つのクラスを紹介しておこう。

■新しいコレクション・クラス − LinkedListクラス

 LinkedListクラスは、1次元のリストを扱うという意味ではArrayList/Listクラスに似た機能を持っているが、異なる性質を持っている。

 主な相違点は2つある。1つは、インデックスによるアクセスができないこと。つまり、添え字を付けて2番目、3番目といった番号指定で要素にアクセスできないのである。リストの途中の要素が必要となる場合は、先頭あるいは末尾から順にたどっていかねばならない。なぜそのような回りくどい機能制限を付けているのかといえば、代わりに中間の要素に対する挿入/削除が素早くできるようになっているからである。

 実は、このような仕様はリーズナブルである。なぜなら、サイズ不定のコレクションに番号でアクセスすることはあまり多くはなく、あったとしても0から順にすべてにアクセスしているだけで、番号そのものに意味がないことが多いためだ。単に、順番にアクセスするだけなら、番号を使わない方法でもよい。つまり、最初や最後の要素を得る手段と、次ないし手前の要素を得る手段さえあれば、番号でアクセスできずともさほど困らないのである。

 逆に、リストの中間に要素を挿入/削除する性能が意味を持つことは多い。加えて、それが高速に処理できれば、メリットになることも多いだろう。

 ここでは実際に、ListクラスとLinkedListクラスの挿入性能の差を見てみよう。以下は、2つの要素を持つリストの中間に、1万個の要素を挿入する作業を100回繰り返すサンプル・コードである。ListクラスとLinkedListクラスについて、それぞれ同じ回数の処理を行っている。

using System;
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    // Listクラスの利用
    DateTime start1 = DateTime.Now;
    for (int i = 0; i < 100; i++)
    {
      List<string> list1 = new List<string>();
      list1.Add("First");
      list1.Add("Last");
      for (int j = 0; j < 10000; j++)
      {
        list1.Insert(1, "Inserted");
      }
    }
    DateTime end1 = DateTime.Now;
    Console.WriteLine(end1 - start1);

    // LinkedListクラスの利用
    DateTime start2 = DateTime.Now;
    for (int i = 0; i < 100; i++)
    {
      LinkedList<string> list2 = new LinkedList<string>();
      list2.AddFirst("First");
      list2.AddLast("Last");
      for (int j = 0; j < 10000; j++)
      {
        list2.AddAfter(list2.First, "Inserted");
      }
    }
    DateTime end2 = DateTime.Now;
    Console.WriteLine(end2 - start2);
  }
}
リスト3 ListとLinkedListの挿入性能の差
コメントで記述した出力例は、Pentium D 3.2GHzのマシンでデバッグ・ビルドによる実行結果。

00:00:05.3480694 (Listクラス利用時)
00:00:00.0820164 (LinkedListクラス利用時)
リスト3の実行結果

 結果は見てのとおり、けた違いの性能差が出ている。

 ちなみに、LinkedListクラスを使うと、データの入れ物となるLinkedListNodeクラスの存在も意識しなければならず、コーディングがやや煩雑になる。しかし、LinkedListNodeクラスのインスタンスは、リストに含めたり除外したりすることが容易であり、その特徴をうまく活用するとリストをばらして再構築するような処理の効率が上がる。ArrayList/Listクラスを使って思うような性能が出ない場合は、LinkedListクラスを試してみるとよいだろう。

 ただし、メモリの使用量(確保されるオブジェクトの数)はLinkedListクラスの方が大きくなるというデメリットもある。上記のプログラムの前半と後半を別々に実行させると、プログラム終了時点でListクラスの方は約6Mbytes程度のメモリを消費しているのに対して、LinkedListクラスはより多くの約11Mbytes程度のメモリを消費していた。

■新しいコレクション・クラス − SortedDictionaryクラス

 新設されたSortedDictionaryクラスは、SortedListクラスと機能がよく似ている。しかし、完全に同じではない。SortedDictionaryクラスとSortedListクラスの関係は、LinkedListクラスとListクラスの関係によく似ている。

 SortedDictionaryクラスとSortedListクラスの違いは、メモリの使用方法と、挿入および削除の速度である。主な相違点は以下のとおりである。

  • SortedListクラスは、SortedDictionaryクラスほどメモリを使用しない

  • SortedDictionaryクラスには、ソートされていないデータに対して高速な挿入操作および削除操作が可能

  • すべての並べ替えられたデータを一度に取り出す場合、SortedListクラスの方がSortedDictionaryクラスよりも高速である

 INDEX
  C# 2.0入門
  第2回 ジェネリック
  1.ジェネリックとは何か?/新しいコレクションの紹介
    2.ジェネリック・コレクションの使い方/ジェネリック・メソッドと型推論
    3.HashtableクラスとDictionaryクラスの非互換性/ジェネリックなクラスを自作する
    4.制約の付いたジェネリックなクラス/C++のtemplate機能との相違
 
インデックス・ページヘ  「C# 2.0入門」


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

本日 月間