C#のwith式とは?使い方・recordとの関係・コピー時の注意点を初心者向けに解説

はじめに

C#のwith式は、既存のオブジェクトをもとにして「一部の値だけを変えた新しいオブジェクト」を作るための構文です。

特にrecordと組み合わせて使われることが多く、イミュータブルなデータ、つまり作成後に直接変更しないデータを扱うときに便利です。

たとえば、ユーザー情報を表すオブジェクトがあり、名前やIDはそのままで、メールアドレスだけを変更した新しいオブジェクトを作りたい場合があります。このような場面でwith式を使うと、すべての値をコンストラクタに渡し直す必要がなく、変更したいプロパティだけを書けば済みます。

この記事では、C#のwith式について、基本構文、recordとの関係、コピー時の注意点、よくあるエラー、使うべき場面まで初心者向けに解説します。

1. C#のwith式とは?まず押さえたい基本

1-1. with式は「一部の値だけ変えてコピーする」ための構文

C#のwith式は、既存のインスタンスをコピーしながら、一部のプロパティだけを変更した新しいインスタンスを作る構文です。

基本的なイメージは次のようになります。

C#
var newObject = oldObject with
{
PropertyName = newValue
};

oldObjectをもとにして、PropertyNameだけをnewValueに変更したnewObjectを作ります。

重要なのは、元のoldObject自体を変更するわけではないという点です。with式は「更新」しているように見えますが、実際には「変更済みのコピー」を作っています。

1-2. with式でできること・できないこと

with式でできる主なことは、次のような処理です。

既存のオブジェクトをコピーすること、一部のプロパティだけを変更すること、元のオブジェクトを変更せずに新しいオブジェクトを作ること、recordや一部のstructなどで簡潔にコピー処理を書くことができます。

一方で、何でもできるわけではありません。

たとえば、変更できないプロパティをwith式で書き換えることはできません。また、with式は基本的に浅いコピーなので、参照型のプロパティを持つ場合は注意が必要です。

次のようなイメージです。

C#
var user2 = user1 with
{
Name = "Taro"
};

このコードはuser1Nameを変更しているのではなく、Nameだけが異なるuser2を作っています。

1-3. with式が使えるC#のバージョンと前提知識

with式は、C# 9.0でrecordとともに登場しました。

そのため、recordと一緒に使う例が非常に多く見られます。現在のC#では、record classrecord structstructなど、条件を満たす型でwith式を利用できます。

初心者がwith式を理解するためには、次の知識があるとスムーズです。

クラスとインスタンスの違い、プロパティの基本、recordの基本、値型と参照型の違い、initアクセサの意味です。

ただし、最初からすべてを完璧に理解する必要はありません。まずは「元のオブジェクトを変えずに、一部だけ変えたコピーを作る構文」と覚えておくとよいでしょう。

1-4. 初心者が混同しやすい「代入」「コピー」「更新」の違い

with式を理解するときに、初心者が混同しやすいのが「代入」「コピー」「更新」の違いです。

代入は、変数に値や参照を入れることです。

C#
var user2 = user1;

この場合、user1user2が同じオブジェクトを指していることがあります。特に通常のクラスでは、片方を通じて中身を変更すると、もう片方にも影響することがあります。

コピーは、元の値をもとに別のインスタンスを作ることです。

C#
var user2 = user1 with { Name = "Taro" };

この場合、user1をもとにした新しいuser2が作られます。

更新は、既存のオブジェクトの状態を直接変更することです。

C#
user1.Name = "Taro";

この場合、user1そのもののNameが変更されます。

with式は見た目こそ更新に似ていますが、実際にはコピーに近い処理です。

2. C#のwith式の基本的な使い方

2-1. recordでwith式を使う基本構文

まず、recordを使った基本例を見てみましょう。

C#
public record User(string Name, int Age);

このUserは、名前と年齢を持つrecordです。

with式を使うと、次のように一部の値だけを変えたコピーを作れます。

C#
var user1 = new User("Alice", 20);

var user2 = user1 with
{
Age = 21
};

Console.WriteLine(user1); // User { Name = Alice, Age = 20 }
Console.WriteLine(user2); // User { Name = Alice, Age = 21 }

user2user1をもとにしていますが、Ageだけが21に変わっています。

2-2. 1つのプロパティだけ変更してコピーする例

with式のもっとも基本的な使い方は、1つのプロパティだけを変更するケースです。

C#
public record Product(string Name, int Price);

var product1 = new Product("Keyboard", 5000);

var product2 = product1 with
{
Price = 4500
};

