Menu

C# Record: Değer Eşitliği, with İfadeleri ve record struct

C# record'ları nedir (C# 9 ve sonrası): konumsal sözdizimi, derleyicinin ürettiği üyeler, değere dayalı eşitlik, with ile yıkıcı olmayan kopyalar, yerleşik ToString, C# 10'dan record struct, record'lar arasında kalıtım ve elle yazılmış eşdeğer sınıf.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Bir record, asıl işi veri tutmak olan ve eşitliği o veriyle tanımlanan bir tiptir. Record'lar C# 9 ile geldi. Tek bir satır yazarsınız ve derleyici bir veri sınıfının ihtiyaç duyduğu üyeleri üretir: property'ler, bir constructor, değere dayalı Equals ve ==, GetHashCode, okunabilir bir ToString, Deconstruct ve with kopyaları desteği.

Record'lar C# 9 ya da sonrasını (.NET 5+) gerektirir, bu yüzden bu sayfadaki record kodu çıktısı yorumlarda olacak şekilde düz C# olarak gösterilir. Son bölüm aynı üyeleri C# 7'de elle yazar; onu çalıştırabilirsiniz.

Konumsal record'lar

En kısa biçim, property'leri addan sonra parantez içinde listeler. Her parametre aynı adda public, init-only bir property olur:

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

Bu tek satırdan derleyici şunları üretir:

  • (string Sku, string Name, decimal Price) alan bir constructor;
  • üç public ... { get; init; } property;
  • hepsi üç property'yi karşılaştıran Equals(object), Equals(Product), GetHashCode() ve == ile != operatörleri;
  • tip adını ve her public property'yi yazdıran ToString();
  • Deconstruct(out string Sku, out string Name, out decimal Price);
  • bir kopyalama constructor'ı (protected, sealed bir record'da private) ve with'in kullandığı gizli bir clone metodu.

Bir record normal bir gövdeyle de yazılabilir; property'lerin varsayılanlara ya da doğrulamaya ihtiyacı olduğunda kullanışlıdır:

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" };

Ve iki biçim birleştirilebilir: konumsal parametreler artı süslü parantez içinde ek üyeler.

public record Order(string Id, decimal Subtotal)
{
    public decimal Tax => Subtotal * 0.23m;
    public decimal Total => Subtotal + Tax;
}

Değer eşitliği

Normal bir class için == "bunlar aynı nesne mi?" diye sorar. Bir record için "bunlar aynı değerlere mi sahip?" diye sorar:

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 buna uygun üretilir, bu yüzden record'lar dictionary anahtarı olarak ve bir HashSet<T> içinde doğru çalışır: aynı değerlere sahip ikinci bir record ilkinin girdisini bulur.

Eşitlik her alanı (konumsal record'larda her property'nin arkasındaki alanı) tipin kendi Equals'ını çağıran EqualityComparer<T>.Default ile karşılaştırır. Bir koleksiyon property'si için bu referans eşitliğidir ve insanları şaşırtır:

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

Bir record bir koleksiyon tutuyorsa ve içeriğine göre karşılaştırılması gerekiyorsa Equals(Basket other) ve GetHashCode()'u override edin ya da kendi değer semantiğinize sahip değiştirilemez bir koleksiyon kullanın.

with ifadeleri: yıkıcı olmayan değişiklikler

Record'lar genellikle değiştirilemezdir, bu yüzden birini değiştirilmiş bir kopya oluşturarak "değiştirirsiniz". with her property'yi kopyalar, sonra süslü parantezlerdeki atamaları uygular:

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

Kopya sığdır. Referans tipindeki bir property referans olarak kopyalanır, bu yüzden iki record nesneyi paylaşır:

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

Ya record property'lerini sonuna kadar değiştirilemez tutun (bir kez doldurulan IReadOnlyList<T> ya da ImmutableList<T>) ya da with içinde yeni bir liste oluşturun: c1 with { Tags = new List<string>(c1.Tags) }.

ToString

Üretilen ToString tip adını ve her public property'yi yazdırır; bu da record'ları log'larda ve debugger'da hoş kılar:

Console.WriteLine(new Product("MUG-01", "Mug", 8.50m));
// Product { Sku = MUG-01, Name = Mug, Price = 8.50 }

