連載:C# 2.0入門

第1回 総論:C# 2.0らしいプログラミングとは

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

C# 2.0らしいソース・コードとは?

 さて、そろそろ「論よりソース」である。能書きを重ねるよりも、実際のソース・コードを見る方が分かりやすいだろう。

 同人ゲームのコードを書いているときに、「これはよい例だ」と思う事例に出合ったので、そのソース・コードの断片をそのまま紹介しよう。

 まず試行錯誤で書き上げた後に、「これはC# 2.0らしくない……」と思ったコードを紹介しよう。これはゲーム中で、主人公の位置を移動させる機能を持ったメソッドである。瞬間移動するWarpToメソッドと、時間をかけ歩いて移動するGoToメソッドの2種類がある。

// 距離やストリーキングのチェック抜きで瞬間移動する
public static void WarpTo(Place distPlace)
{
  goTo(distPlace, true);
}

// 距離やストリーキングのチェックを行いつつ移動する
public static void GoTo(Place distPlace)
{
  goTo(distPlace, false);
}

// WarpToメソッドとGoToメソッドの共通処理
// 引数distPlaceは目的地、引数warpModeは瞬間移動の有無を示す
private static void goTo(Place distPlace, bool warpMode)
{
  // 現在位置と目的地が同じ場合は何もしない
  if (currentPlace == distPlace) return;

  // 他人の目がある移動か?
  bool hasOtherEyes
      = currentPlace.HasOtherEyes || distPlace.HasOtherEyes;

  // 現在の位置を離れるためのイベント・ハンドラを呼ぶ
  currentPlace.OnLeaveing();

  if (!warpMode)
  {
    // 距離計算の起点を確定する
    Place src = currentPlace;
    if (src.HasGlobalPlaceId)
      src = Places.GetPlaceByNumber(src.ParentGlobalPlaceId);

    // 距離計算の終点を確定する
    Place dst = distPlace;
    if (dst.HasGlobalPlaceId)
      dst = Places.GetPlaceByNumber(dst.ParentGlobalPlaceId);

    // 距離がある場合
    if (src.HasDistance && dst.HasDistance)
    {
      // 距離(メートル単位)を計算する
      int distanceMeter = Math.Abs(src.Distance - dst.Distance);
      // 所要時間を計算する
      int minutes = CalcGoingTime(distanceMeter);
      // 現在時刻を進める
      State.GoTime(minutes, hasOtherEyes, false);
    }
  }

  // 現在位置を目的地に
  currentPlace = distPlace;

  // 目的地に入ったイベント・ハンドラを呼び出す
  currentPlace.OnEntering();

  // まだ知らない場所なら、その場所を学ぶ
  if (!IsKnownPlace(currentPlace) && currentPlace.HasDistance)
  {
    General.LearnPlace(currentPlace);
  }

  if (!warpMode)
  {
    // ストリーキング成立チェック
    // hasOtherEyesで非深夜かつ、ファッション刺激度 > 100かつ、
    // 移動終了時に社会評価がマイナスでなければストリーキング成功
    if (hasOtherEyes && !General.Is深夜
        && General.ファッション刺激度 > 100
        && State.修正済み社会評価 >= 0)
    {
      // この部分は自粛によりカット
    }
  }
}
リスト1 C# 2.0らしくないソース・コード(一部分)

 できるだけコメントを補っておいたので、特に内容の説明は行わない。C# 1.xプログラマーなら、問題なく読めるはずである。

 さて、このソース・コードにはどのような問題があり、どのような改善点があり得るだろうか。純粋に構文レベルの問題として考えてみていただきたい。

 では、実際にこのコードを書き換えた結果をお見せしよう(リスト2)。

 リスト2では、C# 2.0の新機能である「匿名メソッド」を活用している。このリストを読むためにC# 1.xプログラマーが必要とする知識は、

  • 「delegate(){……}」という記述がデリゲート型の引数に代入できる匿名メソッドの書式であること

  • 匿名メソッドは上位のスコープに属するため、上位スコープの変数を参照できること

の2点だけである。

// system.windows.forms.dllを参照していれば
// System.Windows.Forms.MethodInvokerデリゲートを使うことで
// 不要となる定義
delegate void MyMethodInvoker();

// 距離やストリーキングのチェック抜きで瞬間移動する
public static void WarpTo(Place distPlace)
{
  goTo(distPlace, delegate() { }, delegate() { });
}

// 距離やストリーキングのチェックを行いつつ移動する
public static void GoTo(Place distPlace)
{
  // 他人の目がある移動か?
  bool hasOtherEyes = currentPlace.HasOtherEyes || distPlace.HasOtherEyes;
  goTo(distPlace,

    delegate() // 匿名メソッド
    {
      // 距離計算の起点を確定する
      Place src = currentPlace;
      if (src.HasGlobalPlaceId)
        src = Places.GetPlaceByNumber(src.ParentGlobalPlaceId);

      // 距離計算の終点を確定する
      Place dst = distPlace;
      if (dst.HasGlobalPlaceId)
        dst = Places.GetPlaceByNumber(dst.ParentGlobalPlaceId);

      // 距離がある場合
      if (src.HasDistance && dst.HasDistance)
      {
        // 距離(メートル単位)を計算する
        int distanceMeter = Math.Abs(src.Distance - dst.Distance);
        // 所要時間を計算する
        int minutes = CalcGoingTime(distanceMeter);
        // 現在時刻を進める
        State.GoTime(minutes, hasOtherEyes, false);
      }
    },

    delegate() // 匿名メソッド
    {
      // ストリーキング成立チェック
      // hasOtherEyesで非深夜かつ、ファッション刺激度 > 100かつ、
      // 移動終了時に社会評価がマイナスでなければストリーキング成功
      if (hasOtherEyes && !General.Is深夜
          && General.ファッション刺激度 > 100
          && State.修正済み社会評価 >= 0)
      {
        // この部分は自粛によりカット
      }
    });
}

