連載:C# 3.0入門

第4回 自動実装と自動定義

株式会社ピーデー 川俣 晶
2008/07/04
Page1 Page2 Page3

ラムダ式を使ったダーティー・テク ―― refの代役

 本題に入る前に、軽い話題として1つの問題解決事例を紹介しよう。実際にC# 3.0でプログラミング中に遭遇した問題と解決である。ポイントだけ抜き出したシンプルなコードで説明を行う。

 まず、以下のようなソース・コードがあったとする。ポイントは、doSomethingメソッドの引数にrefキーワードが付いていて、参照を渡している点である。

using System;

static class SomeClass
{
  public static int A = 0;
}

class Program
{
  private static bool doSomething(ref int a)
  {
    return ++a == 1;
  }

  static void Main(string[] args)
  {
    if (doSomething(ref SomeClass.A))
    {
      Console.WriteLine(SomeClass.A);
    }
    // 出力:1
  }
}
リスト1 refキーワードを使ったサンプル・プログラム

 ところが、SomeClassクラスの実装が変更され、「A」はフィールドからプロパティに変更された。

static class SomeClass
{
  private static int a = 0;

  public static int A
  {
    get { return a; }
    set { a = value; }
  }
}
リスト2 変更後のSomeClassクラス

 しかし、これではコンパイルができない。refパラメータとしてプロパティは渡せないからである。

 さて、この問題はどう解決すればよいだろうか。

 まず、そもそも外部のクラスのフィールドに対してrefパラメータを使うことはうかつであり、好ましいコードではないという点に留意しよう。これが通常の開発フェイズにあるプログラムであれば、もっとマシな設計に変更することで対処すべきだと思う。

 だが、筆者が遭遇した対象はすでに開発フェイズを終了し、メンテナンス・フェイズに入ったソース・コードであった。そのため、大幅な設計変更はリスクが大きいため、変更は最小限にしたいと考えた。

 そこで、refパラメータで変数を渡すことをやめ、代わりに変数の値の読み出しと書き込みを行うデリゲート型を受け渡すようにした。

class Program
{
  private static bool doSomething(Func<int> getter, Action<int> setter)
  {
    setter( getter() + 1 );
    return getter() == 1;
  }

  static void Main(string[] args)
  {
    if (doSomething(
            () => SomeClass.A, (v) => { SomeClass.A = v; }))
    {
      Console.WriteLine(SomeClass.A);
    }
  }
}
リスト3 デリゲート型を受け取るdoSomethingメソッド

 これで、変更はdoSomethingメソッドと、それを呼び出すコードだけで済むようになり、ほかのコードと動作は完全に同等のままとなった。

 さて、このような「変数を渡す代わりに変数を読み書きするデリゲート型を渡す(あるいはラムダ式を渡す)」書き方は、その後も数回活用し、意外と有用であることに気付いた。理由は以下のとおりである。

  • 変数以外が対象でも使える(ファイルやDOMツリーの特定のノードを読み書きするなど)
  • 型のミスマッチがあっても使える(整数型として受け渡すのに、実際に格納する変数が文字列型であってもラムダ式の中で変換してミスマッチを吸収できる)
  • リアルタイムに変化する変数値などに使うと、常に最新値が取り出せる(非同期プログラミングや、寿命の長いオブジェクトで使うと便利)
  • 複数の書き込み対象を引数で渡すことも容易

 恐らく、このようなテクニックはすでに世の中に存在し、名前も付いているだろうが、筆者は仮にgetter/setterパターンと呼んで重宝している(ただし乱用は禁物)。

自動実装プロパティ

 さて、話を最初に巻き戻そう。上記の問題は、「フィールド」と「プロパティ」は等価ではないが、しばしば等価であるかのように扱われるという状況によって発生した問題である。

 その問題の背景には、一部のオブジェクト指向信者が必須の要求として掲げる「アクセサの実装」(ゲッター/セッターのメソッドを用意してフィールドそのものはカプセル化せよ)という非現実的な要求がある。このような要求には、変更に対する対応力の増強という長所があるものの、書くための手間の過大な大きさや、本質的に何の意味も持たない行がソース・コード上に増えて分かりやすさを阻害するなどの短所も大きい。

 これらに折り合いをつけるための方便として、C#では、まずpublicなフィールドとして記述し、アクセサが必要とされた時点でプロパティに書き換えるというテクニックが使われることがある。このようなテクニックは、たいていの場合、リーズナブルであり有効に機能する。

 しかし、フィールドとプロパティは完全に等価ではないため、その部分に引っ掛かると急に破たんを起こしてしまう。上記の例はまさにそれである。

 では、このような破たんを防ぐにはどうすればよいのだろうか?

 もちろん、必ずプロパティでアクセサを付ければよいのである。だが、それは手間がかかりすぎるし、本質的に意味のないコードが増えすぎる。

 このジレンマを解決してくれるのが、C# 3.0の自動実装プロパティである。自動実装プロパティは、以下のように行われる典型的なプロパティの定義を、大幅に簡素化するものである。

class SomeClass
{
  private int a;

  public int A
  {
    get { return a; }
    set { a = value; }
  }
}
リスト4 通常のプロパティ定義

 このリスト4とほぼ同等のプロパティを、自動実装プロパティを用いて定義すると以下のリスト5のようになる。

class SomeClass
{
  public int A { get; set; }
}
リスト5 自動実装されるプロパティ定義

 このように、たった1行になってしまった。

 つまり、フィールドの代わりにプロパティを記述する場合、以下のようなフィールドの代わりに、

public int A;

 以下のような自動実装プロパティを記述することで対処できることになる。

public int A { get; set; }

 もちろん、自動実装プロパティではあらゆるプロパティのニーズには対応できないが、それはそれで構わない。その場合はどのみち記述が長くなるので、従来どおりの長い書き方で実現すればよいからである。

自動実装プロパティのアクセス制御

 前掲の「リスト4 通常のプロパティ定義」と「リスト5 自動実装されるプロパティ定義」は完全に同等ではない。最大の相違は、自動実装されるフィールド(「バッキング・フィールド(Backing Field)」と呼ぶ)にアクセスできない点にある。

 このため、以下のリスト6のようなコードは、ストレートに自動実装プロパティに書き換えることができない。「a」に相当するフィールドにはアクセスできなくなるため、「Console.WriteLine(a);」がコンパイルできなくなるのである。

class SomeClass
{
  private int a = 0;

  public int A
  {
    set { a = value; }
  }

  public void WriteA()
  {
    Console.WriteLine(a);
  }
}
リスト6 フィールドにアクセスするので自動実装プロパティが使えない

 しかしながら、この特徴は少なくとも1つのメリットをもたらす。大文字の名前と小文字の名前を使い分けることができない日本語名を使った場合でも、問題なく自動実装プロパティを使用できるのである*

class SomeClass
{
  public int 日本語名 { get; set; }
}
リスト7 日本語名で自動実装プロパティを使用する

* C#ではフィールドに小文字の名前を使い、対応するプロパティには、その先頭1文字を大文字にした名前を使うことが多い。

 では、リスト6に相当するコードは、自動実装プロパティを使用した場合は実現できないのだろうか?

 この問題は、「プロパティのgetはprivate、setはpublicにできるか?」と読み替えれば可能となる。以下のリスト8は、プロパティのgetアクセサとsetアクセサに対して別のアクセシビリティを指定した例である。このプロパティAは、クラス内からは読み出せるが、クラス外からは読み出せない。しかし、クラス外からの書き込みはできる。

class SomeClass
{
  public int A { private get; set; }

  public void WriteA()
  {
    Console.WriteLine(A);
  }
}
リスト8 getアクセサはprivate、setアクセサはpublic

 完全に等価ではないが、これで同等の機能を発揮するコードを記述することができた。

読み出し専用、書き込み専用はない

 自動実装プロパティは常にgetアクセサとsetアクセサの双方が必要とされることに注意が必要である。それ故に、以下のケースはどちらもエラーになる。

public int A { get; }

public int A { set; }

 なぜ、読み出し専用、書き込み専用のプロパティは許可されないのだろうか? その理由は構造を考えれば明らかだろう。

 暗黙的に確保されるフィールドにアクセスする手段がない以上、すべての書き込みと読み出しはプロパティ経由になる。ということは、読み出し専用プロパティはデフォルト値しか読み出せないし、書き込み専用プロパティに書き込んだ内容は永遠に読み出すことができない。つまり、たとえ可能になったとしても存在意義はないのである。

 ただし、プロパティ経由ではなく、リフレクション経由でバッキング・フィールドにアクセスできることに注意が必要である。

 以下のリスト9は、通常の手段ではアクセスできないバッキング・フィールドをリフレクションで検出し、その値を書き換えてしまう例である。もちろん、リフレクションを使えばクラス内部の詳細にいくらでも介入できるわけで、バッキング・フィールドも例外ではない。しかし、通常の手段ではソース・コード上に一切姿を見せることがないバッキング・フィールドの書き換えは、ソース・コード上での操作と結果の因果関係が分かりにくくなり、保守性を下げる可能性がある。

using System;
using System.Reflection;

class SomeClass
{
  public int A { get; private set; }
}

class Program
{
  static void Main(string[] args)
  {
    var a = new SomeClass();

    Console.WriteLine("a.A={0}", a.A);
    Type t = a.GetType();

    foreach (FieldInfo f in t.GetFields(BindingFlags.NonPublic
                                      | BindingFlags.Instance))
    {
      Console.WriteLine("found: {0}", f.Name);
      f.SetValue(a, (object)123);
    }
    Console.WriteLine("a.A={0}", a.A);
  }
}
リスト9 リフレクションによるバッキング・フィールドへのアクセス

a.A=0
found: <A>k__BackingField
a.A=123
リスト9の実行結果

 

 INDEX
  C# 3.0入門
  第4回 自動実装と自動定義
  1.ラムダ式を使ったダーティー・テク ― refの代役/自動実装プロパティ
    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 記事ランキング

本日 月間