近年、C#は関数型プログラミングの長所を積極的に取り入れ、より安全で宣言的な記述ができる言語へと進化を続けています。

その中でもC# 9.0で導入され、C# 10.0でさらに強化されたwith式(非破壊的代入)は、モダンなC#開発において欠かせない機能の一つとなりました。

オブジェクトの状態を直接書き換えるのではなく、一部の値を変更した新しいインスタンスを生成するというアプローチは、プログラムの予測可能性を高め、バグの混入を防ぐ強力な武器となります。

本記事では、with式の基本的な使い方から、record型や構造体での活用メリット、内部的な動作の仕組みまで、プロフェッショナルな視点で詳しく解説します。

with式とは何か:不変性と非破壊的代入の基本

C#におけるwith式とは、既存のオブジェクトをベースにして、特定のプロパティやフィールドの値だけを書き換えた「新しいオブジェクト」を生成するための構文です。

この操作は、元のオブジェクトの状態を変更しないため、非破壊的代入(Non-destructive mutation)と呼ばれます。

従来のオブジェクト指向プログラミングでは、オブジェクトの状態を変更する際にプロパティに新しい値を直接代入する「破壊的変更」が一般的でした。

しかし、この手法では複数の場所から参照されているオブジェクトが意図せず書き換わってしまう「副作用」のリスクが常に付きまといます。

with式を利用することで、不変性(イミュータビリティ)を保ったまま、状態の変化を表現できるようになります。

これは特にマルチスレッド環境や、データの整合性が重要視されるドメイン駆動設計(DDD)などの文脈で極めて重要な役割を果たします。

record型におけるwith式の活用

with式が最も頻繁に、そして強力に利用されるのがrecord型(レコード型)です。

レコード型は元々、データの保持に特化した型であり、既定で不変(Immutable)な設計が推奨されています。

基本的な構文と動作

レコード型でwith式を使用する場合の基本的な書き方は非常にシンプルです。

対象となるインスタンスの後に with キーワードを続け、波括弧 {} 内に変更したいプロパティを記述します。

C#
using System;

// レコード型の定義(不変なプロパティを持つ)
public record User(string Name, int Age, string Role);

public class Program
{
    public static void Main()
    {
        // オリジナルのインスタンス作成
        User admin = new User("Alice", 30, "Administrator");

        // with式を使用して、Roleだけを変更した新しいインスタンスを作成
        User manager = admin with { Role = "Manager" };

        // 出力して結果を確認
        Console.WriteLine($"Admin: {admin}");
        Console.WriteLine($"Manager: {manager}");

        // 元のインスタンスと新しいインスタンスは別物であることを確認
        Console.WriteLine($"同一インスタンスか: {ReferenceEquals(admin, manager)}");
    }
}
実行結果
Admin: User { Name = Alice, Age = 30, Role = Administrator }
Manager: User { Name = Alice, Age = 30, Role = Manager }
同一インスタンスか: False

上記のコードからわかるように、admin オブジェクトの内容は書き換わっておらず、Role だけが異なる新しい manager オブジェクトが生成されています。

これが非破壊的代入の真髄です。

record classとrecord structの違い

C# 10.0からは、参照型である record class に加えて、値型である record struct も導入されました。

どちらの型でもwith式を利用可能ですが、その性質には若干の違いがあります。

特徴record classrecord struct
型の種類参照型(Heap領域)値型(Stack領域)
既定の不変性推奨(init専用プロパティ)可変・不変どちらも定義可能
with式の動作内部的なCloneメソッド呼び出しビット単位のコピーに近い動作

record struct を使用する場合、特にメモリ効率が求められる小規模なデータ構造において、with式による簡潔な記述と値型のパフォーマンスを両立させることができます。

構造体(struct)と匿名型でのwith式

C# 10.0における重要なアップデートの一つに、通常の構造体(struct)および匿名型でのwith式のサポートがあります。

これにより、レコード型に限らず幅広いシーンで非破壊的代入のメリットを享受できるようになりました。

構造体におけるC# 10以降の挙動

従来の構造体は値型であるため、代入時や引数渡し時にコピーが発生しますが、特定のプロパティだけを変更したコピーを作るには、一度変数に代入してからプロパティを書き換える手順が必要でした。

with式を使えば、これを一行で記述できます。

C#
public struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

public class Example
{
    public void Process()
    {
        Point p1 = new Point { X = 10, Y = 20 };
        
        // C# 10.0以降、通常の構造体でもwith式が使える
        Point p2 = p1 with { Y = 50 };
        
        // p1は (10, 20)、p2は (10, 50) となる
    }
}

匿名型でのプロパティ更新

LINQのクエリ結果などで多用される匿名型でもwith式は有効です。

匿名型は読み取り専用であるため、一部の値を変更したい場合にはwith式が唯一のスマートな解決策となります。

C#
var person = new { Name = "John", Age = 25 };

// 匿名型をベースに一部変更した新しい匿名型を生成
var olderPerson = person with { Age = 26 };

Console.WriteLine(olderPerson);
// 出力: { Name = John, Age = 26 }

with式の内部動作:シャローコピー(浅いコピー)の注意点

with式を正しく使いこなすためには、その内部的な挙動を理解しておく必要があります。

最も重要な点は、with式が実行するコピーはシャローコピー(Shallow Copy:浅いコピー)であるという事実です。

参照型プロパティを含む場合の挙動

