Record bersifat immutable, sehingga mengganti satu nilai berarti membuat record baru berisi seluruh nilai lama kecuali yang diubah. Menulis pembuatan ulang itu secara manual melelahkan dan rawan keliru, apalagi saat property bertambah. With expression menyingkat pola tersebut: satu ekspresion menyalin seluruh nilai dari data asal, lalu menimpa property yang kita sebut. Pola ini dikenal sebagai non-destructive mutation karena data asal tidak tersentuh sama sekali.

Menyalin dengan Perubahan #

Contoh berikut mengubah harga pada record Pesanan tanpa menyentuh pesanan asal:

csharp
public record Pesanan(int Id, string Produk, int Harga);

Pesanan awal = new Pesanan(1, "Kopi Gayo", 45000);
Pesanan ubah = awal with { Harga = 50000 };

Console.WriteLine(awal);
Console.WriteLine(ubah);
Console.WriteLine(ReferenceEquals(awal, ubah));

Output yang Dihasilkan:

text
Pesanan { Id = 1, Produk = Kopi Gayo, Harga = 45000 }
Pesanan { Id = 1, Produk = Kopi Gayo, Harga = 50000 }
False

Mari kita bedah baris demi baris:

  1. awal with { Harga = 50000 }: runtime menyalin nilai semua property milik awal, kemudian menimpa property Harga dengan nilai baru; property yang tidak disebut tetap mengikuti asalnya.
  2. Console.WriteLine(awal): awal tetap 45000; with expression tidak mengubah data asal, sehingga immutabilitas record tetap terjaga.
  3. Console.WriteLine(ubah): hasil dengan-expression adalah record baru berisi Harga sebesar 50000.
  4. ReferenceEquals(awal, ubah): hasilnya False; runtime benar-benar membuat object baru, bukan mengubah object lama.

With Expression pada Struct Biasa #

Sejak C# 10, with expression juga bekerja pada struct biasa yang tidak bertanda record. Salinan tetap dibuat, lalu property yang disebut ditimpa:

csharp
public struct TitikKoordinat
{
    public int X;
    public int Y;

    public TitikKoordinat(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString() => $"({X}, {Y})";
}

TitikKoordinat titikAwal = new TitikKoordinat(3, 4);
TitikKoordinat titikBaru = titikAwal with { X = 10 };

Console.WriteLine(titikAwal);
Console.WriteLine(titikBaru);

Output yang Dihasilkan:

text
(3, 4)
(10, 4)

Struct titikAwal tetap (3, 4) karena penimpaan X terjadi pada salinan titikBaru saja.

Cara Kerja Penyalinan #

Compiler menerjemahkan with expression menjadi tiga langkah yang berurutan:

  1. Salin penuh: runtime membuat duplikat instance asal; seluruh field dan property terbawa apa adanya, termasuk yang tidak kita sebut.
  2. Timpa pilihan: property yang ditulis di dalam kurung kurawal diberi nilai baru pada salinan tersebut.
  3. Kembalikan instance baru: hasil berupa object atau struct yang berdiri sendiri, sementara data asal tidak tersentuh.

Karena penyalinan menyertakan seluruh isi, with expression pada record besar ikut menyalin semua property-nya. Bila hanya satu-dua nilai yang sering berubah dan recordnya berukuran besar, class biasa dengan property set dapat menjadi pilihan yang lebih hemat. Untuk data kecil bersifat immutable, gabungan record dan with expression memberi alur perubahan yang aman tanpa kehilangan jejak versi sebelumnya.