Console.WriteLine(product1.Price); // 5000
Console.WriteLine(product2.Price); // 4500

この例では、商品名はそのままで、価格だけを変更した新しいProductを作っています。

product1の価格は5000のままです。with式によって作られたproduct2だけが4500になります。

2-3. 複数のプロパティを同時に変更する例

with式では、複数のプロパティを同時に変更することもできます。

C#
public record UserProfile(string Name, int Age, string Email);

var profile1 = new UserProfile("Alice", 20, "alice@example.com");

var profile2 = profile1 with
{
Age = 21,
Email = "alice.new@example.com"
};

Console.WriteLine(profile1);
// UserProfile { Name = Alice, Age = 20, Email = alice@example.com }

Console.WriteLine(profile2);
// UserProfile { Name = Alice, Age = 21, Email = alice.new@example.com }

このように、変更したいプロパティだけを{ }の中に書きます。

すべての値を改めて指定する必要がないため、プロパティ数が多い型では特に便利です。

2-4. 元のインスタンスは変更されないことを確認する

with式で重要なのは、元のインスタンスが変更されないことです。

C#
public record Account(string Id, string Status);

var account1 = new Account("A001", "Active");

var account2 = account1 with
{
Status = "Suspended"
};

Console.WriteLine(account1.Status); // Active
Console.WriteLine(account2.Status); // Suspended

account2を作るときにStatusSuspendedへ変更していますが、account1.StatusActiveのままです。

この性質により、変更前と変更後の状態を安全に比較したり、処理の途中で元データを残したりしやすくなります。

2-5. varと明示的な型指定の書き方

with式を使うときは、varを使って書くことが多いです。

C#
var user2 = user1 with
{
Age = 30
};

もちろん、明示的に型を書くこともできます。

C#
User user2 = user1 with
{
Age = 30
};

どちらも意味は同じです。

ローカル変数で型が明らかな場合はvarで簡潔に書けます。戻り値の型や公開APIなど、型を明確にしたい場面では明示的に書くと読みやすくなります。

3. with式とrecordの関係

3-1. with式はrecordと相性がよい理由

with式はrecordと非常に相性がよい構文です。

recordは、データを表すための型として設計されています。値の組み合わせを表現しやすく、等価性の比較や文字列表現なども自動で用意されます。

さらに、recordではwith式を使ったコピーが自然に行えます。

C#
public record Point(int X, int Y);

var p1 = new Point(10, 20);
var p2 = p1 with { X = 15 };

このように、元の値を保ちながら一部だけ変更するコードを簡潔に書けます。

設定値、DTO、状態管理、テストデータなど、データ中心の型ではrecordwith式の組み合わせが特に便利です。

3-2. record classでwith式を使う場合

record classは参照型のrecordです。

次のように定義できます。

C#
public record class User(string Name, int Age);

recordとだけ書いた場合、通常はrecord classとして扱われます。

C#
public record User(string Name, int Age);

record classでも、with式は次のように使えます。

C#
var user1 = new User("Alice", 20);
var user2 = user1 with { Age = 21 };

record classは参照型ですが、with式を使うと新しいインスタンスが作られます。

ただし、参照型のプロパティを持っている場合、その中身まですべて複製されるわけではありません。この点は後ほど詳しく解説します。

3-3. record structでwith式を使う場合

record structは値型のrecordです。

C#
public readonly record struct Size(int Width, int Height);

record structでもwith式を使えます。

C#
var size1 = new Size(100, 200);

var size2 = size1 with
{
Width = 150
};

Console.WriteLine(size1); // Size { Width = 100, Height = 200 }
Console.WriteLine(size2); // Size { Width = 150, Height = 200 }

record structは値型なので、もともと値のコピーとして扱われる性質があります。with式を使うと、その値の一部だけを変えた新しい値を作るような感覚で扱えます。

readonly record structの場合は、作成後に変更しない設計と相性がよく、座標、サイズ、期間、金額のような小さな値オブジェクトに向いています。

3-4. initアクセサとの関係

with式と一緒に理解したいのがinitアクセサです。

initは、オブジェクトの初期化時だけ値を設定できるプロパティを作るための仕組みです。

C#
public record User
{
public string Name { get; init; }
public int Age { get; init; }
}

このようなinitプロパティは、通常の代入では後から変更できません。

C#
var user = new User { Name = "Alice", Age = 20 };

// user.Age = 21; // エラー

しかし、with式では新しいオブジェクトを作る初期化の一部として扱われるため、次のように書けます。

C#
var updatedUser = user with
{
Age = 21
};

