連載:[完全版]究極のC#プログラミング

[補遺]ラムダ式を使用した事例

川俣 晶
2010/06/07
Page1 Page2

 本記事は、(株)技術評論社が発行する書籍『[完全版]究極のC#プログラミング ― 新スタイルによる実践的コーディング』から、許可を得て転載しています。
 同書籍は、もともと本フォーラムにて連載していた『C# 2.0入門』、『C# 3.0入門』の記事を整理統合し、加筆、修正されたものです。

手元でまとめて読みたい方は、ぜひ書店などにてお買い求めください。

 【注意】本記事は、書籍の内容を改変することなく、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。

 本書ではラムダ式について解説しているが、実例があったほうがわかりやすいと思うので、実際に筆者が書いたラムダ式を使用したコードの事例を紹介しよう。

 それは、ゲームなどでよく使われる「フラグ」と呼ばれる機能のソースコード体質改善(リファクタリング)を行う際に体験したことである。ここでいうフラグとは、文字列からなる名前とそれに関連付けられた整数の集まりである。

 なお、以下に紹介するソースコードはわかりやすさを優先して基本機能以外を除去し、若干の修正を加えていることをお断りしておく(つまり、実際にチェックインしたコードと同じではない)。

 さて、単に文字列をキーに整数を保管するだけなら、誰でも思い付くとおり、次のような実装でよい。

private static Dictionary<string, int> flags
                               = new Dictionary<string, int>();

 しかし、裸のままのコレクションを自由に触らせるのは好ましいことではないので、フラグの値を設定/取得するGetFlag/SetFlagメソッドをリストS.1のとおり作成した。

public static void ValidateFlagName(string name)
{
  // 名前の妥当性チェック
  // 正しい名前なら、何もせずreturnする
  throw new ApplicationException(
      name + "はvalidなflagの名前ではありません。");
}

public static int GetFlag(string name)
{
  ValidateFlagName(name);
  int result = 0;
  flags.TryGetValue(name, out result);
  return result;
}

public static void SetFlag(string name, int newValue)
{
  ValidateFlagName(name);
  flags[name] = newValue;
}
リストS.1 Dictionary版フラグ用メソッド

 ちなみに、「登録されていないフラグ名に対しては0を返す」、「誤った名前で処理が進行することは好ましくないので、必ずフラグ名をチェックする」という2つの条件を付加している。ValidateFlagNameメソッドは、フラグ名をチェックして未定義の名前ではない場合は例外を投げる。

 さて、このコードは十分にシンプルであるが、ソースコードの体質改善作業に着手した時点で次の2つの問題が浮かび上がった。

  • フラグ名のチェックが実行時まで遅延されるとバグが早期に発見できない。コンパイル時にフラグ名をチェックできないだろうか?

  • プロファイラにかけたところ、ValidateFlagNameメソッドの処理が遅いことがわかった。このため自動テストに時間がかかりすぎていてテストの実行頻度が落ちている。品質改善のために自動テストを高速化したい

 さて、この問題を解決するにはどうしたらよいだろうか?

 最初に考えた解決案は、フラグ名をフィールド名に置き換えた次のリストS.2のようなクラスを作成することであった。フィールドを直接、無制限かつ自由に外部から操作可能にすることは気持ちが悪いと思う読者もいると思うが、もともとここで扱うフラグはプログラム中の任意の箇所から自由に読み書きを許すものなので、機能的には要求と実装は合致する(さらにいえば、これらは必要があればプロパティに置換可能であり、実際の実装では一部がプロパティとして実装されている)。

