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

Chapter1 C# 3.0らしいプログラミングとは?

川俣 晶
2009/07/31

1.2 C# 3.0らしいソースコードとは?

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

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

 まずはじめに、試行錯誤で書き上げた後に、「これはC# 2.0らしくない……」と思ったコード、リスト1.1を紹介する。これはゲーム中で、主人公の位置を移動させる機能を持ったメソッドである。瞬間移動する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.OnLeaving();

  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.1 C# 3.0らしくないソースコード(一部分)

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

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

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

 リスト1.2では、C# 3.0の新機能である「ラムダ式」を活用している(ラムダ式については、第6章と第7章で詳しく解説する)。このリストを読むためにC# 1.xプログラマーが必要とする知識は、

  • 「()=>{……}」という記述がデリゲート型の引数に代入できるラムダ式の書式であること
  • ラムダ式は上位のスコープに属するため、上位スコープの変数を参照できること

の2点だけである。

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

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

  goTo(distPlace,

    ()=> // ラムダ式
    {
      // 距離計算の起点を確定する
      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);
      }
    },

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

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

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

  移動時間計算();

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

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

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

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

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

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

 それだけではない。

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

public static void TrainTo(Place distPlace)

{

  goTo(distPlace,

    ()=> { /* 電車による移動時間計算 */ },

    ()=> { …… });

}
電車移動を行うTrainToメソッドを実装する場合の例

* 電車による移動時間計算の処理自体をgoToメソッドに渡すため、goToメソッド側は変更する必要がない。

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

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

 名前のあるメソッドにはそのような機能は存在しないので、どうしても値を渡したければ引数などを増やすしかない。しかし、引数を増やすとデリゲート型の定義も変わってしまうが、その場合、それを参照するすべてのコードに影響が及んでしまう。

 すなわち、このような書き方は、C# 1.xではうまくできないのである。

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


 INDEX
  [完全版]究極のC#プログラミング
  Chapter1 C# 3.0らしいプログラミングとは?
    1.はじめに/本書の位置づけ
    2.1.1 意外性あり? 本書で解説すること/C# 3.0の適用範囲/筆者の来歴
  3.1.2 C# 3.0らしいソースコードとは?
    4.1.3 コードの遅延実行という例
    5.1.4 インターフェースとの比較
    6.1.5 後退するクラスの立場
    7.1.6 クラスベースとプロトタイプベース
    8.1.7 クラスベースの問題点/【C#olumn】クラスの問題とは何か?
    9.1.8 JavaScriptとの相違点
    10.まとめ/【C#olumn】金のハンマーと銀の弾丸―クラス至上主義
 
インデックス・ページヘ  「[完全版]究極の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 記事ランキング

本日 月間