initとwithによる不変性
init専用setterで不変オブジェクトを作成し、with式で破壊的変更を行わずにコピーを生成します。
「initとwithによる不変性」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
不変性とは
不変オブジェクトは、作成後に変更できません。不変性により、意図しない変更を防ぎ、オブジェクトは既定でスレッドセーフになり、コードの理解も容易になります。C# には、不変性を扱いやすくするための init セッターと with 式が用意されています。
init 専用セッター
init アクセサーを使用すると、オブジェクトの初期化時(コンストラクターまたはオブジェクト初期化子)にプロパティを設定できますが、その後は設定できません。これは set に相当する不変版です。
public class Point
{
public double X { get; init; }
public double Y { get; init; }
}
var p = new Point { X = 3.0, Y = 4.0 }; // OK — init phase
p.X = 5.0; // COMPILE ERROR — cannot set after init
// All of these are valid init-phase assignments:
var p2 = new Point(X: 1.0, Y: 2.0);
var p3 = new Point { X = 0, Y = 0 };レコードの位置指定プロパティにおける init
位置指定レコードのプロパティは、既定で init 専用です。これによってレコードが不変になります。コンパイラーは各位置指定パラメーターに対して get; init; を生成します。
// This record:
public record Person(string Name, int Age);
// Is equivalent to:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string Name, int Age) { this.Name = Name; this.Age = Age; }
// + Deconstruct, Equals, GetHashCode, ToString
}
var alice = new Person("Alice", 30);
alice.Name = "Bob"; // COMPILE ERRORwith 式: 非破壊的な変更
with 式は、特定のプロパティを変更したレコードのコピーを作成します。元のレコードは変更されないため、真の不変性が保たれます。
public record Person(string Name, int Age, string Email);
var alice = new Person("Alice", 30, "alice@example.com");
// Create a modified copy — alice is unchanged
var olderAlice = alice with { Age = 31 };
var renamed = alice with { Name = "Alicia", Email = "alicia@example.com" };
Console.WriteLine(alice.Name); // Alice (unchanged)
Console.WriteLine(olderAlice.Age); // 31
Console.WriteLine(renamed.Name); // Aliciaレコード以外の型での with
C# 10 では、コピーコンストラクターのセマンティクスを持つ任意の構造体やクラスで with を使用できます。ただし、最も自然に使えるのはレコードです。クラスでは手動で実装する必要があります。
// struct with with-expression:
public struct Temperature
{
public double Celsius { get; init; }
public double Fahrenheit => Celsius * 9 / 5 + 32;
}
var t1 = new Temperature { Celsius = 20 };
var t2 = t1 with { Celsius = 25 }; // copy with change
Console.WriteLine(t1.Celsius); // 20 — unchanged
Console.WriteLine(t2.Celsius); // 25with 式のチェーン
複数の with 式をチェーンして複雑な変換を組み立てられます。各ステップで、直前の値から新しい不変値が作成されます。
public record Order(int Id, string Status, decimal Total, DateTime UpdatedAt);
var order = new Order(42, "Pending", 99.99m, DateTime.UtcNow);
// Apply a promotion discount then mark as confirmed
var finalOrder = order
with { Total = order.Total * 0.9m } // 10% off
with { Status = "Confirmed" }
with { UpdatedAt = DateTime.UtcNow };
Console.WriteLine(order.Status); // Pending (original unchanged)
Console.WriteLine(finalOrder.Status); // Confirmed不変型の計算プロパティ
不変型の派生プロパティは、自然に純粋なものになります。固定されたプロパティ値から計算され、同じ入力に対して常に同じ結果を返します。
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return this with { Amount = Amount + other.Amount };
}
public Money Multiply(decimal factor) =>
this with { Amount = Amount * factor };
public override string ToString() =>
$"{Amount:F2} {Currency}";
}
var price = new Money(10.00m, "USD");
var tax = price.Multiply(0.08m);
var total = price.Add(tax);
Console.WriteLine(total); // 10.80 USD不変コレクション
レコードを ImmutableList<T> や System.Collections.Immutable のその他の型と組み合わせることで、完全に不変なデータ構造を作成できます。
using System.Collections.Immutable;
public record ShoppingCart(
string UserId,
ImmutableList<CartItem> Items)
{
public ShoppingCart AddItem(CartItem item) =>
this with { Items = Items.Add(item) };
public ShoppingCart RemoveItem(int productId) =>
this with { Items = Items.RemoveAll(i => i.ProductId == productId) };
public decimal Total => Items.Sum(i => i.Price * i.Quantity);
}不変性によるスレッドセーフ
不変オブジェクトは本質的にスレッドセーフです。構築後に状態が変化しないため、複数のスレッド間で共有する際に同期処理は必要ありません。
// Immutable config record shared across all threads safely
public record AppConfig(
string ConnectionString,
int MaxRetries,
TimeSpan Timeout);
// Register as singleton — safe because record is immutable
builder.Services.AddSingleton(
new AppConfig(
ConnectionString: config["DB"]!,
MaxRetries: 3,
Timeout: TimeSpan.FromSeconds(30)));
// Any thread can read this simultaneously without locks実践例: 関数型イベントソーシング
不変レコードはイベントソーシングと自然に組み合わせられます。各ドメインイベントは不変であり、イベントを畳み込むことで状態を導出するため、変更による予期しない問題がありません。
public record OrderState(
int Id,
string Status = "Draft",
decimal Total = 0m);
public static OrderState Apply(OrderState state, object evt) => evt switch
{
OrderPlaced e => state with { Status = "Pending", Total = e.Total },
OrderShipped _ => state with { Status = "Shipped" },
OrderCancelled _ => state with { Status = "Cancelled" },
_ => state
};
// Fold events to get current state:
var state = events.Aggregate(
new OrderState(id),
Apply);クイックチェック
'with' 式は元のレコードに対して何を行いますか。
まとめ: init と with による不変性
重要なポイント:
initアクセサー: 初期化中のみ設定でき、その後は設定できません- レコードの位置指定プロパティは、既定で
init専用です with式: 変更したコピーを作成します。元の値は変更されません- 複数段階の変換には
with式をチェーンして使用します - 不変型は同期処理なしでスレッドセーフになります
ImmutableList<T>と組み合わせて、完全に不変なオブジェクトグラフを作成できます
よくある質問
「initとwithによる不変性」レッスンは無料ですか?
はい。「initとwithによる不変性」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「initとwithによる不変性」で何を学びますか?
init専用setterで不変オブジェクトを作成し、with式で破壊的変更を行わずにコピーを生成します。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「initとwithによる不変性」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Record型:基礎と構文
- initとwithによる不変性
- 値の等価性と分解
- ドメイン駆動設計におけるRecord