前回の記事ではプロパティの基本構文を整理しました。本記事では init アクセサ、required 修飾子、抽象・仮想プロパティ、インターフェースプロパティ、インデクサなど、応用的な機能をまとめます。
init アクセサ(C# 9.0〜)
init は set の代わりに使えるアクセサで、オブジェクト初期化子での設定のみを許可します。初期化後は変更できない「初期化時限定の書き込み」を実現します。
public class Person
{
public string Name { get; init; }
public int Age { get; init; }
}
// オブジェクト初期化子で設定 — OK
var p = new Person { Name = "Alice", Age = 30 };
// 初期化後の変更 — コンパイルエラー
// p.Name = "Bob";
set と init の違い
| 特徴 | set |
init |
|---|---|---|
| コンストラクタ内 | ✅ | ✅ |
| オブジェクト初期化子 | ✅ | ✅ |
| 初期化後の代入 | ✅ | ❌ |
| 不変性の保証 | なし | 初期化後は不変 |
init は record 型との相性が良く、イミュータブルなデータモデルを作る際に重宝します。
init アクセサでバリデーション
init にも set と同様にロジックを記述できます。
public class Product
{
private string _sku = "";
public string Sku
{
get => _sku;
init
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("SKU は空にできません。");
_sku = value;
}
}
}
var product = new Product { Sku = "ABC-123" }; // OK
// var bad = new Product { Sku = "" }; // ArgumentException
required 修飾子(C# 11〜)
required を付けたプロパティは、オブジェクト初期化子での設定が必須になります。コンストラクタのパラメータなしに「設定漏れ」を防げます。
public class User
{
public required string Email { get; set; }
public required string DisplayName { get; set; }
public string? Bio { get; set; } // こちらは任意
}
// Email と DisplayName は必ず設定する必要がある
var user = new User
{
Email = "alice@example.com",
DisplayName = "Alice"
};
// Email を省略するとコンパイルエラー
// var bad = new User { DisplayName = "Bob" };
required と init の組み合わせ
required と init を組み合わせると「初期化時に必ず設定し、以降は変更不可」という制約を表現できます。
public class Order
{
public required string OrderId { get; init; }
public required DateTime CreatedAt { get; init; }
public string? Note { get; set; }
}
var order = new Order
{
OrderId = "ORD-001",
CreatedAt = DateTime.UtcNow,
Note = "急ぎ"
};
// order.OrderId = "ORD-002"; // コンパイルエラー(init のため変更不可)
コンストラクタで required を満たす
コンストラクタ内でプロパティを設定する場合、SetsRequiredMembers 属性を付けることで required の制約をコンストラクタが満たしていることを宣言できます。
using System.Diagnostics.CodeAnalysis;
public class User
{
public required string Email { get; set; }
public required string DisplayName { get; set; }
public User() { }
[SetsRequiredMembers]
public User(string email, string displayName)
{
Email = email;
DisplayName = displayName;
}
}
// コンストラクタ経由 — required の警告なし
var user = new User("alice@example.com", "Alice");
抽象プロパティ(abstract)
abstract プロパティは基底クラスでシグネチャだけを宣言し、派生クラスで実装を強制します。
public abstract class Shape
{
public abstract double Area { get; }
public abstract string Name { get; }
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public override double Area => Math.PI * Radius * Radius;
public override string Name => "Circle";
}
public class Square : Shape
{
public double Side { get; }
public Square(double side) => Side = side;
public override double Area => Side * Side;
public override string Name => "Square";
}
abstract プロパティは抽象クラス(abstract class)内でのみ宣言でき、派生クラスは override で実装を提供する必要があります。
仮想プロパティ(virtual / override)
virtual プロパティはデフォルト実装を持ちつつ、派生クラスで上書き可能にします。
public class Animal
{
public virtual string Sound => "...";
}
public class Dog : Animal
{
public override string Sound => "Woof!";
}
public class Cat : Animal
{
public override string Sound => "Meow!";
}
Animal animal = new Dog();
Console.WriteLine(animal.Sound); // Woof!
インターフェースプロパティ
インターフェースにプロパティを定義すると、実装クラスにプロパティの提供を強制できます。
public interface IIdentifiable
{
string Id { get; }
}
public interface ITimestamped
{
DateTime CreatedAt { get; }
DateTime? UpdatedAt { get; set; }
}
public class Article : IIdentifiable, ITimestamped
{
public string Id { get; } = Guid.NewGuid().ToString();
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
デフォルト実装(C# 8.0〜)
C# 8.0 以降、インターフェースプロパティにもデフォルト実装を持たせることができます。
public interface IVersioned
{
int Version => 1; // デフォルト実装
}
public class Document : IVersioned
{
// Version を実装しなくてもコンパイル可能
// IVersioned 経由でアクセスするとデフォルト値 1 が返る
}
IVersioned doc = new Document();
Console.WriteLine(doc.Version); // 1
ただしデフォルト実装はインターフェース型の参照を通じてのみアクセス可能です。Document 型の変数から直接 Version を呼ぶことはできません。
インデクサ(Indexer)
インデクサはオブジェクトに [](インデックス演算子)でアクセスできるようにする特殊なプロパティです。this[...] で定義します。
public class Sentence
{
private string[] _words;
public Sentence(string text)
{
_words = text.Split(' ');
}
// インデクサ
public string this[int index]
{
get => _words[index];
set => _words[index] = value;
}
public int Length => _words.Length;
public override string ToString() => string.Join(" ", _words);
}
var sentence = new Sentence("Hello World Today");
Console.WriteLine(sentence[0]); // Hello
sentence[1] = "C#";
Console.WriteLine(sentence); // Hello C# Today
文字列キーのインデクサ
インデクサの引数は int に限りません。string やその他の型も使えます。
public class HttpHeaders
{
private readonly Dictionary<string, string> _headers = new(StringComparer.OrdinalIgnoreCase);
public string? this[string name]
{
get => _headers.TryGetValue(name, out var value) ? value : null;
set
{
if (value is null)
_headers.Remove(name);
else
_headers[name] = value;
}
}
}
var headers = new HttpHeaders();
headers["Content-Type"] = "application/json";
headers["Authorization"] = "Bearer token123";
Console.WriteLine(headers["content-type"]); // application/json(大文字小文字を無視)
複数パラメータのインデクサ
インデクサには複数のパラメータを定義することもできます。
public class Matrix
{
private readonly double[,] _data;
public Matrix(int rows, int cols)
{
_data = new double[rows, cols];
}
public double this[int row, int col]
{
get => _data[row, col];
set => _data[row, col] = value;
}
}
var m = new Matrix(2, 2);
m[0, 0] = 1.0;
m[0, 1] = 2.0;
m[1, 0] = 3.0;
m[1, 1] = 4.0;
Console.WriteLine(m[1, 1]); // 4
プロパティの各機能と対応バージョン
| 機能 | C# バージョン |
|---|---|
| プロパティ基本構文 | 1.0 |
| 自動実装プロパティ | 3.0 |
| 自動実装プロパティの初期値 | 6.0 |
| get のみの自動実装プロパティ | 6.0 |
| Expression-Bodied プロパティ | 6.0(get)/ 7.0(get + set) |
| インターフェースのデフォルト実装 | 8.0 |
init アクセサ |
9.0 |
required 修飾子 |
11 |
まとめ
initアクセサはオブジェクト初期化子での設定のみを許可し、初期化後の変更を防ぐrequired修飾子でプロパティの設定漏れをコンパイル時に検出できるrequired+initで「必須かつ不変」なプロパティを定義できる- 抽象プロパティで派生クラスに実装を強制、仮想プロパティでデフォルト実装を提供できる
- インターフェースプロパティで契約としてプロパティを要求でき、C# 8.0 からはデフォルト実装も可能
- インデクサで
[]を使った直感的なアクセスを独自クラスに追加できる