連載:C# 3.0入門

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

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

“名無し”のクラス ―― 匿名型

 さて、同等な結果をより楽に手に入れるためなら、いかなる努力もいとわない、というのはプログラマーが持つ健全な資質の1つである。

 そのようなプログラマーであれば、自動実装プロパティを見て、こう思ったかもしれない。

「ならば自動実装クラスがあってもいいんじゃないか? クラスの宣言なしに、いきなりインスタンスを初期化式で記述してオブジェクトになるような機能」

 実際、このような機能はほかの言語には存在する。そもそも、Ajaxの世界で使用されるJSONは、JavaScriptのそのような機能(オブジェクト・リテラル)の書式そのものである。

 このような機能をC# 3.0は匿名型として実装している。

 例えば、以下のリスト10の変数aに格納されているのは、匿名型のオブジェクトである。

using System;

class Program
{
  static void Main(string[] args)
  {
    var a = new { 名前 = "網島研修生", 口座残高 = 39 };

    Console.WriteLine(
        "名前: {0} 口座残高: {1:C}", a.名前, a.口座残高);
    // 出力:名前: 網島研修生 口座残高: \39
  }
}
リスト10 匿名型の使用例

 C++、Java、2.0までのC#など、強く型付けされた伝統的なオブジェクト指向言語を見慣れた読者は、思わず「何だ、これは!?」と思ったかもしれない。何しろ、変数aが参照しているオブジェクトに関しては、型の宣言どころか、型名すら書かれていないのである。まるで、厳格に型付けされていない簡易スクリプト言語のように見えるかもしれない。

 しかし、そうではない。匿名型は正当なクラスを少ない文字数で書くための記法、一種のシンタックス・シュガーなのである。

 そのような前提から容易に推測できるとおり、クラスの概念がないJavaScriptのオブジェクト・リテラルとは似て非なるまったくの別物である。使い方も性質もまったく違う。

 では、C# 3.0の匿名型が実現する機能とは何だろうか。それは具体的には以下のようなものだ。

  • 読み出し専用プロパティとreadonlyのprivateフィールドを持つクラスとインスタンスを作り出す
  • プロパティ、フィールドの型は初期化式の型が使用される
  • クラス自身の名前は匿名(プログラマーには見えない)
  • プロパティの並び順は厳格に保存され、同じ名前、同じ型、同じ並び順の匿名型は同一のクラスによって実現される(相互に代入可能)

 C# 3.0の言語仕様から具体的な生成内容を引用する。匿名型を使ったコードは、コンパイラにより次のようなコードに展開される。

new { p1 = e1 , p2 = e2 , …… pn = en }

 

class __Anonymous1
{
  private readonly T1 f1 ;
  private readonly T2 f2 ;
  ……
  private readonly Tn fn ;

  public __Anonymous1(T1 a1, T2 a2,…, Tn an) {
    f1 = a1 ;
    f2 = a2 ;
    ……
    fn = an ;
  }

  public T1 p1 { get { return f1 ; } }
  public T2 p2 { get { return f2 ; } }
  ……
  public T1 p1 { get { return f1 ; } }
  public override bool Equals(object o) { …… }
  public override int GetHashCode() { …… }
}
リスト11 匿名型の展開内容

 ここで、T1、T2、……Tnは初期化式から推測された型が使われる。

 実際に、上のリスト10から生成されたクラスは、Reflector for .NETを使えば見ることができる。重要ではない属性やメソッドなどを除去した内容は以下のようになる。確かにreadonlyのフィールド、getのみのプロパティ、初期値を受け取るコンストラクタなどが生成されていることが分かる。

internal sealed class <>f__AnonymousType0<<名前>j__TPar, <口座残高>j__TPar>
{
  // Fields
  private readonly <口座残高>j__TPar <口座残高>i__Field;
  private readonly <名前>j__TPar <名前>i__Field;

  // Methods
  public <>f__AnonymousType0(<名前>j__TPar 名前, <口座残高>j__TPar 口座残高);

