連載:C# 3.0入門

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

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

オブジェクト初期化子

 匿名型は、LINQと併用する場合を除き、出番はあまり多くないだろう。その理由は、匿名型に課せられた最大の制限が「匿名」ということにある。名前さえあれば、自由に受け渡して加工できる。

 ならば、名前のあるオブジェクトをもっと簡単に作り出す構文があればよいことになる。それがC# 3.0のオブジェクト初期化子ということになる。

 例えば、先ほどのリスト14を見ると、「座標」オブジェクトの初期化が回りくどいことが分かる(以下のリスト18、19に抜粋)。

class 座標
{
  public double 緯度;
  public double 経度;
}
リスト18 座標クラス

var pos = new 座標();
pos.緯度 = 35.669569;
pos.経度 = 139.657581;
リスト19 座標オブジェクトの初期化

 これは、オブジェクト初期化子を用いると以下のように簡略化できる。

var pos = new 座標() { 緯度 = 35.669569, 経度 = 139.657581 };
リスト20 オブジェクト初期化子で初期化する

 中カッコ内に書いた、「フィールド/プロパティ名 = 初期化式」というリストが、そのオブジェクトの該当フィールド/プロパティ名の値を初期化する。

 すべてのフィールド/プロパティ名について記述する必要はなく、書かなかったフィールド/プロパティはクラス側で用意した初期値で初期化される。

 コレクションもこの構文で初期化できる。以下、List<T>クラスをオブジェクト初期化子により初期化している例を示す。

using System;
using System.Collections.Generic;

class A
{
  public List<int> list = new List<int>();
}

class Program
{
  static void Main(string[] args)
  {
    var a = new A() { list = { 2, 3, 5, 7, 11, 13 } };

    a.list.ForEach((i) => { Console.WriteLine("{0}", i); });
    // 出力:
    // 2
    // 3
    // 5
    // 7
    // 11
    // 13
  }
}
リスト21 オブジェクト初期化子でコレクションを初期化

オブジェクト初期化子の本質とは?

 オブジェクトのフィールド/プロパティを初期化する手段をまとめると、おおむね以下の3つに分けられる。

フィールドの宣言時に初期値を指定する

コンストラクタで書き込む

オブジェクト初期化子を使う

 これらの性質の違いを把握することは、オブジェクト初期化子を使いこなすうえで重要なポイントになる。

 まず、初期化される順番だが、上記の の順に実行される。オブジェクト初期化子は最後に実行されるので、クラス内でどのような初期化を行っていても、最終的にそれらは上書きされ、オブジェクト初期化子で指定された値で確定する。

 以下のリスト22を実行すると、そのことが確認できるだろう。

using System;

class A
{
  public int Target = 1; // フィールドの宣言時に初期値を指定する

  public A()
  {
    Target = 2; // コンストラクタで書き込む
  }
}

class Program
{
  static void Main(string[] args)
  {
    A a = new A() { Target = 3 }; // オブジェクト初期化子を使う
    Console.WriteLine(a.Target); // 出力:3
  }
}
リスト22 初期化の順番を調べる

 さて、これらの間の相違はほかにもある。実は、オブジェクト初期化子はprivateなどのフィールドや、readonlyのフィールドなどは初期化できない。

using System;

class A
{
  private int Target1 = 1;
  public readonly int Target2 = 1;

  public A()
  {
    Target1 = 2;
    Target2 = 2;
  }
}

class Program
{
  static void Main(string[] args)
  {
    A a = new A()
    {
      Target1 = 3,
      // エラー  1  'A.Target1' はアクセスできない保護レベルに
      //             なっています。

      Target2 = 3,
      // エラー  2  読み取り専用フィールドに割り当てることは
      //            できません(コンストラクタ、変数初期化子では可)。
    };
    Console.WriteLine(a.Target1);
    // エラー  3  'A.Target1' はアクセスできない保護レベルに
    //             なっています。

    Console.WriteLine(a.Target2);
  }
}
リスト23 初期化できないケース

 なぜ「 フィールドの宣言時に初期値を指定する」「 コンストラクタで書き込む」と違って「 オブジェクト初期化子を使う」はprivateやreadonlyのフィールドを初期化できないのだろうか。

 その理由は、オブジェクト初期化子が、その名に反して「オブジェクト自身の初期化段階」の中で実行されないことにある。オブジェクト初期化子とは、オブジェクト外部に存在するものであり、オブジェクトの初期化が完了するまではオブジェクトに手出しができないのである。そして、外部からオブジェクトに手出しができる段階に入ると、もはやreadonlyのフィールドを書き換えることは許されない。また、あくまで外部から書き込みを行う立場であるから、privateなフィールドに手出しできないのも当然の成り行きといえる。

 しかし、これは悪いことばかりではない。オブジェクト初期化子がオブジェクトの外部に存在するということは、オブジェクト初期化子が存在する場所から利用できる資源はすべて利用できるからだ。

 例えば次のリスト24は、オブジェクト初期化子を記述したメソッド(Mainメソッド)の引数を利用して初期化を行っている。

using System;

class A
{
  public int Target;
}