つまり、initは「既存オブジェクトを直接変更させない」ための仕組みであり、with式は「変更後のコピーを作る」ための仕組みです。

3-5. recordが自動生成するコピー処理の仕組み

recordでは、with式を使うために必要なコピー処理がコンパイラによって自動的に用意されます。

開発者が毎回コピー用のコンストラクタやCloneメソッドを書く必要はありません。

たとえば、次のようなrecordがあるとします。

C#
public record User(string Name, int Age);

この型に対して、次のようにwith式を使えます。

C#
var user2 = user1 with { Age = 30 };

内部的には、元のuser1をもとにコピーを作り、そのコピーに対して指定されたプロパティの値を設定するような処理が行われます。

そのため、recordではwith式を自然に利用できます。

4. with式を使うメリット

4-1. オブジェクトの一部だけを簡潔に変更できる

with式の大きなメリットは、オブジェクトの一部だけを簡潔に変更できることです。

たとえば、プロパティが多い型をコンストラクタで作り直す場合、変更しない値もすべて書かなければなりません。

C#
var user2 = new User(
user1.Id,
user1.Name,
"new@example.com",
user1.Age,
user1.Status
);

with式を使うと、変更したい部分だけを書けます。

C#
var user2 = user1 with
{
Email = "new@example.com"
};

どの値を変更しているのかが明確になり、コードも短くなります。

4-2. イミュータブルな設計に向いている

with式は、イミュータブルな設計と相性がよいです。

イミュータブルとは、作成後に状態を変更しないことを意味します。

通常の可変オブジェクトでは、次のように値を変更します。

C#
user.Email = "new@example.com";

この方法は簡単ですが、どこで状態が変わったのか追いにくくなることがあります。

一方、with式を使うと、元のオブジェクトを残したまま、変更後の新しいオブジェクトを作れます。

C#
var updatedUser = user with
{
Email = "new@example.com"
};

状態の変化を明示的に扱えるため、バグを減らしやすくなります。

4-3. 変更前と変更後の状態を安全に扱いやすい

with式では元のオブジェクトが変更されないため、変更前と変更後の状態を同時に扱えます。

C#
var before = new User("Alice", 20);
var after = before with { Age = 21 };

Console.WriteLine(before.Age); // 20
Console.WriteLine(after.Age); // 21

この性質は、ログ出力、差分確認、履歴管理、状態遷移の処理などで役立ちます。

たとえば、更新前の値と更新後の値を比較したい場合に、元データを壊さずに処理できます。

4-4. コンストラクタ呼び出しよりコードが読みやすくなる場面

プロパティ数が多い型では、コンストラクタで新しいインスタンスを作ると、どの値を変更しているのかわかりにくくなります。

C#
var settings2 = new AppSettings(
settings1.Host,
settings1.Port,
true,
settings1.Timeout,
settings1.RetryCount
);

このコードだけを見ると、どの引数が何を意味しているのか判断しづらい場合があります。

with式なら、変更対象が明確です。

C#
var settings2 = settings1 with
{
EnableLogging = true
};

特に、似た型の値や同じ型の引数が多い場合、with式の方が読みやすくなることがあります。

4-5. DTOや設定値のコピーで役立つケース

with式は、DTOや設定値のコピーでよく役立ちます。

DTOとは、データ転送用のオブジェクトのことです。APIのレスポンス、リクエスト、画面表示用データなどで使われます。

たとえば、APIから受け取ったユーザー情報をもとに、表示用に一部だけ加工したい場合があります。

C#
var displayUser = apiUser with
{
Name = apiUser.Name.Trim()
};

また、設定値をもとに、開発環境用の設定だけを少し変えたい場合にも便利です。

C#
var devSettings = defaultSettings with
{
LogLevel = "Debug"
};

元の設定を壊さずに派生設定を作れるため、意図しない副作用を避けやすくなります。

5. with式の注意点:コピー時に気をつけること

5-1. with式は基本的に浅いコピーである

with式を使うときに最も注意したいのが、コピーの深さです。

with式によるコピーは、基本的に浅いコピーです。

浅いコピーとは、オブジェクト自体は新しく作られても、その中にある参照型のプロパティまでは複製されないコピーのことです。

たとえば、List<string>を持つrecordを考えてみます。

C#
public record User(string Name, List<string> Tags);

var user1 = new User("Alice", new List<string> { "admin" });

var user2 = user1 with
{
Name = "Bob"
};

この場合、user1user2は別のUserインスタンスですが、TagsList<string>は同じインスタンスを共有します。

5-2. 参照型プロパティは同じインスタンスを共有する