Koleksiyonlar tip adları olarak yazdırılır (System.Collections.Generic.List`1[System.String]) ve iç içe record'lar özyinelemeli olarak yazdırılır. ToString'i override ederek tüm çıktıyı değiştirebilirsiniz:

public record Money(decimal Amount, string Currency)
{
    public override string ToString() => $"{Amount:F2} {Currency}";
}

record struct (C# 10)

Tek başına record, record class demektir: bir referans tipi. C# 10, aynı üretilmiş üyelere sahip bir değer tipi olan record struct'ı ekledi:

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 }

Varsayılanlardaki fark akılda tutulmaya değer: konumsal bir record struct'ın property'leri, struct'ların genellikle davranış biçimine uygun olarak değiştirilebilirdir ({ get; set; }), readonly record struct ve record class'ınkiler ise init-only'dir. Aralarında bir struct ile bir class arasında seçim yapar gibi seçin: serbestçe kopyalanan küçük değerler readonly record struct'a, geri kalan her şey record'a uyar.

Kalıtım

Bir record başka bir record'dan kalıtım alabilir (bir class'tan değil ve bir class bir record'dan kalıtım alamaz). Konumsal parametreler temele constructor argümanları gibi geçirilir:

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 }

Eşitlik, üretilmiş bir EqualityContract property'si aracılığıyla çalışma zamanı tipini içerir. Bir Circle'ın, ikisi de Shape üzerinden karşılaştırılsa bile aynı Color'a sahip bir Square'e asla eşit olmamasının ve değişken temel tipte tanımlı olsa bile ToString ve with'in türetilmiş tip üzerinde çalışmasının nedeni budur.

C# 7'de aynı şey: değer eşitliğine sahip bir sınıf

Record'lar kendiniz yazabileceğiniz kodu üretir ve onu görmek davranışlarını açıklar. İşte public record Point(int X, int Y);'ye eşdeğer bir C# 7 sınıfı: yalnızca get property'ler, bir constructor, Deconstruct, değer eşitliği, eşleşen bir hash kodu, ==, ToString ve with ifadesinin yerini tutan bir With metodu.

Çıktı:

True
False
Point { X = 3, Y = 10 }
Point { X = 3, Y = 4 }
x=3, y=10
True
False

İki property için yaklaşık 30 satır ve her yeni property constructor'a, Deconstruct'a, Equals'a, GetHashCode'a ve ToString'e yeniden dokunmak demektir. Birini unutmak klasik bir hatadır (== olan ama farklı hash'lenen iki nokta; bir HashSet onları kaybeder). Record'ların ortadan kaldırdığı şey bu bakım yüküdür.

Sınıf bilerek sealed'dır: kalıtımla birleşen değer eşitliği, record'ların EqualityContract aracılığıyla ürettiği ek tip kontrolünü gerektirir ve sealed yapmak sorunun etrafından dolaşır.

Record ne zaman kullanılmalı

Record'lar değerleriyle tanımlanan ve oluşturulduktan sonra değişmeyen verilere uyar:

  • web API'leri için istek ve yanıt modelleri;
  • bir sistemin parçaları arasında geçirilen mesajlar, komutlar ve event'ler;
  • yapılandırma ve seçenek nesneleri;
  • bileşik dictionary anahtarları (record CacheKey(string Region, int Year));
  • bir hesaplamanın sonuçları (record PriceQuote(decimal Net, decimal Tax)).

Kimliğin değerlerden daha önemli olduğu yerlere pek uymazlar: bir Entity Framework varlığı adı değiştikten sonra bile "42 numaralı müşteri"dir ve EF Core'un değişiklik izlemesi referans kimliğine dayanır. Orada bir class kullanın.

Yaygın hatalar

  • Koleksiyonlar için derin eşitlik beklemek. Bir List<T> property'si referansa göre karşılaştırılır. Listeleri eşit görünen iki record eşit değildir.
  • with'in derin kopyalamasını beklemek. İç içe nesneler ve koleksiyonlar orijinal ile kopya arasında paylaşılır.
  • Yanlışlıkla değiştirilebilir konumsal record struct. Ayarlanabilir property'ler istemiyorsanız readonly ekleyin.
  • Record'ları EF Core varlıkları olarak kullanmak. Değer eşitliği ve kopyalama değişiklik izlemeyle çatışır.
  • Bir C# 8 projesine record eklemek. Record'lar C# 9 gerektirir (.NET 5 ve sonrası için varsayılan). Eski hedeflerde sınıfı yukarıda gösterildiği gibi elle yazın.

Sıkça Sorulan Sorular

C#'ta record nedir?

Record (C# 9), derleyicinin değere dayalı eşitlik, okunabilir bir ToString(), bir Deconstruct metodu ve with kopyaları desteği ürettiği bir class'tır; record struct (C# 10) ile bir struct'tır. public record Product(string Sku, decimal Price); iki init-only property'ye sahip eksiksiz bir tiptir. Property değerleri eşit olan iki record, farklı nesneler olsalar da eşittir.

C#'ta record ile class arasındaki fark nedir?

Bir record altta bir class'tır, bu yüzden bir referans tipidir ve başka record'lardan kalıtım alabilir. Farklar üretilen üyelerdedir: record'lar değere göre karşılaştırır (== ve Equals her alanı kontrol eder), ToString()'ten property'lerini yazdırır ve with'i destekler. Normal bir class ise siz bu üyeleri yazmadıkça referansa göre karşılaştırır ve tip adını yazdırır.

C#'ta with ifadesi ne yapar?

var sale = product with { Price = 6.00m };, product'ın her property'sini kopyalayan ve sonra listelenenleri ayarlayan yeni bir record oluşturur. Orijinal değişmez. Kopya sığdır: bir List<T> property'si iki record tarafından paylaşılır, bu yüzden birinin üzerinden ona eklemek diğerinden de görünür.

C#'ta record struct nedir?

record struct (C# 10), bir record class ile aynı üretilmiş üyelere sahip bir değer tipidir: değer eşitliği, ToString, Deconstruct ve with. Bir record class'ın aksine konumsal property'leri varsayılan olarak değiştirilebilirdir; onları init-only yapmak için readonly record struct bildirin. Koordinatlar ya da para miktarları gibi küçük değerler için kullanın.

C#'ta record ne zaman kullanılmalı?

Kimliği değerleri olan veriler için: DTO'lar, API istek ve yanıt modelleri, mesajlar ve event'ler, yapılandırma ve dictionary anahtarları. Zamanla değişen ve bir id ile tanımlanan varlıklar için, örneğin Entity Framework varlıkları için record'lardan kaçının, çünkü değer eşitliği ve with kopyaları değişiklik izlemeye ters düşer.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA