C# ではオブジェクトを生成・初期化する方法が複数あり、バージョンを重ねるごとに選択肢が増えてきました。本記事では 呼び出し側の視点 から、初期化パターンを体系的に整理します。
コンストラクタの定義側の詳細はコンストラクタの記事、プロパティの init / required の宣言側の詳細はプロパティ応用編を参照してください。
初期化パターンの全体像
まず、典型的なクラスに対する初期化パターンを一覧で示します。
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point() { }
public Point(int x, int y) { X = x; Y = y; }
}
// 1. パラメーターなしコンストラクタ
var p1 = new Point(); // X=0, Y=0
// 2. パラメーターありコンストラクタ
var p2 = new Point(20, 20); // X=20, Y=20
// 3. オブジェクト初期化子
var p3 = new Point { X = 30, Y = 30 }; // X=30, Y=30
// 4. コンストラクタ + オブジェクト初期化子
var p4 = new Point(10, 10) { X = 40 }; // X=40, Y=10(X が上書きされる)
それぞれのパターンを詳しく見ていきます。
コンストラクタによる初期化
最も基本的なパターンです。コンストラクタのパラメーターで値を渡して初期化します。
public class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
var alice = new Person("Alice", 30);
コンストラクタは必ず設定してほしい値を強制できるのが利点です。パラメーターを渡さなければコンパイルエラーになるため、初期化漏れを防げます。
オプション引数との組み合わせ
オプション引数を使うと、一部のパラメーターを省略可能にできます。
public class ConnectionConfig
{
public string Host { get; }
public int Port { get; }
public bool UseSsl { get; }
public ConnectionConfig(string host, int port = 443, bool useSsl = true)
{
Host = host;
Port = port;
UseSsl = useSsl;
}
}
var c1 = new ConnectionConfig("example.com"); // port=443, useSsl=true
var c2 = new ConnectionConfig("example.com", 8080); // useSsl=true
var c3 = new ConnectionConfig("example.com", useSsl: false); // 名前付き引数で指定
オブジェクト初期化子(C# 3.0〜)
{ } の中でプロパティを自由に設定するパターンです。コンストラクタのオーバーロードを増やさずに、任意のプロパティを組み合わせて初期化できます。
public class UserProfile
{
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public int Age { get; set; }
public string? Bio { get; set; }
}
// 必要なプロパティだけ設定できる
var u1 = new UserProfile { Name = "Alice" };
var u2 = new UserProfile { Name = "Bob", Email = "bob@example.com", Age = 25 };
実行順序
オブジェクト初期化子はコンストラクタの 後 に実行されます。
public class Demo
{
public int Value { get; set; }
public Demo()
{
Value = 100;
Console.WriteLine($"コンストラクタ: {Value}"); // 100
}
}
// 1. コンストラクタ(Value = 100)
// 2. オブジェクト初期化子(Value = 200 で上書き)
var d = new Demo { Value = 200 };
Console.WriteLine(d.Value); // 200
この順序を理解しておくと、コンストラクタとオブジェクト初期化子を併用した場合の挙動が明確になります。
ネストしたオブジェクトの初期化
オブジェクト初期化子はネストもできます。
public class Address
{
public string City { get; set; } = "";
public string Street { get; set; } = "";
}
public class Customer
{
public string Name { get; set; } = "";
public Address Address { get; set; } = new();
}
var customer = new Customer
{
Name = "Alice",
Address =
{
City = "Tokyo", // customer.Address.City = "Tokyo" と等価
Street = "Shibuya"
}
};
Address = { ... } は既存の Address インスタンスのプロパティを設定します(new Address { ... } とは異なり、新しいインスタンスは作りません)。Address が null だと NullReferenceException になるため、初期値を設定しておく必要があります。
新しいインスタンスで置き換えたい場合は明示的に new を書きます。
var customer = new Customer
{
Name = "Bob",
Address = new Address { City = "Osaka", Street = "Umeda" }
};
コレクション初期化子(C# 3.0〜)
IEnumerable<T> を実装し Add メソッドを持つ型には、コレクション初期化子を使えます。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var people = new List<Person>
{
new("Alice", 30),
new("Bob", 25),
new("Charlie", 35)
};
Dictionary の初期化
Dictionary は 2 通りの構文で初期化できます。
// コレクション初期化子(C# 3.0〜)
var dict1 = new Dictionary<string, int>
{
{ "apple", 100 },
{ "banana", 200 }
};
// インデクサ初期化子(C# 6.0〜)
var dict2 = new Dictionary<string, int>
{
["apple"] = 100,
["banana"] = 200
};
インデクサ初期化子は内部的に dict2["apple"] = 100 を呼ぶため、同一キーの場合は後勝ちになります。コレクション初期化子は Add を呼ぶため、同一キーは例外になります。
init アクセサで初期化後の変更を防ぐ(C# 9.0〜)
set の代わりに init を使うと、オブジェクト初期化子での設定は許可するが、初期化後の変更は禁止できます。
public class Point
{
public int X { get; init; }
public int Y { get; init; }
}
// オブジェクト初期化子 — OK
var p = new Point { X = 10, Y = 20 };
// 初期化後の変更 — コンパイルエラー
// p.X = 30;
set vs init — 呼び出し側の違い
public class MutablePoint
{
public int X { get; set; }
public int Y { get; set; }
}
public class ImmutablePoint
{
public int X { get; init; }
public int Y { get; init; }
}
// 初期化はどちらも同じ
var mp = new MutablePoint { X = 10, Y = 20 };
var ip = new ImmutablePoint { X = 10, Y = 20 };
// 初期化後
mp.X = 100; // OK(set なので変更可能)
// ip.X = 100; // コンパイルエラー(init なので変更不可)
init はコンストラクタ呼び出しとオブジェクト初期化子のいいとこ取りです。「初期化子の柔軟さ」と「初期化後の不変性」を両立できます。
init + コンストラクタの併用
コンストラクタでデフォルト値を設定しつつ、オブジェクト初期化子で上書きを許可するパターンです。
public class Config
{
public string Locale { get; init; }
public int Timeout { get; init; }
public Config()
{
Locale = "ja-JP";
Timeout = 30;
}
}
// デフォルト値をそのまま使う
var c1 = new Config();
// 一部だけ上書き
var c2 = new Config { Timeout = 60 };
// 初期化後は変更不可
// c2.Timeout = 90; // コンパイルエラー
required で初期化漏れを防ぐ(C# 11〜)
required を付けたプロパティは、オブジェクト初期化子(またはコンストラクタ)での設定がコンパイル時に強制されます。
public class Order
{
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
// OK — required プロパティがすべて設定されている
var order = new Order
{
OrderId = "ORD-001",
CustomerId = "CUST-100"
};
// コンパイルエラー — CustomerId が未設定
// var bad = new Order { OrderId = "ORD-002" };
初期化パターンの安全性比較
| パターン | 初期化漏れ | 初期化後の変更 |
|---|---|---|
{ get; set; } |
検出できない | 変更可能 |
{ get; init; } |
検出できない | 変更不可 |
required ... { get; set; } |
コンパイルエラー | 変更可能 |
required ... { get; init; } |
コンパイルエラー | 変更不可 |
| コンストラクタ引数 | コンパイルエラー | プロパティ次第 |
もっとも厳格なのは required ... { get; init; } で、「必ず設定し、かつ変更できない」という制約をコンパイル時に保証できます。
target-typed new(C# 9.0〜)
型が推論できる文脈では new(...) と型名を省略できます。
Point p1 = new(10, 20); // new Point(10, 20) と等価
List<string> names = new() { "A", "B" }; // new List<string>() と等価
// フィールド・プロパティの初期化
class Canvas
{
private Point _origin = new(0, 0);
private List<Point> _points = new();
}
メソッド引数や return 文でも型が確定していれば使えます。
void Draw(Point p) { /* ... */ }
Draw(new(10, 20));
Point CreateOrigin() => new(0, 0);
with 式でコピー&変更する(C# 9.0〜)
with 式は既存オブジェクトのコピーを作り、指定したプロパティだけ変更する構文です。もとのオブジェクトは変更されません。
record での with 式
public record Point(int X, int Y);
var p1 = new Point(10, 20);
var p2 = p1 with { X = 30 }; // Y はそのまま
Console.WriteLine(p1); // Point { X = 10, Y = 20 }(変更されない)
Console.WriteLine(p2); // Point { X = 30, Y = 20 }
struct での with 式(C# 10〜)
C# 10 以降、struct にも with 式が使えます。
public struct Size
{
public int Width { get; init; }
public int Height { get; init; }
}
var s1 = new Size { Width = 100, Height = 200 };
var s2 = s1 with { Width = 300 };
Console.WriteLine(s2.Width); // 300
Console.WriteLine(s2.Height); // 200
with 式はイミュータブルなオブジェクトを扱う際に「一部だけ変えた新しいインスタンスを作る」パターンを簡潔に書けます。
匿名型(Anonymous Type)
型を明示的に定義せずに、プロパティだけを持つ軽量なオブジェクトを作れます。C# 3.0 で LINQ と共に導入されました。
var point = new { X = 10, Y = 20 };
Console.WriteLine(point.X); // 10
// point.X = 30; // コンパイルエラー(匿名型のプロパティは読み取り専用)
匿名型のプロパティはすべて 読み取り専用(init 相当)です。メソッドの戻り値やクラス間の受け渡しには向かないため、用途は LINQ のクエリ内や一時的なデータのグルーピングに限定されます。
パターン別の使い分けガイド
| シナリオ | 推奨パターン |
|---|---|
| 必須パラメーターが少ない(1〜3 個) | コンストラクタ引数 |
| オプションのプロパティが多い | オブジェクト初期化子 |
| 必須 + オプションの混在 | コンストラクタ + オブジェクト初期化子 |
| 初期化後は変更させたくない | init アクセサ |
| 初期化漏れを防ぎたいがコンストラクタは使わない | required + init |
| イミュータブルなデータモデル | record または required init |
| 一部だけ変えたコピーが欲しい | with 式 |
| 型定義なしに一時的にまとめたい | 匿名型 |
まとめ
- コンストラクタは必須パラメーターの強制に向くが、オプション項目が増えるとオーバーロードが膨らむ
- オブジェクト初期化子は任意のプロパティを柔軟に設定でき、コンストラクタの後に実行される
initアクセサで「初期化子では設定できるが、初期化後は変更不可」を実現できるrequired修飾子で初期化漏れをコンパイル時に検出でき、initと組み合わせると最も厳格な制約になるwith式でイミュータブルなオブジェクトの一部だけを変えたコピーを簡潔に作れる- target-typed new で型が自明な場面の冗長性を削減できる