参照型プロパティを持つ場合、with式でコピーしても、その参照先は共有されることがあります。

C#
public record User(string Name, List<string> Tags);

var user1 = new User("Alice", new List<string> { "admin" });

var user2 = user1 with
{
Name = "Bob"
};

user2.Tags.Add("editor");

Console.WriteLine(string.Join(", ", user1.Tags)); // admin, editor
Console.WriteLine(string.Join(", ", user2.Tags)); // admin, editor

user2.Tagsに要素を追加しただけなのに、user1.Tagsにも反映されています。

これは、Tagsが同じList<string>インスタンスを参照しているためです。

with式は便利ですが、参照型プロパティを含む場合は「中身も完全に別物になる」と思い込まないようにしましょう。

5-3. Listや配列を持つrecordで起きやすい落とし穴

List<T>や配列を持つrecordでは、浅いコピーによる落とし穴が起きやすいです。

C#
public record Order(string Id, List<string> Items);

var order1 = new Order("O001", new List<string> { "Book" });

var order2 = order1 with
{
Id = "O002"
};

order2.Items.Add("Pen");

Console.WriteLine(string.Join(", ", order1.Items)); // Book, Pen

order2だけにPenを追加したつもりでも、order1にも影響しています。

配列でも同じような問題が起きます。

C#
public record Report(string Title, string[] Lines);

var report1 = new Report("Daily", new[] { "Line1" });
var report2 = report1 with { Title = "Weekly" };

report2.Lines[0] = "Changed";

Console.WriteLine(report1.Lines[0]); // Changed

このような問題を避けるには、List<T>の代わりに不変コレクションを使う、コピー時にコレクションも複製する、外部から変更できない設計にする、といった工夫が必要です。

5-4. 深いコピーが必要な場合の考え方

深いコピーとは、オブジェクト本体だけでなく、その中に含まれる参照型プロパティも別のインスタンスとしてコピーすることです。

with式だけでは深いコピーにならない場合があります。

たとえば、List<string>もコピーしたいなら、次のように明示的に新しいリストを作ります。

C#
var order2 = order1 with
{
Id = "O002",
Items = new List<string>(order1.Items)
};

ネストしたrecordを持つ場合は、内側にもwith式を使えます。

C#
public record Address(string City, string Street);
public record User(string Name, Address Address);

var user1 = new User("Alice", new Address("Tokyo", "Street 1"));

var user2 = user1 with
{
Address = user1.Address with
{
Street = "Street 2"
}
};

このように、必要な部分だけ明示的にコピーすることが重要です。

5-5. with式で変更できないプロパティがある場合

with式で変更できるのは、初期化または設定が可能なプロパティです。

たとえば、getだけのプロパティは、通常with式で変更できません。

C#
public record User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

このようなプロパティに対して、次のように書くとエラーになります。

C#
// var user2 = user1 with { Name = "Bob" }; // エラー

with式で変更したい場合は、initsetを使って初期化できる形にする必要があります。

C#
public record User
{
public string Name { get; init; } = "";
}

ただし、すべてのプロパティを変更可能にすればよいわけではありません。設計上、変更させたくない値はwith式でも変更できないようにしておくことがあります。

5-6. nullを含むプロパティを扱うときの注意点

with式では、nullを含むプロパティにも注意が必要です。

次のような型を考えます。

C#
public record User(string Name, Address? Address);
public record Address(string City);

Addressnullの可能性がある場合、ネストしたwith式を直接使うとエラーや例外の原因になります。

C#
// Addressがnullの場合、これは安全ではない
var user2 = user1 with
{
Address = user1.Address with
{
City = "Tokyo"
}
};

Addressnullの可能性があるなら、事前に確認する必要があります。

C#
var user2 = user1 with
{
Address = user1.Address is null
? new Address("Tokyo")
: user1.Address with { City = "Tokyo" }
};

nullable reference typesを有効にしている場合は、コンパイラの警告も参考にしながら、nullの可能性を明確に扱いましょう。

6. record以外でwith式は使える?class・structとの違い

6-1. 通常のclassでwith式を使える条件

初心者向けには「with式はrecordで使うもの」と説明されることが多いですが、通常のclassでも条件を満たせば使える場合があります。

ただし、通常のclasswith式を使うには、コピーに必要な仕組みが用意されている必要があります。recordのように自動で扱いやすい形になるわけではないため、初心者には少しわかりにくい場合があります。

たとえば、単純な通常クラスで次のように書いても、期待通りにwith式を使えないことがあります。

C#
public class User
{
public string Name { get; init; } = "";
public int Age { get; init; }
}