class Program
{
  static void Main(string[] args)
  {
    A a = new A() { Target = args.Length };
    Console.WriteLine(a.Target);
  }
}
リスト24 実行ファイルのパラメータの数で初期化する

 オブジェクト初期化子なしで同様の値によりフィールドTargetを初期化するには、コンストラクタ経由で値を送り込むか、あるいはオブジェクト生成後に明示的にTargetの値を書き換えるしかない。

コレクションはreadonlyでも初期化できる

 例外的に、オブジェクト初期化子を使ってreadonlyのオブジェクトを初期化できるケースがある。コレクションの初期化は、コレクション自身を書き換えるわけではないのでreadonlyでも使用できるのである。読み出し専用プロパティでも同様である。

 この2つのケースでオブジェクト初期化子を使って初期化した例を以下に示す。

using System;
using System.Collections.Generic;

class A
{
  public readonly List<int> List1 = new List<int>();
  private List<int> list2 = new List<int>();

  public List<int> List2
  {
    get { return list2 ;}
  }
}

class Program
{
  static void Main(string[] args)
  {
    var a = new A() {
      List1 = { 2, 3, 5 },
      List2 = { 7, 11, 13 },
    };

    a.List1.ForEach((i) => { Console.WriteLine("{0}", i); });
    // 出力:
    // 2
    // 3
    // 5

    a.List2.ForEach((i) => { Console.WriteLine("{0}", i); });
    // 出力:
    // 7
    // 11
    // 13
  }
}
リスト25 readonlyフィールド、読み出し専用プロパティを初期化する

オブジェクト初期化子の使用例

 最後に、さるプログラムのために昨夜書いたばかりのコードを、テーマに沿わない部分を大幅に削除、修正を行ったうえで紹介する。あまりよいプログラムだとは思っていないが、オブジェクト初期化子を使うということが何を意味するかが浮き彫りになっていると思うので、掲載することにした。

using System;
using System.Collections.Generic;
using System.Reflection;

// スキルのレベルを整数で保管するクラス
// (このクラスはデータ保存のためシリアライズされる)
public static class Flags
{
  public static int スチル撮影;
  public static int 動画編集;
}

// 1つの種類のスキルを扱うクラス
public class Skill
{
  public readonly string Name;
  public readonly string Description;
  private readonly Func<int> getter;
  private readonly Action<int> setter;

  public static implicit operator Skill(SkillTemplate t)
  {
    return new Skill(t);
  }

  public Skill(SkillTemplate t)
  {
    Name = t.Name;
    Description = t.Description;
    getter = t.getter;
    setter = t.setter;
  }
}

// スキルを初期化するテンプレート
public class SkillTemplate
{
  public string Name;
  public string Description;
  public Func<int> getter;
  public Action<int> setter;
}

// スキルの一覧
public static class Skills
{
  public static Skill スチル撮影 = new SkillTemplate()
  {
    Name = "スチル撮影",
    Description = "静止画撮影を行うスキル",
    getter = () => Flags.スチル撮影,
    setter = (v) => { Flags.スチル撮影 = v; },
  };

  public static Skill 動画編集 = new SkillTemplate()
  {
    Name = "動画編集",
    Description = "動画データの編集を行うスキル",
    getter = () => Flags.動画編集,
    setter = (v) => { Flags.動画編集 = v; },
  };

  public static Skill[] GetSkillList()
  {
    List<Skill> list = new List<Skill>();
    foreach (FieldInfo info in typeof(Skills).GetFields(
                    BindingFlags.Public | BindingFlags.Static))
    {
      Skill item = (Skill)info.GetValue(null);
      list.Add(item);
    }
    return list.ToArray();
  }
}
リスト26 オブジェクト初期化子の使用例

 このプログラムは「スチル撮影」といった個人の能力を「スキル」という形でモデル化してオブジェクトに格納している。スキルの具体的な内容はあらかじめ決まっていて不変なので、Skillオブジェクトはreadonlyのフィールドでのみ構成されている(変化するスキルの能力値は別のFlagsクラスに格納している)。

 ここで問題になるのは、オブジェクト初期化子はreadonlyのフィールドを初期化できないことである。そこで、Skillクラスとは別に、同等の情報を自由に読み書きできるSkillTemplateクラスを用意し、これを初期化する。そして、SkillTemplateクラスからSkillクラスへの暗黙的な変換(implicit operator Skill)を用意し、

Skill スチル撮影 = new SkillTemplate() { …… };

という異なる型の代入を可能としている。これにより、SkillTemplateクラスを用意しなければならなかった点を除けば、割とすっきりとソース・コードをまとめることができた。

 しかし、SkillTemplateクラスの必要性はあまり好ましいものではない。どのようにすればきれいにコードがまとまるかは、まだ完全な結論が出ていない。大幅にソース・コードをすっきりまとめられるが、モヤモヤした感覚が残る、というのが筆者なりのオブジェクト初期化子の感想である。

次回予告

 Visual Studio 2008でC#プログラミングを行っていると、コレクションのクラスに対してIntelliSenseを機能させたときに表れる項目が少なすぎたり多すぎたりして驚くことはないだろうか? 事実として、ソース・コード先頭のusing文1行の有無によってIntelliSenseの表示項目数が増減するのである。

 これはC# 3.0の新機能となる「拡張メソッド」が引き起こしている状況である。次回はこのミステリアスな機能について取り上げたいと考えている。End of Article

 

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

本日 月間