連載:C# 2.0入門

第4回 Findメソッドとnull許容型

株式会社ピーデー 川俣 晶
2007/08/31

ForEachだけではない繰り返しメソッド

 実は、System.Arrayクラスやジェネリック・コレクションのクラスなどが持つ繰り返しメソッドはForEachメソッドだけではない。Find、FindIndex、FindAllのような検索系のメソッドも持っている。

 FindメソッドはForEachメソッドとよく似ているが、繰り返しではなく検索に特化したメソッドである。

 主な相違は2つある。

 1つは、第2引数で指定するデリゲートがAction<T>ではなくPredicate<T>になっていることである。つまり、メソッドの戻り値がvoid型ではなくbool型になっている。メソッドがtrueを返すと繰り返しを打ち切るようになっている。もう1つは、引数のメソッドがtrueを返したときの列挙値を、Findメソッドそのものの戻り値として返すことである。

 さて、ここで注目したいのは、trueを返すと繰り返しを打ち切るという効能である。これを使えば、break文がなくとも、それに相当するコードを書くことができる。

using System;

class Program
{
  static void Main(string[] args)
  {
    string[] 玉電駅名 = {
      "下高井戸","七軒町","六所神社前","山下","豪徳寺前","宮ノ坂"
    };

    Array.Find(玉電駅名, delegate(string 駅名)
    {
      if (駅名. Contains("前"))
      {
        Console.WriteLine(
          "前の付く駅名としては例えば{0}があります。", 駅名);
        return true;
      }
      return false;
    });
    // 出力:前の付く駅名としては例えば六所神社前があります。
  }
}
リスト3 ForEachメソッドの代わりにFindメソッドを使った例

 しかし、これは読んでうれしいコードではない。return文が2つもあり、返すtrue/falseも直感的に意味が分かりにくい。

 さらにいえば、本来検索のためのFindメソッドを、繰り返しのために使っていることもコードの分かりにくさを生んでいるといえる。パッと見て、このソースが何をするものであるか、かなり分かりにくいといえるだろう。

 では、やはりforeach文に回帰すべきなのだろうか? そうではない。

 Findメソッドを前提に考えるなら、Findメソッドを検索という目的に特化してやれば、コードはすっきりと明快になる。つまり、検索とは関係ない結果の出力機能を第2引数の匿名メソッドの外に出すのである。これがリスト4である。

using System;

class Program
{
  static void Main(string[] args)
  {
    string[] 玉電駅名 = {
      "下高井戸","七軒町","六所神社前","山下","豪徳寺前","宮ノ坂"
    };

    // 検索機能
    string 前の付く駅名 = Array.Find(玉電駅名, delegate(string 駅名)
    {
      return 駅名. Contains("前");
    });

    // 出力機能
    Console.WriteLine(
      "前の付く駅名としては例えば{0}があります。", 前の付く駅名);
    // 出力:前の付く駅名としては例えば六所神社前があります。
  }
}
リスト4 出力は検索と分ける

 これでコードはかなりすっきりした。

 だが、実はこのソースにはもう1つ大きなメリットが存在する。検索機能と出力機能がソース・コード上で完全に分離したのである。Array.Findメソッド呼び出し部分が検索機能であり、続くConsole.WritreLineメソッドが出力機能である。

 一方、foreach文を使ったリスト1では、この2つの機能は明瞭(めいりょう)に分離されていない。では、なぜ分離されているとよいのだろうか。

 分離されているということは、バラバラに切り離して開発することが容易になるからである。特に、リスト4のコードはVisual Studio 2005が持つリファクタリング支援機能の「メソッドの抽出」を使って、検索機能と出力機能を別々に切り出せることに注目しよう。

 それに対して、foreach文を使ったリスト1は、出力機能だけを切り出すことはできるが、出力機能抜きの検索機能をメソッドに抽出することはできない。

 実際に、リファクタリング支援機能で切り分けた例をリスト5に示す。

using System;

class Program
{
  static void Main(string[] args)
  {
    string[] 玉電駅名 = {
      "下高井戸","七軒町","六所神社前","山下","豪徳寺前","宮ノ坂"
    };

    string 前の付く駅名 = Get前の付く駅名(玉電駅名);
    Write前の付く駅名(前の付く駅名);
    // 出力:前の付く駅名としては例えば六所神社前があります。
  }

  // 出力機能
  private static void Write前の付く駅名(string 前の付く駅名)
  {
    Console.WriteLine(
      "前の付く駅名としては例えば{0}があります。", 前の付く駅名);
  }

  // 検索機能
  private static string Get前の付く駅名(string[] 玉電駅名)
  {
    string 前の付く駅名 = Array.Find(玉電駅名, delegate(string 駅名)
    {
      return 駅名. Contains("前");
    });
    return 前の付く駅名;
  }
}
リスト5 「メソッドの抽出」で検索機能と出力機能を分けた例

 このソースには、明らかに冗長な変数がいくつも見られる。

 そこで、リファクタリング・カタログの1つである「ローカル変数の除去」を適用してみよう*

* 念のために補足すると、リファクタリングというのはツールで実行するものではなく、人間が行うものである。ツールは、リファクタリングの書き換えの一部を自動化して支援するにすぎない。ローカル変数の除去は、Visual Studioが支援してくれるわけではないので、手動で書き換えて行う。

using System;

class Program
{
  static void Main(string[] args)
  {
    string[] 玉電駅名 = {
      "下高井戸","七軒町","六所神社前","山下","豪徳寺前","宮ノ坂"
    };

    Write前の付く駅名(Get前の付く駅名(玉電駅名));
    // 出力:前の付く駅名としては例えば六所神社前があります。
  }

  // 出力機能
  private static void Write前の付く駅名(string 前の付く駅名)
  {
    Console.WriteLine(
      "前の付く駅名としては例えば{0}があります。", 前の付く駅名);
  }

  // 検索機能
  private static string Get前の付く駅名(string[] 玉電駅名)
  {
    return Array.Find(玉電駅名, delegate(string 駅名)
    {
      return 駅名. Contains("前");
    });
  }
}
リスト6 冗長なローカル変数を除去した例

 このとおり、役割ごとのメソッドに分けられた、非常にすっきりしたソース・コードを得ることができた。後は、メソッドごとに分担してバラバラに改良することも容易だろう。

複数の結果が欲しい場合

 以上の例では見つけ出す対象は1つだけだが、繰り返しを打ち切らず、すべての対象から探し出すことも容易である。その方法はFindメソッドではなくFindAllメソッドを使うだけである。それにより、結果が配列として得られるようになる。

using System;

class Program
{
  static void Main(string[] args)
  {
    string[] 玉電駅名 = {
      "下高井戸","七軒町","六所神社前","山下","豪徳寺前","宮ノ坂"
    };

    string [] 前の付く駅名
        = Array.FindAll(玉電駅名, delegate(string 駅名)
    {
      return 駅名. Contains("前");
    });

    Array.ForEach(前の付く駅名, delegate(string 駅名)
    {
      Console.WriteLine("前の付く駅名には{0}があります。", 駅名);
    });
  }
}
リスト7 リスト4を複数の結果を得るように改造した

前の付く駅名には六所神社前があります。
前の付く駅名には豪徳寺前があります。
リスト7の実行結果

 この場合には結果が複数になるため、それを格納するコレクションのオブジェクトが生成される。その分だけメモリと実行速度にインパクトを与えてしまう。foreach文で繰り返しながら、見つかったオブジェクトに関して出力を行う書き方よりも、メモリと実行速度が落ちるというわけである。それ故に、採用するかどうかはケースバイケースということになるだろう。しかし、それらが問題にならない場合は、ソースを扱いやすくする良いやり方ではないかと思う。

偉大なる前進とは何か?

 さて、これらのコードを見てどう思うだろうか。筆者は、ここに「偉大なる前進」の歴然とした証拠を見る。

 果たして何が前進したのだろうか。順を追って説明しよう。

 ソース・コードから重複を見つけ出し、それを1つにまとめる作業は、リファクタリングの典型的な作業の1つである。しかし、それはコンピュータ史の黎明期(れいめいき)に「サブルーチン」という機能が発明されて以来、繰り返されてきた「古い常識」でもある。

 だが、この「古い常識」はしばしばうまく利用できない。例えば、繰り返し構文は、繰り返される文だけを抜き出すのは容易でも、繰り返し構文そのものをうまく抜き出せないことが多かった。

 単純な例を出せば、例えば以下のようなコードがあったとき、同じ繰り返し文が2つあるから1つにまとめよう……と思っても、そう簡単には1つにまとめられなかったわけである。繰り返し文には後ろにブロックが付いていて、ブロックは共通ではないからだ。

繰り返し文
{
  func1(i);
}

繰り返し文
{
  func2(i);
}

 しかし、繰り返し文を検索機能ととらえ、実行文と分離できれば、そのような面倒は消滅する。

結果 = 検索
func1(結果);

結果 = 検索
func2(結果);

結果 = 検索
func1(結果);
func2(結果);

 このような書き方は、プログラム言語自身が持つ処理の単位が、「値」から「値の集まり」に変化しつつあることを示す。いい換えれば、「値の処理」+「繰り返し」によって実現していた機能が「値の集まりに対する処理」へと変化していくわけである。

 つまり、冒頭の昔話で書いた「単独の値ではなく、値の集まりに対して行われる演算機能」が実現されたことを意味するわけである。そこでは「明示的な繰り返し構文なしで複数の値に演算を行うことができる」と書いたが、これらのFindメソッドを使ったサンプル・コードはまさに明示的な繰り返し構文(foreach/for/while/do)抜きに、複数の値に演算を行っている。そして、すでに書いたとおり、「ソース・コードの量と質を大きく変えてしまう可能性」を現実のものにしたわけである。

そしてC# 3.0とLINQへ続く

 Find系メソッドを使った処理は、「1つの値の集まり」に対する処理を実現するが、「複数の値の集まり」に対する処理までは実現してくれない。そのため、例えば複数の配列やコレクションをまたがった処理は記述しにくい。

 だが、それはC# 3.0で搭載されるというLINQがフォローしてくれるのだろう。それ故に、C# 2.0とは「値の集まり」を処理するコードを記述する時代への1つのマイルストーンにすぎない……と思った方がよいのかもしれない。

 お楽しみはこれからである。


 INDEX
  C# 2.0入門
  第4回 Findメソッドとnull許容型
    1.MATステートメントの思い出/前回に語り残したこと:ForEachメソッドのbreak問題
  2.ForEachだけではない繰り返しメソッド/複数の結果が欲しい場合/偉大なる前進とは何か?/そしてC# 3.0とLINQへ続く
    3.null許容型とは何か?/なぜnullを入れたいのか
    4.null許容型の内部構造/null合体演算子
    5.is演算子の挙動に注意/3値論理型として使用できるbool?型/nullを許容するとパフォーマンスに影響するか?/補足:null許容への批判
 
インデックス・ページヘ  「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 記事ランキング

本日 月間