この型に対して常にrecordと同じ感覚でwith式が使えるとは限りません。

通常のclassでコピーを簡潔に扱いたい場合は、まずrecord classを検討するのが一般的です。

6-2. structでwith式を使う場合の特徴

structは値型です。

C#では、structに対してもwith式を使える場合があります。

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

var p1 = new Point { X = 10, Y = 20 };

var p2 = p1 with
{
X = 15
};

structは値型なので、変数に代入したときにも値がコピーされる性質があります。with式は、そのコピーに対して一部の値を変更するような形で使えます。

ただし、可変なstructは扱いが難しくなることがあります。初心者がデータを安全に扱いたい場合は、readonly record structなども選択肢になります。

6-3. recordと通常のclassでの使い勝手の違い

recordと通常のclassでは、with式の使い勝手が大きく異なります。

recordは、データを表す型として設計されており、コピーや値の比較がしやすいように作られています。そのため、with式を自然に使えます。

一方、通常のclassは、状態や振る舞いを持つオブジェクトを表すために使われることが多く、必ずしもコピーを前提としていません。

たとえば、ユーザーの状態を表す単純なデータならrecordが向いています。

C#
public record UserDto(string Id, string Name, string Email);

一方、データベース接続、ファイル操作、外部APIクライアントのように、内部状態やリソースを持つものは通常のclassの方が自然です。

with式を使いたいからといって、すべてをrecordにすればよいわけではありません。型の役割に合わせて選びましょう。

6-4. 初心者はまずrecordで使うのがおすすめな理由

初心者がwith式を学ぶなら、まずrecordで使うのがおすすめです。

理由は、recordならwith式の目的がわかりやすいからです。

C#
public record User(string Name, int Age);

var user1 = new User("Alice", 20);
var user2 = user1 with { Age = 21 };

このコードは非常にシンプルで、「元のデータをもとに、一部だけ変えた新しいデータを作る」というwith式の考え方を理解しやすいです。

通常のclassで無理に使おうとすると、コピー処理や設計の話が複雑になります。まずはrecordで基本を押さえ、その後にstructや通常クラスでの利用を学ぶとよいでしょう。

7. with式の具体的な活用例

7-1. ユーザー情報の一部だけを更新する例

ユーザー情報を表すrecordで、メールアドレスだけを変更する例です。

C#
public record User(
string Id,
string Name,
string Email,
bool IsActive
);

var user = new User(
"U001",
"Alice",
"alice@example.com",
true
);

var updatedUser = user with
{
Email = "alice.new@example.com"
};

Console.WriteLine(user.Email); // alice@example.com
Console.WriteLine(updatedUser.Email); // alice.new@example.com

このように、変更したい項目だけを書けるため、コードが読みやすくなります。

ユーザーIDや名前など、変更しない値を何度も書く必要がありません。

7-2. 設定オブジェクトをコピーして一部変更する例

アプリケーション設定でもwith式は便利です。

C#
public record AppSettings(
string Host,
int Port,
bool EnableLogging,
int TimeoutSeconds
);

var productionSettings = new AppSettings(
"example.com",
443,
false,
30
);

var developmentSettings = productionSettings with
{
Host = "localhost",
Port = 5000,
EnableLogging = true
};

本番用設定をもとに、開発用設定だけを少し変えています。

すべての値を最初から書き直すよりも、どの部分を変更しているのかが明確です。

7-3. APIレスポンスやDTOを加工する例

APIレスポンスやDTOを加工するときにもwith式は役立ちます。

C#
public record UserResponse(
string Id,
string Name,
string Email
);

var response = new UserResponse(
"U001",
" Alice ",
"alice@example.com"
);

var normalizedResponse = response with
{
Name = response.Name.Trim()
};

この例では、APIから受け取った名前の前後の空白を取り除いた新しいDTOを作っています。

元のレスポンスを残したまま加工後のデータを作れるため、処理の見通しがよくなります。

7-4. テストデータを少しだけ変えて作る例

テストコードでは、似たデータを少しずつ変えて使いたい場面がよくあります。

C#
public record User(
string Name,
int Age,
string Role
);

var baseUser = new User("Alice", 20, "User");

var adminUser = baseUser with
{
Role = "Admin"
};

var olderUser = baseUser with
{
Age = 30
};

共通の基準データを作り、必要な部分だけを変えたテストデータを簡単に作れます。

テストデータの重複が減り、「このテストでは何を変えたいのか」がわかりやすくなります。

7-5. ネストしたオブジェクトを扱う場合の例

recordの中に別のrecordが入っている場合、内側のオブジェクトにもwith式を使えます。

C#
public record Address(string Prefecture, string City);
public record User(string Name, Address Address);

var user1 = new User(
"Alice",
new Address("Tokyo", "Shibuya")
);

var user2 = user1 with
{
Address = user1.Address with
{
City = "Shinjuku"
}
};

Console.WriteLine(user1.Address.City); // Shibuya
Console.WriteLine(user2.Address.City); // Shinjuku

外側のUserをコピーしつつ、内側のAddressもコピーしてCityだけを変更しています。

ネストが深くなるとコードが読みにくくなることもあるため、必要に応じてメソッド化するなどの工夫も大切です。

8. with式と似た書き方・関連機能との違い

8-1. オブジェクト初期化子との違い

オブジェクト初期化子は、新しいオブジェクトを作るときにプロパティを設定する構文です。

C#
var user = new User
{
Name = "Alice",
Age = 20
};

一方、with式は既存のオブジェクトをもとにして新しいオブジェクトを作ります。

C#
var user2 = user1 with
{
Age = 21
};

オブジェクト初期化子は「ゼロから作る」イメージ、with式は「既存のものをもとに一部だけ変えて作る」イメージです。

8-2. コンストラクタで新しく作る方法との違い

コンストラクタでも、変更後のオブジェクトを作ることはできます。

C#
var user2 = new User(user1.Name, 21, user1.Email);

しかし、プロパティが多い場合は、変更しない値もすべて渡す必要があります。

with式なら、変更する値だけを書けます。

C#
var user2 = user1 with
{
Age = 21
};

コンストラクタは、必須項目を明確にしたい場合や、生成時に検証を行いたい場合に向いています。

with式は、既存の値をベースに一部だけ変えたい場合に向いています。

8-3. Cloneメソッドとの違い

Cloneメソッドは、オブジェクトをコピーするために自分で用意するメソッドです。

C#
var user2 = user1.Clone();

Cloneメソッドは、浅いコピーにするのか、深いコピーにするのかを自分で設計できます。

一方、with式は言語機能として用意されたコピー構文です。

C#
var user2 = user1 with
{
Age = 21
};

with式は書き方が簡潔ですが、基本的に浅いコピーである点に注意が必要です。

複雑なコピー処理や深いコピーが必要な場合は、専用のメソッドを用意した方がわかりやすいこともあります。

8-4. initプロパティとの違い

initプロパティは、初期化時だけ値を設定できるプロパティです。

C#
public string Name { get; init; } = "";

with式は、既存オブジェクトをもとにコピーを作る構文です。

C#
var user2 = user1 with
{
Name = "Bob"
};

initはプロパティ側の仕組み、with式はコピーを作る式です。

両者は一緒に使われることが多く、イミュータブルなデータ設計をしやすくします。

8-5. readonlyやイミュータブル設計との関係

readonlyやイミュータブル設計は、値が意図せず変更されることを防ぐための考え方です。

with式は、既存オブジェクトを直接変更せず、新しいオブジェクトを作るため、イミュータブル設計と相性がよいです。

ただし、with式を使えば必ず完全にイミュータブルになるわけではありません。

たとえば、List<T>のような可変コレクションをプロパティに持っている場合、その中身は変更できてしまいます。

C#
public record User(string Name, List<string> Tags);

より安全にしたい場合は、読み取り専用コレクションや不変コレクションを使うことを検討しましょう。

9. with式でよくあるエラーと対処法

9-1. with式が使えない型でエラーになる

通常のclassに対して、recordと同じ感覚でwith式を使おうとするとエラーになることがあります。

C#
public class User
{
public string Name { get; init; } = "";
}

var user1 = new User { Name = "Alice" };

// var user2 = user1 with { Name = "Bob" }; // エラーになる場合がある

対処法としては、データ中心の型であればrecordとして定義するのがわかりやすいです。

C#
public record User(string Name);

with式を使いたい型が、そもそもコピーに向いている設計かどうかも確認しましょう。

9-2. setできないプロパティを変更しようとしてエラーになる

with式の中で変更できるのは、設定可能なプロパティです。

C#
public record User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

このようなgetのみのプロパティは、with式で変更できません。

C#
// var user2 = user1 with { Name = "Bob" }; // エラー

変更したい場合は、initを使います。

C#
public record User
{
public string Name { get; init; } = "";
}

ただし、設計上変更させたくないプロパティは、無理にinitsetを付けない方がよい場合もあります。

9-3. C#のバージョンが古くて使えない

with式は比較的新しいC#の機能です。古いC#バージョンを使っているプロジェクトでは利用できない場合があります。

エラーが出る場合は、プロジェクトの言語バージョンを確認しましょう。

.csprojで言語バージョンを指定している場合は、次のような設定を確認します。

XML
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>

ただし、むやみにlatestへ変更すると、チームやビルド環境との互換性に影響する場合があります。プロジェクトで利用している.NETのバージョンやチームのルールに合わせて判断しましょう。

9-4. 参照型プロパティの変更が元オブジェクトにも影響する

with式でよくあるトラブルが、参照型プロパティの共有です。

C#
public record User(string Name, List<string> Roles);

var user1 = new User("Alice", new List<string> { "User" });
var user2 = user1 with { Name = "Bob" };

user2.Roles.Add("Admin");

Console.WriteLine(string.Join(", ", user1.Roles)); // User, Admin

user2だけを変更したつもりでも、Rolesが共有されているためuser1にも影響しています。

対処法は、コピー時にリストも新しく作ることです。

C#
var user2 = user1 with
{
Name = "Bob",
Roles = new List<string>(user1.Roles)
};

または、可変なList<T>ではなく、不変コレクションを使う方法もあります。

9-5. recordの定義方法によって意図通りにコピーされない

recordの定義方法によって、with式で扱いやすいプロパティとそうでないプロパティがあります。

たとえば、プライマリコンストラクタで定義したrecordはシンプルに扱えます。

C#
public record User(string Name, int Age);

一方で、独自のプロパティや計算プロパティを追加している場合、どの値がコピーされ、どの値が再計算されるのかを意識する必要があります。

C#
public record User(string FirstName, string LastName)
{
public string FullName => $"{FirstName} {LastName}";
}

この場合、FullNameは保存された値ではなく、FirstNameLastNameから計算される値です。

C#
var user1 = new User("Alice", "Smith");

var user2 = user1 with
{
LastName = "Brown"
};

Console.WriteLine(user2.FullName); // Alice Brown

計算プロパティ、手動で定義したプロパティ、参照型プロパティが混在すると、コピーの挙動がわかりにくくなることがあります。recordはできるだけシンプルなデータ表現として使うと理解しやすくなります。

10. C#のwith式を使うべき場面・使わない方がよい場面

10-1. with式を使うと便利な場面

with式は、既存のデータをもとに一部だけ変更した新しいデータを作る場面で便利です。

たとえば、次のようなケースに向いています。

DTOの一部を加工する場合、設定オブジェクトを派生させる場合、テストデータを少しだけ変更する場合、状態管理で変更前後の値を扱う場合、イミュータブルな値オブジェクトを作る場合です。

C#
var updated = current with
{
Status = "Completed"
};

このようなコードは、変更内容が明確で読みやすくなります。

10-2. with式を使わない方が読みやすい場面

一方で、with式を使わない方がよい場面もあります。

たとえば、新しいオブジェクトをゼロから作る場合は、通常のコンストラクタやオブジェクト初期化子の方が自然です。

C#
var user = new User("Alice", 20);

また、複雑な検証や変換が必要な場合は、専用のメソッドを用意した方が読みやすいことがあります。

C#
var updatedUser = user.ChangeEmail("new@example.com");

このようなメソッド名があると、「メールアドレスを変更する」という業務上の意味が明確になります。

単にプロパティをコピーするだけならwith式、意味のある操作として表現したいならメソッド、という使い分けを考えるとよいでしょう。

10-3. 大きなオブジェクトや複雑な参照構造での判断基準

大きなオブジェクトや複雑な参照構造を持つオブジェクトでは、with式を使うか慎重に判断する必要があります。

with式は浅いコピーであるため、内部に多くの参照型プロパティがあると、どこまで共有され、どこから新しく作られるのかがわかりにくくなることがあります。

C#
var newOrder = oldOrder with
{
Customer = oldOrder.Customer with
{
Address = oldOrder.Customer.Address with
{
City = "Tokyo"
}
}
};

このようにネストが深いwith式は、読みにくくなる場合があります。

複雑な更新が必要な場合は、専用の更新メソッド、ファクトリメソッド、マッピング処理などを検討しましょう。

10-4. チーム開発で使うときの命名・設計のポイント

チーム開発でwith式を使う場合は、型の設計方針をそろえることが大切です。

たとえば、DTOや設定値にはrecordを使う、可変なコレクションを直接公開しない、深いコピーが必要な型では専用メソッドを用意する、といったルールがあると安全です。

また、変数名も重要です。

C#
var updatedUser = user with
{
Email = "new@example.com"
};