レコードや構造体のメンバに参照型(クラスや配列など)が含まれている場合、with式はその「参照(メモリアドレス)」をコピーします。

そのため、コピー先のオブジェクトでその参照型メンバの中身を書き換えると、コピー元のオブジェクトにも影響が及びます。

C#
using System;
using System.Collections.Generic;

public record Team(string TeamName, List<string> Members);

public class Program
{
    public static void Main()
    {
        var teamA = new Team("DevGroup", new List<string> { "Alice", "Bob" });

        // teamAをベースにTeamNameだけ変えたteamBを作成
        var teamB = teamA with { TeamName = "Marketing" };

        // teamBのMembers(参照型)に要素を追加
        teamB.Members.Add("Charlie");

        // teamAのMembersも影響を受けてしまう(参照が同じであるため)
        Console.WriteLine($"Team A Members Count: {teamA.Members.Count}"); 
        // 出力結果: Team A Members Count: 3
    }
}

このように、ネストされた参照型オブジェクトまでは新しく生成されないため、コレクションなどを保持する場合は注意が必要です。

深いコピー(Deep Copy)が必要な場合は、コンストラクタやファクトリメソッドを手動で定義するなどの対策を検討してください。

コンパイラが生成する「clone」メソッド

record classにおいて、with式がどのように実現されているかというと、コンパイラが背後で特別なCloneメソッドを生成しています。

  1. コンパイラはレコード型に対して、自分自身のコピーを返す非公開の Clone メソッドを作成します。
  2. with式が呼ばれると、まずこの Clone メソッドが呼び出され、インスタンスの全フィールドがコピーされます。
  3. その後、with式の波括弧内で指定されたプロパティに対して、オブジェクト初期化子と同様の仕組みで値が上書きされます。

このプロセスにより、実行時の型安全性を保ちつつ、効率的なオブジェクトの複製が可能になっています。

実践的な活用シーンとメリット

なぜ、プログラミングにおいてこれほどまでにwith式が推奨されるのでしょうか。

その具体的なメリットと活用シーンを整理します。

不変性の維持によるスレッドセーフな設計

マルチスレッドプログラミングにおいて、共有されたオブジェクトの状態が書き換わることはバグの最大の原因となります。

すべてのオブジェクトを不変(Immutable)にし、状態の変化を「新しいオブジェクトの生成」として扱うことで、ロック制御の不要なスレッドセーフな設計が容易になります。

DDD(ドメイン駆動設計)における値オブジェクト

DDDにおける「値オブジェクト(Value Object)」は、その属性の組み合わせによって識別されるオブジェクトであり、一度作成されたら変更されないことが望ましいとされています。

例えば「通貨と金額を保持するMoneyクラス」や「期間を表すDateRangeクラス」などは、with式と非常に相性が良いです。

C#
public record Money(decimal Amount, string Currency);

var price = new Money(1000, "JPY");
// 割引適用後の価格を新しいインスタンスとして表現
var discountedPrice = price with { Amount = price.Amount * 0.9m };

設定値の管理と部分更新

アプリケーションの構成設定(Configuration)を保持するクラスにおいても、with式は有用です。

基本設定をベースにしつつ、特定の環境(本番環境やテスト環境)向けに一部のパラメータだけを上書きした設定オブジェクトを作る際に、コードの可読性が飛躍的に向上します。

with式を使用する際の注意点と制限

非常に便利なwith式ですが、万能ではありません。

使用にあたっての制限事項も把握しておきましょう。

通常のクラス(class)では使用不可

残念ながら、recordを付与しない通常のクラス(class)ではwith式を使用することができません

クラスで同様の機能を実現したい場合は、手動でクローンメソッドを実装するか、そのクラスをレコード型(record class)に書き換える必要があります。

既存の巨大なクラスライブラリを扱う際には注意してください。

大規模なデータ構造でのパフォーマンス

with式は常に「新しいインスタンス」を生成します。

そのため、非常に巨大なプロパティを持つ構造体やレコードに対して、ループ内で頻繁にwith式を適用すると、ガベージコレクション(GC)の負荷やメモリコピーのコストが増大する可能性があります。

とはいえ、現代の.NETランタイムは小規模なオブジェクトの生成と回収に最適化されているため、通常のビジネスロジックで問題になることは稀です。

パフォーマンスが極めてシビアなパスでのみ、プロファイリング結果に基づいた検討を行えば十分でしょう。

まとめ

C#のwith式は、単なるシンタックスシュガー(構文上の糖衣)以上の価値を持っています。

それは、オブジェクト指向言語であるC#の中に「不変データ構造を扱う」という関数型のエッセンスを自然な形で取り込んだ革新的な機能です。

record型や構造体と組み合わせてwith式を活用することで、以下のような恩恵が得られます。

  • 副作用の排除:元のオブジェクトが保護され、予期せぬ状態変化によるバグを防止できる。
  • コードの宣言的な記述:何を変えたいのかが明確になり、コードの意図が伝わりやすくなる。
  • ボイラープレートの削減:手動でのコピー処理やコンストラクタ呼び出しの羅列から解放される。

特にC# 10以降、構造体でも利用可能になったことで、その適用範囲はさらに広がりました。

もし、まだコードの中でプロパティへの直接代入(Setterの呼び出し)を多用しているのであれば、この機会にrecord型とwith式を用いた「非破壊的なプログラミングスタイル」を取り入れてみてはいかがでしょうか。

より堅牢で、メンテナンス性の高いコードへの第一歩となるはずです。