連載:C# 3.0入門

第2回 ラムダ式と型推論

株式会社ピーデー 川俣 晶
2008/05/16
Page1 Page2 Page3

 前回ではラムダ式について解説したが、実例があった方が分かりやすいと思うので、本文に先立って、最近実際に筆者が書いたラムダ式を使用したコードの事例を紹介しよう。

ラムダ式を使用した事例

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

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

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

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

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

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;
}
リスト1 Dictionary版フラグ用メソッド

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

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

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

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

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

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

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

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

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

  1. アプリケーションの難読化を実行すると、フィールド名が変更され、変更された名前が常に同じとは限らない。つまり、ファイルに書き出した情報を読み込むと、同じフラグに値を読み込めない可能性がある

  2. フラグ名を文字列処理で生成したいケースがある。例えば、「機能名+番号」がフラグ名になるケースでは、文字列結合でフラグ名を得る必要がある。このようなケースでは、文字列を基にフラグを読み書きするGetFlag(名前)/SetFlag(名前, 値)というメソッドを用意せざるを得ない

 1の名前の問題は本筋ではないので解決策を軽く説明しよう。これは以下のように、属性で名前を添えることで解決した。フラグ名の互換性が意味を持つのはプログラム外部とのやりとりだけなので、属性で本来の名前を添えておけば十分である。ちなみに、属性で名前を添えるという解決策は、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 所持金;
  ……
}
リスト3 属性によりフラグ名をフィールドに添付

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

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

 実際に、この意図を実装したのが以下のクラスである。

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));
      }
    }
  }
}
リスト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メソッドの実装内容は以下のようになるだろう(抜粋)。

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);
  }
}
リスト5 GetFlagメソッドの内容(フラグ名と変数infoを関連付けた場合)

 しかし、フラグ名とラムダ式を関連付けたマップを作成していれば以下の内容でよい(抜粋)。

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

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

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

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


 INDEX
  C# 3.0入門
  第2回 ラムダ式と型推論
  1.ラムダ式を使用した事例
    2.使える既存のデリゲート/ジェネリック・メソッドと型推論
    3.オーバーロードの解決/匿名メソッドとラムダ式の違いと式ツリー

インデックス・ページヘ  「C# 3.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 記事ランキング

本日 月間