public static class Flags
{
  public static int 起床回数;
  public static int 社会評価;
  public static int 所持金;
    …
}
リストS.2 フィールド版フラグ

 これで、「正しいフラグ名を使用していることがコンパイル時に検証される」、「コンパイル時に名前が検証されるので、実行時に重いValidateFlagNameメソッドを呼ぶ必要がない」という2つの成果を得られる。ちなみに、フラグはファイルに対して保存/読み込みの機能を持つ必要があるが、それはクラスのシリアライズと考えれば、なんら難しい話ではない。リフレクション機能を使って、フィールドの名前と値のペアを読み取る/値を書き込む処理を記述すれば容易に実現できる。

 しかし、これでハッピーエンドとはならなかった。次のような問題が出たからである。

  • アプリケーションの難読化を実行すると、フィールド名が変更され、変更された名前がつねに同じとは限らない。つまり、ファイルに書き出した情報を読み込むと、同じフラグに値を読み込めない可能性がある
  • フラグ名を文字列処理で生成したいケースがある。たとえば、「機能名+番号」がフラグ名になるケースでは、文字列結合でフラグ名を得る必要がある。このようなケースでは、文字列をもとにフラグを読み書きする「GetFlag(名前)」や「SetFlag(名前, 値)」というメソッドを用意せざるをえない

 はじめの名前の問題は本筋ではないので解決策を簡単に説明しよう。これは、次のリストS.3のように、属性で名前を添えることで解決した。フラグ名の互換性が意味を持つのはプログラム外部とのやり取りだけなので、属性で本来の名前を添えておけば十分である。ちなみに、属性で名前を添えるという解決策は、C#がフィールド名に許していない文字を使ったフラグ名対策としても有効である。

public class FlagNameAttribute : Attribute
{
  public readonly string Name;
  public FlagNameAttribute(string name)
  {
    Name = name;
  }
}

public static class Flags
{
  [FlagName("起床回数")]
  public static int 起床回数;

  [FlagName("社会評価")]
  public static int 社会評価;

  [FlagName("所持金")]
  public static int 所持金;
    …
}
リストS.3 属性によりフラグ名をフィールドに添付

 さて、上記の2番目の問題であるが、文字列でフラグ名を受け取ってフィールドを読み書きすること自体は、リフレクションを使えば難しい話ではない。しかし、プロファイラで実際に測定してみると、リフレクションは予想以上に重い処理だった。このソースコードの体質改善では「自動テストの高速化」というテーマを掲げているので、速度が遅いやり方はNGである。

 具体的にリフレクションの何が遅いのかというと、名前を与えて目的のフィールドを探し出す処理である。ならば、ある特定の名前のフィールドを読み書きするラムダ式を実行開始時点で用意しておき、事前にそれをフラグ名と結び付けておけば素早くフラグを読み書きできることになる。

 実際に、この意図を実装したのが次のリストS.4に示したクラスである。

public static class FlagsList
{
  // フラグのセッターとなるデリゲート
  private delegate void FlagSetter(int val);

  // フラグのゲッターとなるデリゲート
  private delegate int FlagGetter();

  // フラグ名とセッターを結び付けるマップ
  private static Dictionary<string, FlagSetter> setterMap
                    = new Dictionary<string, FlagSetter>();

  // フラグ名とゲッターを結び付けるマップ
  private static Dictionary<string, FlagGetter> getterMap
                    = new Dictionary<string, FlagGetter>();

  public static int GetFlag(string name)
  {
    FlagGetter getter;

    if (getterMap.TryGetValue(name, out getter)) return getter();

    throw new ApplicationException(
        name + "はvalidなflagの名前ではありません。");
  }

  public static void SetFlag(string name, int val)
  {
    FlagSetter setter;
    if (setterMap.TryGetValue(name, out setter))
    {
      setter(val);
      return;
    }
    throw new ApplicationException(
        name + "はvalidなflagの名前ではありません。");
  }

  static FlagsList() // コンストラクタ
  {
    foreach (FieldInfo info0 in typeof(Flags).GetFields(
                     BindingFlags.Public | BindingFlags.Static))
    {

      // ラムダ式に別個にキャプチャさせるため、
      // 内部で宣言したinfoにコピーする
      FieldInfo info = info0;


      // フィールド名に付加した属性からフラグ名を取得し、
      // ゲッターとセッターのマップを作成
      FlagNameAttribute[] names
        = (FlagNameAttribute[])info.GetCustomAttributes(
                              typeof(FlagNameAttribute), false);

      if (names.Length > 0)
      {
        string name = names[0].Name;
        getterMap.Add(name, () => (int)info.GetValue(null));
        setterMap.Add(name, (val) => info.SetValue(null, val));
      }
    }
  }
}
リストS.4 マップによりフィールドとラムダ式を関連付けたFlagsListクラス

 効率という観点からいえば、おそらくラムダ式を使わず、フラグ名と変数infoの値を関連付けるマップを1つ作成するだけでも十分だろう。そのほうがメモリ消費量も少ない。しかし、実際にはゲッター(読み出し)とセッター(書き込み)の2つのマップを作成している。このコードの真価は、フラグの型のバリエーションを増やした時点で発揮される。

 たとえば、0か1しかありえないフラグはint型よりもbool型で表現するほうがよいとして、次のようなフィールドも許すことにしたとしよう。

[FlagName("犬を飼っている")]
public static bool 犬を飼っている;

 これに対処して、FlagsListクラスのコンストラクタは次のように修正できる。なお、GetValue/SetValueメソッドはフィールドの値を読み書きするためのFieldInfoクラスのメソッドである。

getterMap.Add(name, () => (int)info.GetValue(null));
setterMap.Add(name, (val) => info.SetValue(null, val));
if (info.FieldType == typeof(bool))
{
  getterMap.Add(name,
          () => (bool)info.GetValue(null) ? 1 : 0);
  setterMap.Add(name,
          (val) => info.SetValue(null, val == 0 ? false : true));
}
else
{
  getterMap.Add(name, () => (int)info.GetValue(null));
  setterMap.Add(name, (val) => info.SetValue(null, val));
}

 これにより、GetFlag/SetFlagメソッド経由のアクセスは従来どおりint型の値で行うが、フィールドに直接アクセスする場合はfalse/trueで読み書きできるようになった。

 もし、フラグ名と変数infoの値を関連付けるマップを作成しているとすれば、GetFlagメソッドの実装内容は次のリストS.5のようになるだろう。

FieldInfo info;

if (fieldInfoMap.TryGetValue(name, out info))
{
  if (info.FieldType == typeof(bool))
  {
    return (bool)info.GetValue(null) ? 1 : 0;
  }
  else
  {
    return (int)info.GetValue(null);
  }
}
リストS.5 GetFlagメソッドの内容(フラグ名と変数infoを関連付けた場合)(抜粋)

 しかし、フラグ名とラムダ式を関連付けたマップを作成していれば、次ページのリストS.6の内容でよい。

FlagGetter getter;
if (getterMap.TryGetValue(name, out getter)) return getter();
リストS.6 GetFlagメソッドの内容(フラグ名とラムダ式を関連付けた場合)(抜粋)

 両者の差は、型の判定とそれに伴う場合分けを実行するタイミングにある。前者は、判定と場合分けをGetFlag/SetFlagメソッドが呼ばれるごとに実行するが、後者はコンストラクタで1回実行するだけである。

 この差は、GetFlag/SetFlagメソッドの実行回数が多ければ速度差となる。実際に両者のコードを自動テストで比較したが、後者のほうがわずかに速い結果になった(あくまで筆者が開発中のプログラムでの結果なので、別のプログラムでも同じ結果になるとは限らないが)。この程度であればまだそれほど大きな差ではないが、このような判定と場合分けの種類が増えれば、その差は広がっていくだろう。逆にいえば、ソースコードの体質改善を進め、速度を犠牲にせずに判定と場合分けを増やす自由を得るために、このようなコードは有効だといえる。これが、ラムダ式を、整数や文字列と同じように湯水のごとく使うプログラミングがもたらした成果である。

 もちろん、このようなコードがつねに優れている、最善である、このようなコードを書くべきである、筆者は推奨する、といいたいわけではない。これは、たまたま筆者が書いたコードの一例にすぎない。最善ではないかもしれないし、つねに良い結果を出すとは限らない。しかし、C# 3.0ではありうる選択肢の1つに入ったのだろう……と思うのである。


 INDEX
  [完全版]究極のC#プログラミング
  ラムダ式を使用した事例/デザインパターン・ミニカタログ
  1.[補遺]ラムダ式を使用した事例
    2.C# 3.0 デザインパターン・ミニカタログ

インデックス・ページヘ  「[完全版]究極のC#プログラミング」


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

本日 月間