  // Properties
  public <口座残高>j__TPar 口座残高 { get; }
  public <名前>j__TPar 名前 { get; }
}
リスト12 リスト10の実行ファイルを逆コンパイルした結果(抜粋)

 以上のように、匿名型も厳格に型付けされた存在であり、型チェックは実行時まで遅延されることはなく、コンパイル時にチェックが行われる。

 しかし、匿名の型であることから、型を明示して宣言することはできず、varキーワードを使った「型を明示しない変数宣言」が必須のものとして要求されている。varは「なくてもよいオプション機能」ではなく、C# 3.0で必須の機能として要求された存在である1例がここにある。

匿名型の等価性

 すでに書いたとおり、同じ名前、同じ型、同じ並び順の匿名型は同一のクラスによって実現される。そのため、以下のように「研修生情報 = 振込後情報」といった代入は可能となっている。

using System;

class Program
{
  static void Main(string[] args)
  {
    var 研修生情報 = new { 名前 = "網島研修生", 口座残高 = 39 };
    var 振込後情報 = new { 名前 = "網島研修生", 口座残高 = 5000 };

    Console.WriteLine( "名前: {0} 口座残高: {1:C}",
                            研修生情報.名前, 研修生情報.口座残高);
    // 出力:名前: 網島研修生 口座残高: \39

    研修生情報 = 振込後情報;

    Console.WriteLine("名前: {0} 口座残高: {1:C}",
                            研修生情報.名前, 研修生情報.口座残高);
    // 出力:名前: 網島研修生 口座残高: \5,000
  }
}
リスト13 相互代入できる匿名型のオブジェクト

 しかし、「同じ名前、同じ型、同じ並び順」という条件が1つでも外れると型は互換にならない。

 例えば、以下のように名前が食い違えば異なる型になり、コンパイルは通らなくなる。

var 研修生情報 = new { 名前 = "網島研修生", 口座残高 = 39 };
var 振込後情報 = new { Name = "網島研修生", 口座残高 = 5000 };

 また、以下のように順番を変えてもコンパイル・エラーになる。

var 研修生情報 = new { 名前 = "網島研修生", 口座残高 = 39 };
var 振込後情報 = new { 口座残高 = 5000, 名前 = "網島研修生" };

 もちろん、型を変えてもコンパイル・エラーになる(「39」はInt32型、「5000u」はUInt32型になり、型が一致しない)。

var 研修生情報 = new { 名前 = "網島研修生", 口座残高 = 39 };
var 振込後情報 = new { 名前 = "網島研修生", 口座残高 = 5000u };

匿名型の簡易記法

 匿名型は、より少ない文字数で記述する簡易記法の一種であるが、さらに短く書くための手段が用意されている。

 例えば、あるメソッドで、引数として渡された値を使用して匿名型インスタンスを作りたいと思ったとしよう。通常の表記であれば、以下のように記述する。

using System;

class 座標
{
  public double 緯度;
  public double 経度;
}

class Program
{
  private static void Sample( string name, 座標 pos )
  {
    var a = new {
        補足 = "井の頭通り沿い",
        name = name,
        緯度 = pos.緯度,
        経度 = pos.経度
    };

    // ……
    // aを使用した何かの処理があるとする
    // ……

    Console.WriteLine("{0}, 緯度経度={1},{2}, {3}",
                        a.name, a.緯度, a.経度, a.補足);
  }

  static void Main(string[] args)
  {
    var pos = new 座標();
    pos.緯度 = 35.669569;
    pos.経度 = 139.657581;
    Sample("和田堀給水所", pos);
  }
}
リスト14 通常の匿名型インスタンスの書き方

 しかし匿名型の記述は、次のようにさらに簡素化できる。

var a = new { 補足 = "井の頭通り沿い", name, pos.緯度, pos.経度 };
リスト15 簡易表記による匿名型インスタンスの書き方

 このとおり、name、緯度、経度の記述が大幅に簡素化された。実は、=を使わないで名前のみを記述すると、以下のような解釈が行われる。

a  →  a = a

b.c  →  c = b.c

 これらは、指定された変数などの名前と値を持つプロパティを匿名型に生成する表記である。つまり、すでに存在する変数などがあって、使用したい名前と同じ名前で宣言されていれば、その名前を書くだけで匿名型に取り込まれるわけである。

 しかし、このような表記は、あくまで「同じ名前のプロパティ」を作る機能であり、値は初期化時にコピーされるにすぎないことに注意が必要である。つまり、基になった変数を書き換えても、その値が反映されるわけではない。以下にその例を示す。

using System;

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

    Console.WriteLine("a={0} x.a={1}", a, x.a);
    // 出力:a=123 x.a=123

    a = 456;
    Console.WriteLine("a={0} x.a={1}", a, x.a);
    // 出力:a=456 x.a=123
  }
}
リスト16 基になる変数を書き換えたとき

匿名型の使用目的

 さて、ここで「匿名型」は何に使えるのか、という問題について考えてみよう。

 実は、これは非常に難しい問題となる。なぜなら、JavaScriptのオブジェクト・リテラルと比較してあまりにも制約が厳しく、用途は著しく制限されてしまうからだ。それは、アバウトで危険な用法を、予防的に強く抑止する効能を持つ。

 そもそもC#は、「危険なポインタがあるおかしな言語」といった「無節操に何でもあり」と見られることがあるが、これはまったくの間違いである。典型的な知ったかぶりの発言なので、そのような発言を行う者はほかの発言も十分に疑ってかかる方がよいだろう。

 C#でのポインタは、何重にも使用制限、機能制限が設けられていて、C言語のポインタと同じようには扱うことができない。危険な行為に対する予防的な制約が厳しく設けられているわけである(その点で、ポインタが要求される低レベル操作を記述する際、C言語によるコード記述を要求するほかの言語よりもよほど安全である)。

 匿名型も同様であり、実質的に「局所的」かつ「変更不可能」なデータの集まりとして使用することしかできないようになっている。広域的なデータ交換にすら使用されるJavaScriptのオブジェクト・リテラルとはまったく違うわけである。

 では、匿名型は何に使えばよいのだろうか。

 1つのメソッド内で、データのまとまりを瞬時に差し替えて使う、というのが典型的な使い方だろうか。匿名型の値をメソッド外に持ち出すことは、object型にキャストすれば可能ではあるが、持ち出した値を活用することは現実的にはかなり難しいだろう。

 ただ、1つだけ可能性があるのはラムダ式(ないし匿名メソッド)である。ラムダ式で変数をキャプチャしてしまえば、ラムダ式内部からでも匿名型の値を扱うことができる。

using System;

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

    Action lambda = () =>
      {
        Console.WriteLine(a.b);
      };
    lambda(); // 出力:123
  }
}
リスト17 ラムダ式で匿名型を使う

 さらに型推論を使うと、ラムダ式の引数や戻り値に匿名型を使うことも不可能ではない。そこまでして、あえて匿名型を受け渡すことにどこまで意義があるかは分からないが、NyaRuRu氏がブログで詳しく解説されているので、興味のある方はそちらを参照願いたい。

NyaRuRuの日記:匿名型と型推論 (C# 3.0)

 さて、これほど制約の多い匿名型が存在する理由はいったい何だろうか? 上記のような面倒を抱え込んでまで、あえて使う機会がどれほどあるのだろうか?

 その答えは簡単である。

 それは、C# 3.0の目玉機能の1つであるLINQのselect句で使用するためである。LINQによるクエリの結果から、選択した情報だけをまとめる場合、そこでどうしても「select句で列挙した値だけを含むオブジェクト」を作り出す機能が必要とされるのだが、それは誰も明示的に名前を付けない匿名的な存在なのである。

 このあたりの詳しい話は、LINQを取り上げる際に再び扱う。


 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 記事ランキング

本日 月間