user2のような名前よりも、updatedUseradminUserのように意味がわかる名前を使うと、コードの意図が伝わりやすくなります。

with式は便利ですが、チーム全体でコピーの深さやイミュータブル設計への理解がそろっていないと、思わぬバグにつながることがあります。

11. C#のwith式に関するよくある質問

11-1. with式は元のオブジェクトを変更しますか?

いいえ、with式は元のオブジェクトを直接変更しません。

with式は、元のオブジェクトをもとにして新しいオブジェクトを作ります。

C#
var user2 = user1 with
{
Age = 21
};

この場合、user1はそのままで、Ageが変更されたuser2が作られます。

ただし、参照型プロパティを共有している場合、その参照先の中身を変更すると、元のオブジェクトにも影響することがあります。

11-2. with式はrecordでしか使えませんか?

いいえ、with式はrecord以外でも使える場合があります。

ただし、初心者が最初に学ぶならrecordで使うのがもっともわかりやすいです。

recordwith式によるコピーと相性がよく、少ないコードで安全に扱いやすいデータ型を作れます。

通常のclassで同じように使おうとすると、コピー処理の設計が必要になったり、意図がわかりにくくなったりすることがあります。

11-3. with式は深いコピーですか?

いいえ、with式は基本的に浅いコピーです。

つまり、オブジェクト本体はコピーされても、参照型プロパティの参照先までは自動的に複製されません。

C#
public record User(string Name, List<string> Tags);

このような型でwith式を使うと、Tagsのリストは共有される可能性があります。

深いコピーが必要な場合は、リストやネストしたオブジェクトを明示的にコピーする必要があります。

C#
var user2 = user1 with
{
Tags = new List<string>(user1.Tags)
};

11-4. with式とCloneはどちらを使うべきですか?

単純に一部のプロパティだけを変更したコピーを作りたい場合は、with式が便利です。

C#
var user2 = user1 with
{
Name = "Bob"
};

一方で、複雑なコピー処理や深いコピーが必要な場合は、Cloneメソッドや専用のコピー用メソッドを用意した方がよいことがあります。

with式は簡潔さがメリットです。Cloneはコピー処理を自分で制御しやすいのがメリットです。

どちらを使うかは、コピー処理の複雑さと、コードを読む人に意図が伝わりやすいかで判断しましょう。

11-5. with式でprivate setのプロパティは変更できますか?

通常、外部から設定できないprivate setのプロパティは、with式で自由に変更することはできません。

C#
public record User
{
public string Name { get; private set; } = "";
}

このようなプロパティは、外部からの変更を制限するためのものです。

with式で変更したいプロパティには、initを使うのが一般的です。

C#
public record User
{
public string Name { get; init; } = "";
}

ただし、設計上変更させたくない値であれば、with式で変更できないようにしておくことも大切です。

11-6. with式はパフォーマンスに影響しますか?

with式は新しいインスタンスを作るため、当然ながらコピーのコストが発生します。

小さなDTOや設定値、値オブジェクトで使う程度であれば、多くの場合は大きな問題になりにくいです。

ただし、大きなオブジェクトを大量にコピーする場合や、頻繁にwith式を呼び出す場合は、パフォーマンスに影響する可能性があります。

また、参照型プロパティを深くコピーするような処理を自分で追加すると、その分コストも増えます。

パフォーマンスが重要な場面では、実際に計測して判断しましょう。読みやすさや安全性のためにwith式を使う価値がある場合も多いですが、無条件に使うのではなく、処理内容に合っているかを確認することが大切です。

まとめ

C#のwith式は、既存のオブジェクトをもとにして、一部の値だけを変更した新しいオブジェクトを作るための構文です。

特にrecordと相性がよく、DTO、設定値、テストデータ、状態管理などで便利に使えます。

基本的な書き方は次のようになります。

C#
var newObject = oldObject with
{
PropertyName = newValue
};

with式を使うと、元のオブジェクトを変更せずに新しいオブジェクトを作れるため、イミュータブルな設計をしやすくなります。

一方で、注意点もあります。

特に重要なのは、with式は基本的に浅いコピーであるという点です。List<T>や配列などの参照型プロパティを持つ場合、コピー後のオブジェクトと元のオブジェクトが同じインスタンスを共有することがあります。

深いコピーが必要な場合は、参照型プロパティも明示的にコピーする必要があります。

初心者はまず、recordと組み合わせてwith式を使うところから始めるのがおすすめです。with式の仕組みを理解すると、C#でデータを安全かつ読みやすく扱うための選択肢が広がります。