// WarpToとGoToの共通処理
private static void goTo(
  Place distPlace, // 目的地
  MyMethodInvoker 移動時間計算,
  MyMethodInvoker ストリーキング成立チェック)
{
  // 現在位置と目的地が同じ場合には何もしない
  if (currentPlace == distPlace) return;

  // 現在の位置を離れるイベント・ハンドラを呼ぶ
  currentPlace.OnLeaveing();

  移動時間計算();

  // 現在位置を目的地に
  currentPlace = distPlace;
  // 目的地に入ったイベント・ハンドラを呼び出す
  currentPlace.OnEntering();

  // まだ知らない場所なら、その場所を学ぶ
  if (!IsKnownPlace(currentPlace) && currentPlace.HasDistance)
  {
    General.LearnPlace(currentPlace);
  }
  ストリーキング成立チェック();
}
リスト2 C# 2.0らしいソース・コード(一部分)

 このリスト1とリスト2を見ると、(匿名メソッドを除けば)同じ名前の3つのメソッドから構成されているにもかかわらず、処理コードの所属するメソッドが大きく違うことが分かると思う。

 リスト1では、移動時間計算とストリーキング成立チェックのコードは、共通処理を行うgoToメソッドに含まれていた。しかし、リスト2ではこの2つのコードは、GoToメソッドに書き込まれている。

 この違いは、特にWarpToメソッドの動作を理解するためにソース・コードを読んでいるプログラマーには大きな恩恵をもたらす。このようなプログラマーは、まずWarpToメソッドのソースを読み、そこから呼び出されるgoToメソッドのソースを追いかけることになる。その際、リスト1はWarpToメソッド経由では実行されない膨大なコードを見る羽目になる。しかし、リスト2はそのようなコードをまったく見ることはない。

 逆にいえば、GoToメソッドでのみ実行され、WarpToメソッド経由では実行されないコードはすべてGoToメソッドに含まれることになる。それにより、コードの把握しやすさ、メンテナンスのやりやすさが向上している。

 それだけではない。

 リスト2は拡張性も向上している。ここには徒歩移動を行うGoToメソッドと、特殊状況でやむを得ず主人公の居場所を瞬間移動させるWarpToメソッドしかないが、そのほかに電車移動を行うTrainToメソッドや、自動車で移動を行うDriveToメソッドなどを追加する必要が生じたとしよう。それらのメソッドがGoToメソッドと違うのは主に移動時間の計算になるが、その計算処理は引数でgoToメソッドに渡すというアーキテクチャである。そのため、以下のようなコードを書き足すだけで、既存のメソッドは変更することなく、新しい移動手段を追加することができる。

public static void TrainTo(Place distPlace)
{
  goTo(distPlace,
    delegate() { /* 電車による移動時間計算 */ },
    delegate() { …… });
}
電車移動を行うTrainToメソッドを実装する場合の例
電車による移動時間計算の処理自体をgoToメソッドに渡すため、goToメソッド側は変更する必要がない。

 一方、リスト1はすべてがgoToメソッド内に混在し、分かりにくく扱いにくい。

 かといって、名前のあるメソッドを使って匿名メソッドとほぼ等価なコードをC# 1.xでは書けないことに注意しよう。リスト2のGoToメソッドにおいて、変数hasOtherEyesは、2つの匿名メソッドから参照されているが、これは匿名メソッドが上位のスコープに属するという機能性から可能になることである。

 名前のあるメソッドにはそのような機能性は存在しないので、どうしても値を渡したければ引数などを増やすしかない。しかし、引数を増やすとデリゲート型の定義も変わってしまう。状況に応じて、カスタムなデリゲート型をいちいち定義するという煩雑な手間が発生してしまう。しかも、デリゲート型を再定義すると、それを参照するすべてのコードに影響が波及してしまう。

 つまりこのような書き方は、C# 1.xではうまく書けないのである。

 それ故に、この事例はソース・コードの分かりやすさとカスタマイズのしやすさが向上している事例であり、かつ、C# 1.xではうまく書けなかったものがC# 2.0になって書けるようになった事例であるということができる。


 INDEX
  C# 2.0入門
  第1回 総論:C# 2.0らしいプログラミングとは
    1.意外性あり? この連載で解説すること
  2.C# 2.0らしいソース・コードとは?
    3.インターフェイスとの比較
    4.後退するクラスの立場、クラスの持つ問題点
 
インデックス・ページヘ  「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 記事ランキング

本日 月間