連載:C# 2.0入門

第6回 部分クラスと静的クラス

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

リフレクションと部分クラス

 部分クラスの効能として「巨大なクラスを複数のソース・ファイルに分割して記述できる」と述べたが、これには異論があり得るだろう。1つのソース・ファイルに収まらないような巨大なクラスを作成すると保守性が下がるので、作るべきではない……というのは1つの健全な発想だ。

 しかし、C# 2.0の世界では別の可能性が存在するのではないかと考える。

 第1回の「後退するクラスの立場」でも述べたように、C# 2.0では動作をカスタマイズする主体を、クラスではなくメソッドにすることができる。そのような方針でコードを記述すると、クラスの役割はメソッドやデータの入れ物へと後退する。

 単なる入れ物へと立場を後退させたクラスは、データもメソッドも格納できる一種のコレクションのような存在になる。そして、格納されたデータやメソッドは、リフレクションの機能を用いて容易に列挙して扱うことができる。

 さて、クラスを単なる入れ物と割り切って使うとすれば、それに格納されるアイテムの数はさまざまである。少数であることもあれば、多数であることもあるだろう。それは自然な成り行きであって、多くを格納することは間違いとはいえない。そして何より、リフレクションによる列挙という使われ方を前提とするなら、格納されるアイテムがどれほど増えようとも、別のクラスに分割することはできない。あるカテゴリに属するデータやメソッドは特定のクラスに含まれているという前提での列挙になるためだ。

 そのような記述方法は机上の空論ではなく、実際に筆者が同人ソフトを書く際に使ってうまく機能した実績がある(このようなリスキーなチャレンジに、同人ソフトは持ってこいである。具体的なお客さまに依頼されて書くコードで、このような冒険はできない)。

 実際に、その同人ソフトのコードの一部を抜粋して以下に紹介しよう。

public static class AdviceInfos
{
  [AdviceInfo(50, "必読最初のアドバイス")]
  public static readonly AdviceInfo 必読最初のアドバイス = new AdviceInfo(
    new string[] {
      "まず私から出されるアドバイスはすべて読むとよいと思います。",
      ……中略……
    },
    delegate()
    {
      return true;
    });

  [AdviceInfo(200,"努町での買い物")]
  public static readonly AdviceInfo 努町での買い物 = new AdviceInfo(
    new string[] {
      "駅の近くに商店街があります。",
      ……中略……
    },
    delegate()
    {
      General.LearnPlace(Places.Place努町商店街);
      return true;
    });
  ……中略……
}
リスト5 クラスを入れ物として使った例

public class AdviseInfoList
{
  public static AdviceInfo[] GetList()
  {
    return list.ToArray();
  }

  private static List<AdviceInfo> list = new List<AdviceInfo>();

