Menu
flag Ar iconالعربيةdown icon

السجل Record في C#: مساواة القيمة وتعابير with وrecord struct

ما هي السجلات في C# (C# 9 وما بعده): الصياغة الموضعية، والأعضاء التي يولّدها المترجم، والمساواة بالقيمة، والنسخ غير المدمّرة بـ with، وToString المدمجة، وrecord struct من C# 10، والوراثة بين السجلات، والصنف المكافئ مكتوبًا يدويًا.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

السجل (record) نوع مهمته الرئيسية حمل البيانات، وتُعرَّف مساواته بتلك البيانات. وصلت السجلات في C# 9. تكتب سطرًا واحدًا، ويولّد المترجم الأعضاء التي يحتاجها صنف البيانات: الخصائص، والمُنشئ، وEquals و== بالقيمة، وGetHashCode، وToString مقروءة، وDeconstruct، ودعم نسخ with.

تحتاج السجلات إلى C# 9 أو أحدث (.NET 5+)، لذا تُعرض شيفرة السجلات في هذه الصفحة كـ C# عادية، ومخرجاتها في التعليقات. ويكتب القسم الأخير الأعضاء نفسها يدويًا في C# 7، ويمكنك تشغيله.

السجلات الموضعية

يسرد الشكل الأقصر الخصائص بين أقواس بعد الاسم. يصبح كل معامل خاصية عامة للتهيئة فقط بالاسم نفسه:

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

من هذا السطر الواحد يولّد المترجم:

  • مُنشئًا يأخذ (string Sku, string Name, decimal Price)؛
  • ثلاث خصائص public ... { get; init; }؛
  • Equals(object) وEquals(Product) وGetHashCode() والمعاملين == و!=، وكلها تقارن الخصائص الثلاث؛
  • ToString() تطبع اسم النوع وكل خاصية عامة؛
  • Deconstruct(out string Sku, out string Name, out decimal Price)؛
  • مُنشئ نسخ (protected، أو private في سجل مختوم) ودالة استنساخ مخفية تستخدمها with.

ويمكن أيضًا كتابة السجل بجسم عادي، وهذا مفيد حين تحتاج الخصائص إلى قيم افتراضية أو تحقق:

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

ويمكن الجمع بين الشكلين: معاملات موضعية وأعضاء إضافية بين أقواس معقوصة.

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

مساواة القيمة

في الصنف العادي تسأل == "هل هذا الكائن نفسه؟". وفي السجل تسأل "هل لهما القيم نفسها؟":

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 بما يطابق، فتعمل السجلات بشكل صحيح كمفاتيح قاموس وفي HashSet<T>: سجل ثانٍ بالقيم نفسها يجد مدخل الأول.

تقارن المساواة كل حقل (في السجلات الموضعية، الحقل خلف كل خاصية) بـ EqualityComparer<T>.Default، التي تستدعي Equals الخاصة بالنوع. وفي خاصية مجموعة تكون هذه مساواة مراجع، وهذا يفاجئ الناس:

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

إن حمل سجل مجموعة ويجب أن يقارن بمحتواها فتجاوز Equals(Basket other) وGetHashCode()، أو استخدم مجموعة غير قابلة للتغيير بدلالات قيمة من صنعك.

تعابير with: تغييرات غير مدمّرة

السجلات عادة غير قابلة للتغيير، فـ "تغيّر" واحدًا بإنشاء نسخة معدّلة. تنسخ with كل خاصية، ثم تطبّق الإسنادات بين الأقواس:

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

النسخ سطحي. تُنسخ الخاصية من نوع مرجعي كمرجع، فيتشارك السجلان الكائن:

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

إما أن تبقي خصائص السجل غير قابلة للتغيير حتى العمق (IReadOnlyList<T> تُملأ مرة، أو ImmutableList<T>)، أو تنشئ قائمة جديدة في with: c1 with { Tags = new List<string>(c1.Tags) }.

ToString

تطبع ToString المولّدة اسم النوع وكل خاصية عامة، وهذا يجعل السجلات مريحة في السجلات النصية والمصحّح:

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

