Record adalah tipe yang tugas utamanya menyimpan data, dan kesamaannya ditentukan oleh data itu. Record hadir di C# 9. Kamu menulis satu baris, lalu compiler membuat member yang dibutuhkan sebuah class data: property, constructor, Equals dan == berdasarkan nilai, GetHashCode, ToString yang mudah dibaca, Deconstruct, dan dukungan untuk salinan with.
Record butuh C# 9 atau sesudahnya (.NET 5+), jadi kode record di halaman ini ditampilkan sebagai C# biasa, dengan output-nya di komentar. Bagian terakhir menulis member yang sama secara manual di C# 7, dan kode itu bisa kamu jalankan.
Record posisional
Bentuk terpendeknya mencantumkan property di dalam tanda kurung setelah nama. Setiap parameter menjadi property public init-only dengan nama yang sama:
public record Product(string Sku, string Name, decimal Price);
var mug = new Product("MUG-01", "Mug", 8.50m);
Console.WriteLine(mug.Name); // Mug
Console.WriteLine(mug); // Product { Sku = MUG-01, Name = Mug, Price = 8.50 }
// mug.Price = 4m; // error CS8852: init-only property
var (sku, name, price) = mug; // generated Deconstruct
Console.WriteLine($"{sku} {price}"); // MUG-01 8.50
Dari satu baris itu, compiler membuat:
- constructor yang menerima
(string Sku, string Name, decimal Price); - tiga property
public ... { get; init; }; Equals(object),Equals(Product),GetHashCode(), serta operator==dan!=, yang semuanya membandingkan ketiga property itu;ToString()yang mencetak nama tipe dan setiap property public;Deconstruct(out string Sku, out string Name, out decimal Price);- copy constructor (protected, atau private di record yang sealed) dan method clone tersembunyi yang dipakai oleh
with.
Record juga bisa ditulis dengan body biasa, yang berguna saat property butuh nilai default atau validasi:
public record Customer
{
public required string Email { get; init; } // required: C# 11
public string Name { get; init; } = "";
}
var c = new Customer { Email = "ana@example.com" };
Kedua bentuk itu juga bisa digabung: parameter posisional plus member tambahan di dalam kurung kurawal.
public record Order(string Id, decimal Subtotal)
{
public decimal Tax => Subtotal * 0.23m;
public decimal Total => Subtotal + Tax;
}
Value equality
Untuk class biasa, == bertanya "apakah ini object yang sama?". Untuk record, pertanyaannya "apakah nilainya sama?":
var a = new Product("MUG-01", "Mug", 8.50m);
var b = new Product("MUG-01", "Mug", 8.50m);
Console.WriteLine(a == b); // True
Console.WriteLine(a.Equals(b)); // True
Console.WriteLine(ReferenceEquals(a, b)); // False: still two objects
GetHashCode dibuat agar sesuai, jadi record berfungsi dengan benar sebagai key dictionary dan di dalam HashSet<T>: record kedua dengan nilai yang sama akan menemukan entri milik record pertama.
Kesamaan membandingkan setiap field (untuk record posisional, field di balik setiap property) dengan EqualityComparer<T>.Default, yang memanggil Equals milik tipe itu. Untuk property berupa collection, artinya kesamaan berdasarkan referensi, dan ini sering mengejutkan:
public record Basket(string Owner, List<string> Items);
var x = new Basket("Ana", new List<string> { "tea" });
var y = new Basket("Ana", new List<string> { "tea" });
Console.WriteLine(x == y); // False: two different List objects
Jika sebuah record menyimpan collection dan harus dibandingkan berdasarkan isinya, lakukan override pada Equals(Basket other) dan GetHashCode(), atau pakai immutable collection dengan semantik nilai buatanmu sendiri.
Ekspresi with: perubahan non-destruktif
Record biasanya immutable, jadi kamu "mengubahnya" dengan membuat salinan yang sudah dimodifikasi. with menyalin setiap property, lalu menerapkan pengisian nilai di dalam kurung kurawal:
var mug = new Product("MUG-01", "Mug", 8.50m);
var sale = mug with { Price = 6.00m };
Console.WriteLine(sale); // Product { Sku = MUG-01, Name = Mug, Price = 6.00 }
Console.WriteLine(mug.Price); // 8.50: the original is untouched
Salinannya dangkal (shallow). Property bertipe reference type disalin sebagai referensi, jadi kedua record berbagi object yang sama:
public record Customer { public List<string> Tags { get; init; } = new(); /* ... */ }
var c1 = new Customer { Email = "ana@example.com", Tags = { "vip" } };
var c2 = c1 with { Name = "Ana" };
c2.Tags.Add("newsletter");
Console.WriteLine(string.Join(",", c1.Tags)); // vip,newsletter
Pilih salah satu: buat property record tetap immutable sampai ke dalam (IReadOnlyList<T> yang diisi sekali, atau ImmutableList<T>), atau buat list baru di dalam with: c1 with { Tags = new List<string>(c1.Tags) }.
ToString
ToString yang dibuat otomatis mencetak nama tipe dan setiap property public, sehingga record nyaman dilihat di log dan di debugger:
Console.WriteLine(new Product("MUG-01", "Mug", 8.50m));
// Product { Sku = MUG-01, Name = Mug, Price = 8.50 }
Collection dicetak sebagai nama tipenya (System.Collections.Generic.List`1[System.String]), dan record bersarang dicetak secara rekursif. Kamu bisa mengganti seluruh output-nya dengan melakukan override pada ToString:
public record Money(decimal Amount, string Currency)
{
public override string ToString() => $"{Amount:F2} {Currency}";
}
record struct (C# 10)
record saja berarti record class: sebuah reference type. C# 10 menambahkan record struct, value type dengan member otomatis yang sama:
public readonly record struct Point(int X, int Y);
var p = new Point(3, 4);
var q = p with { Y = 10 };
Console.WriteLine(p == new Point(3, 4)); // True
Console.WriteLine(q); // Point { X = 3, Y = 10 }
Perbedaan default-nya perlu diingat: record struct posisional punya property yang bisa diubah ({ get; set; }), sesuai dengan perilaku struct pada umumnya, sedangkan readonly record struct dan record class punya property init-only. Pilih di antara keduanya seperti saat memilih antara struct dan class: nilai kecil yang bebas disalin cocok dengan readonly record struct; selebihnya, record.
Pewarisan
Record bisa mewarisi record lain (bukan class, dan class tidak bisa mewarisi record). Parameter posisional diteruskan ke base seperti argumen constructor:
public abstract record Shape(string Color);
public record Circle(string Color, double Radius) : Shape(Color);
public record Square(string Color, double Side) : Shape(Color);
Shape a = new Circle("red", 2);
Shape b = new Circle("red", 2);
Shape c = new Square("red", 2);
Console.WriteLine(a == b); // True
Console.WriteLine(a == c); // False: different runtime types are never equal
Console.WriteLine(a); // Circle { Color = red, Radius = 2 }
Kesamaan juga memperhitungkan tipe runtime, lewat property EqualityContract yang dibuat otomatis. Itulah sebabnya Circle tidak pernah sama dengan Square yang punya Color sama, meskipun keduanya dibandingkan lewat Shape, dan itulah sebabnya ToString dan with bekerja pada tipe turunan meskipun variabelnya bertipe base.
Hal yang sama di C# 7: class dengan value equality
Record membuat kode yang sebenarnya bisa kamu tulis sendiri, dan melihat kodenya menjelaskan perilakunya. Berikut class C# 7 yang setara dengan public record Point(int X, int Y);: property get-only, constructor, Deconstruct, value equality, hash code yang sesuai, ==, ToString, dan method With sebagai pengganti ekspresi with.
Output:
True
False
Point { X = 3, Y = 10 }
Point { X = 3, Y = 4 }
x=3, y=10
True
False
Sekitar 30 baris hanya untuk dua property, dan setiap property baru berarti menyentuh lagi constructor, Deconstruct, Equals, GetHashCode, dan ToString. Lupa salah satunya adalah bug klasik (dua point yang == tetapi hash-nya berbeda, sehingga HashSet kehilangan jejaknya). Pekerjaan perawatan seperti itulah yang dihilangkan oleh record.
Class ini sengaja dibuat sealed: value equality yang digabung dengan pewarisan butuh pemeriksaan tipe tambahan yang dibuat record lewat EqualityContract, dan menjadikannya sealed menghindari masalah itu.
Kapan memakai record
Record cocok untuk data yang ditentukan oleh nilainya dan tidak berubah setelah dibuat:
- model request dan response untuk web API;
- pesan, command, dan event yang dikirim antarbagian sebuah sistem;
- object konfigurasi dan options;
- key dictionary gabungan (
record CacheKey(string Region, int Year)); - hasil sebuah perhitungan (
record PriceQuote(decimal Net, decimal Tax)).
Record kurang cocok jika identitas lebih penting daripada nilai: entity Entity Framework tetap "customer 42" meskipun namanya berubah, dan change tracking di EF Core bergantung pada identitas referensi. Pakai class untuk kasus itu.
Kesalahan umum
- Mengira kesamaan collection dibandingkan sampai ke isinya. Property
List<T>dibandingkan berdasarkan referensi. Dua record dengan list yang terlihat sama tidak dianggap sama. - Mengira
withmembuat salinan mendalam. Object dan collection bersarang dipakai bersama oleh record asli dan salinannya. - Tanpa sengaja membuat
record structposisional yang bisa diubah. Tambahkanreadonly, kecuali kamu memang ingin property yang bisa diisi ulang. - Memakai record sebagai entity EF Core. Value equality dan penyalinan bertentangan dengan change tracking.
- Menambahkan record ke proyek C# 8. Record butuh C# 9 (default untuk .NET 5 dan sesudahnya). Untuk target yang lebih lama, tulis class-nya secara manual seperti di atas.
Pertanyaan yang sering diajukan
Apa itu record di C#?
Record (C# 9) adalah class, atau struct jika memakai record struct (C# 10), yang dibuatkan compiler kesamaan berdasarkan nilai, ToString() yang mudah dibaca, method Deconstruct, dan dukungan untuk salinan with. public record Product(string Sku, decimal Price); sudah merupakan tipe lengkap dengan dua property init-only. Dua record dengan nilai property yang sama dianggap sama, meskipun keduanya object yang berbeda.
Apa perbedaan record dan class di C#?
Di baliknya, record adalah class, jadi ia reference type dan bisa mewarisi record lain. Perbedaannya ada di member yang dibuat otomatis: record dibandingkan berdasarkan nilai (== dan Equals memeriksa setiap field), mencetak property-nya lewat ToString(), dan mendukung with. Class biasa dibandingkan berdasarkan referensi dan mencetak nama tipenya, kecuali kamu menulis member itu sendiri.
Apa fungsi ekspresi with di C#?
var sale = product with { Price = 6.00m }; membuat record baru yang menyalin setiap property dari product, lalu mengisi property yang disebutkan. Record aslinya tidak berubah. Salinannya dangkal (shallow): property List<T> dipakai bersama oleh kedua record, jadi menambah isinya lewat salah satu record akan terlihat juga dari record yang lain.
Apa itu record struct di C#?
record struct (C# 10) adalah value type dengan member otomatis yang sama seperti record class: value equality, ToString, Deconstruct, dan with. Berbeda dengan record class, property posisionalnya secara default bisa diubah; deklarasikan sebagai readonly record struct agar menjadi init-only. Pakai untuk nilai kecil seperti koordinat atau jumlah uang.
Kapan sebaiknya memakai record di C#?
Untuk data yang identitasnya adalah nilainya: DTO, model request dan response API, pesan dan event, konfigurasi, serta key untuk dictionary. Hindari record untuk entity yang berubah seiring waktu dan dikenali lewat id, seperti entity Entity Framework, karena value equality dan salinan with bertentangan dengan change tracking.