  static AdviseInfoList()
  {
    List<int> idList = new List<int>();

    foreach (System.Reflection.FieldInfo info
        in typeof(AdviceInfos).GetFields())
    {
      object[] result = info.GetCustomAttributes(
                          typeof(AdviceInfoAttribute), false);

      if (result.Length != 0)
      {
        int id = ((AdviceInfoAttribute)result[0]).ID;

        if (idList.Contains(id))
        {
          throw new ApplicationException(
            "AdviceInfo ID " + id.ToString()
            + " は重複しています。");
        }
        idList.Add(id);
        AdviceInfo advice = (AdviceInfo)info.GetValue(null);
        advice.ID = id;
        advice.Name = ((AdviceInfoAttribute)result[0]).Name;
        list.Add(advice);
      }
    }

    // 本当に必要か分からないが、ID順ソートしておく
    list.Sort(delegate(AdviceInfo x, AdviceInfo y)
    {
      return x.ID - y.ID;
    });
  }
}
リスト6 リスト5のクラスから一覧表を作成するクラス

 リスト5のAdviceInfosクラスは、ゲーム中で行われる「アドバイス」の情報をまとめて入れておく「入れ物」として機能している。しかし、このクラスは、表示されるメッセージ本体を含むために、長くなりがちである。実際、このクラスは646行あった。かろうじて1ファイルでも扱えるサイズだが、2倍になれば分割を考えたいところだ。その場合は、当然部分クラスの機能を使用できることになる。

 さて、このようなコードを見て、不自然な書き方だと思った読者も多いと思う。単に多数のオブジェクトを格納しておくだけなら、配列でもよいはずである。だが、配列ではこのコードほどうまく扱うことができない。その理由は以下のとおりである。

  • 配列の初期化構文はクラスと違って複数ファイルに分割できない
  • 特定の1つのアドバイスを参照する際、リスト5のコードでは「AdviceInfos.必読最初のアドバイス」と記述できるが、配列を使うと「AdviceInfos[0]」のような表記になり、分かりにくい「0」というマジック・ナンバーが入り込んでしまう

 後者の問題を解消するために、0を定数に定義しても、定義された定数と実際の内容が食い違ってしまうリスクを排除できない。また、配列ではなくDictionaryクラスのようなコレクションを使うと、「AdviceInfos["必読最初のアドバイス"]」のように記述することもできる。これなら、マジック・ナンバーを覚えずに済むが、本当に存在するアドバイスであるか否かが、コンパイル時にはチェックできない。

 上記のクラスを入れ物として使う方式は、これらの問題をすべて解決している。

 このような書き方が本当に高い実用性があり、定着していくかはまだ分からない。しかし、仮にこのような書き方が「あり」だとすれば、部分クラスの機能によってクラスを複数ファイルに分割できることは、大きな助けになるだろう。

部分クラスの注意点

 部分クラスを使ううえでのいくつかの注意点を以下にまとめておく。

■属性

 個々の部分クラスに付いた属性は、すべてその型に付加されたものと見なされる。以下の2つのコードは等価である。

[a("part1")]
partial class A {}

[a("part2")]
partial class A {}
[a("part1"), a("part2")]
partial class A {}

 つまり、個々の部分に異なる属性を付けて区別するようなことはできない。

■修飾子

  • public、protected、internal、privateのアクセス修飾子はすべての部分クラスで同じでなければならない

  • 部分クラスの宣言の少なくとも1つにabstractが付いていれば、抽象クラスになる

  • 部分クラスの宣言の少なくとも1つにsealedが付いていれば、シール・クラスになる

  • 特定の1つの部分クラスにunsafeが付いていれば、その部分クラスだけがunsafeコンテキストになる

■型パラメータと制約

  • ジェネリック型の型パラメータは、すべての部分クラスで同じ内容、順序、名前が指定されねばならない

  • 制約(where句)は同じ内容が指定されていなければならないが、順番は問わない

■基本クラス

  • 部分クラスの宣言が基本クラスの指定を含む場合、すべての部分クラスは同じ基本クラスの指定を含まねばならない

■基本インターフェイス

  • 部分クラスの宣言に付加された基本インターフェイスの宣言は、すべて集められて、その型が実装するインターフェイスと見なされる。ただし、同じインターフェイスを複数指定した場合でも、インターフェイス・メンバの実装は1つだけ行う

■メンバ

  • クラス内の複数の部分で同じメンバを宣言することはできないが、partialな内部クラスは宣言できる

■所属する名前空間

  • 1つの型の部分クラスは同じ名前空間に属する必要がある

■名前のバインディング

 部分クラスごとに別のusingディレクティブが指定されると、部分クラスごとで同じ名前が、異なる意味を持つ場合があり得る。

 例えば以下の2つのコードでは、Aの持つ意味が異なるため、a1とa2は異なる型となる。

namespace N
{
  using A = System.String;
  partial class X
  {
    private A a1;
  }

}

namespace N
{
  using A = System.Text.StringBuilder;
  partial class X
  {
    private A a2;
  }
}


 INDEX
  C# 2.0入門
  第6回 部分クラスと静的クラス
    1.値型と参照型の相違は何か?
    2.部分クラス/自動生成コードと安全に共存する
  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 記事ランキング

本日 月間