تُطبع المجموعات كاسم نوعها (System.Collections.Generic.List`1[System.String])، وتُطبع السجلات المتداخلة تكراريًا. ويمكنك استبدال المخرجات كلها بتجاوز ToString:

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

record struct (C# 10)

record وحدها تعني record class: نوعًا مرجعيًا. أضافت C# 10 النوع record struct، وهو نوع قيمة بالأعضاء المولّدة نفسها:

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 }

الفرق في الافتراضيات يستحق التذكّر: لـ record struct الموضعية خصائص قابلة للتغيير ({ get; set; })، بما يطابق سلوك البنى المعتاد، بينما لـ readonly record struct وrecord class خصائص للتهيئة فقط. اختر بينها كما تختار بين بنية وصنف: القيم الصغيرة التي تُنسخ بحرية تناسبها readonly record struct؛ وكل ما عداها record.

الوراثة

يمكن للسجل أن يرث من سجل آخر (لا من صنف، ولا يمكن لصنف أن يرث من سجل). تُمرَّر المعاملات الموضعية إلى الأساس كوسائط مُنشئ:

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 }

تتضمن المساواة النوع وقت التشغيل، عبر خاصية EqualityContract مولّدة. ولهذا لا يساوي Circle أبدًا Square بـ Color نفسه، مع أن كليهما يُقارن عبر Shape، ولهذا تعمل ToString وwith على النوع المشتق حتى حين يكون المتغيّر من نوع الأساس.

الشيء نفسه في C# 7: صنف بمساواة قيمة

تولّد السجلات شيفرة يمكنك كتابتها بنفسك، ورؤيتها تشرح سلوكها. هذا صنف في C# 7 مكافئ لـ public record Point(int X, int Y);: خصائص للقراءة فقط، ومُنشئ، وDeconstruct، ومساواة قيمة، ورمز تجزئة مطابق، و==، وToString، ودالة With تقوم مقام تعبير with.

المخرجات:

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

نحو 30 سطرًا لخاصيتين، وكل خاصية جديدة تعني لمس المُنشئ وDeconstruct وEquals وGetHashCode وToString مجددًا. ونسيان واحدة منها خطأ تقليدي (نقطتان متساويتان بـ == لكن بتجزئتين مختلفتين، فتفقد HashSet أثرهما). هذه الصيانة هي ما تزيله السجلات.

الصنف sealed عن قصد: مساواة القيمة مع الوراثة تحتاج إلى فحص النوع الإضافي الذي تولّده السجلات عبر EqualityContract، والختم يتفادى المشكلة.

متى تستخدم سجلًا

تناسب السجلات البيانات التي تُعرَّف بقيمها ولا تتغيّر بعد الإنشاء:

  • نماذج الطلبات والاستجابات لواجهات API الويب؛
  • الرسائل والأوامر والأحداث المتبادلة بين أجزاء نظام؛
  • كائنات الإعدادات والخيارات؛
  • مفاتيح القواميس المركّبة (record CacheKey(string Region, int Year))؛
  • نتائج حساب (record PriceQuote(decimal Net, decimal Tax)).

ولا تناسب حيث تهمّ الهوية أكثر من القيم: كيان Entity Framework هو "العميل 42" حتى بعد تغيّر اسمه، ويعتمد تتبّع التغييرات في EF Core على هوية المرجع. استخدم صنفًا هناك.

أخطاء شائعة

  • توقّع مساواة عميقة للمجموعات. خاصية List<T> تُقارن بالمرجع. سجلان بقائمتين متشابهتين ليسا متساويين.
  • توقّع أن تنسخ with نسخًا عميقًا. الكائنات والمجموعات المتداخلة مشتركة بين الأصل والنسخة.
  • record struct موضعية قابلة للتغيير عن غير قصد. أضف readonly ما لم تكن تريد خصائص قابلة للضبط.
  • استخدام السجلات ككيانات EF Core. مساواة القيمة والنسخ يتعارضان مع تتبّع التغييرات.
  • إضافة سجل إلى مشروع C# 8. تحتاج السجلات إلى C# 9 (الافتراضي في .NET 5 وما بعده). على الأهداف الأقدم اكتب الصنف يدويًا كما يظهر أعلاه.

الأسئلة الشائعة

ما هو السجل (record) في C#؟

السجل (C# 9) صنف، أو مع record struct (C# 10) بنية، يولّد له المترجم مساواة بالقيمة وToString() مقروءة ودالة Deconstruct ودعمًا لنسخ with. public record Product(string Sku, decimal Price); نوع كامل بخاصيتين للتهيئة فقط. سجلان بقيم خصائص متساوية متساويان، مع أنهما كائنان مختلفان.

ما الفرق بين السجل والصنف في C#؟

record صنف في الأساس، فهو نوع مرجعي ويمكنه الوراثة من سجلات أخرى. الفروق في الأعضاء المولّدة: تقارن السجلات بالقيمة (تفحص == وEquals كل حقل)، وتطبع خصائصها من ToString()، وتدعم with. أما الصنف العادي فيقارن بالمرجع ويطبع اسم نوعه ما لم تكتب تلك الأعضاء بنفسك.

ماذا يفعل تعبير with في C#؟

تنشئ var sale = product with { Price = 6.00m }; سجلًا جديدًا ينسخ كل خصائص product ثم يضبط المذكورة. يبقى الأصل دون تغيير. والنسخ سطحي: خاصية List<T> يتشاركها السجلان، فالإضافة إليها عبر أحدهما مرئية عبر الآخر.

ما هي record struct في C#؟

record struct (C# 10) نوع قيمة له الأعضاء المولّدة نفسها لصنف السجل: مساواة القيمة وToString وDeconstruct وwith. وبخلاف صنف السجل تكون خصائصه الموضعية قابلة للتغيير افتراضيًا؛ صرّح بها readonly record struct لجعلها للتهيئة فقط. استخدمها للقيم الصغيرة مثل الإحداثيات أو المبالغ المالية.

متى أستخدم سجلًا في C#؟

للبيانات التي هويتها قيمها: كائنات نقل البيانات (DTO)، ونماذج طلبات API واستجاباتها، والرسائل والأحداث، والإعدادات، ومفاتيح القواميس. وتجنّب السجلات للكيانات التي تتغيّر مع الوقت وتُعرَّف بمعرّف، مثل كيانات Entity Framework، لأن مساواة القيمة ونسخ with تعارض تتبّع التغييرات.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن