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

البنية Struct في C#: أنواع القيم والبنية مقابل الصنف ومتى تستخدمها

كيف تعمل البنى في C#: دلالات القيمة والنسخ عند الإسناد، وخطأ التعديل في List<T>، وقواعد المُنشئات، والمساواة، والتغليف (boxing)، وreadonly struct وrecord struct، ومقارنة البنية بالصنف مع إرشادات متى تكون البنية الخيار الصحيح.

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

البنية (struct) نوع قيمة تعرّفه بنفسك. تبدو كصنف، بحقول وخصائص ودوال ومُنشئات، لكن متغيّر نوع البنية يحتوي على البيانات مباشرة بدل مرجع إلى كائن. هذا الفرق الواحد يغيّر طريقة تصرّف الإسناد واستدعاءات الدوال والمساواة وnull.

دلالات القيمة: الإسناد ينسخ

الأنواع الرقمية المدمجة وbool وchar وDateTime وTimeSpan وGuid كلها بنى. وبناك تعمل بالطريقة نفسها:

المخرجات:

struct: s1.X = 1, s2.X = 50
class:  c1.X = 50, c2.X = 50
after Move: s1.X = 1, c1.X = 150

مع البنية تكون s2 نسخة مستقلة، وتعمل Move على نسختها الخاصة أيضًا. لتسمح لدالة بتغيير بنية المستدعي مرّرها بالمرجع بـ ref (راجع ref وout). ولتجنّب نسخ بنية كبيرة دون السماح بالتغيير أضافت C# 7.2 معاملات in.

نتائج أخرى لكونها نوع قيمة:

  • لا يمكن لمتغيّر بنية أن يكون null أبدًا. لا تُترجم PointS p = null;. استخدم PointS? (نوع قيمة قابل لـ null) حين يكون لـ "لا قيمة" معنى.
  • البنية غير المهيّأة أصفار كلها: الحقول الرقمية 0، وحقول bool تساوي false، والحقول المرجعية null.
  • لا يمكن للبنية أن ترث من بنية أو صنف آخر، ولا يمكن لشيء أن يرث من بنية. ويمكنها تنفيذ واجهات.
  • متغيّرات البنى المحلية تعيش عادة في المكدّس أو داخل الكائن المحتوي، فإنشاء واحدة لا يحجز في الكومة.

فخ قائمة البنى: CS1612

لأن قراءة بنية تنتج نسخة، لا يُترجم هذا السطر البريء المظهر:

var points = new List<PointS> { new PointS(1, 2) };
points[0].X = 10;
// error CS1612: Cannot modify the return value of 'List<PointS>.this[int]' because it is not a variable

مفهرس List<T> دالة تعيد نسخة من العنصر. ضبط X على تلك النسخة المؤقتة لن يغيّر شيئًا، فيوقفك المترجم. ويظهر الخطأ نفسه حين تعيد خاصية بنية: order.Location.X = 10. انسخ، وعدّل، وأعد الكتابة:

المخرجات:

35
7

المصفوفات هي الاستثناء: array[0] هي العنصر نفسه لا نسخة. هذا الألم المتكرر هو سبب النصيحة القياسية بجعل البنى غير قابلة للتغيير: إن لم يمكن تعديل البنية فلا شيء يضيع بتعديل نسخة.

المُنشئات والقيم الافتراضية

في C# 7 حتى 9 تكون قواعد مُنشئات البنى صارمة:

  • لا يمكنك التصريح بمُنشئ بلا معاملات. new PointS() موجود دائمًا ويصفّر كل حقل.
  • المُنشئ الذي تصرّح به يجب أن يُسند إلى كل حقل (وكل خاصية تلقائية) قبل أن يعود.
  • مهيّئات الحقول (public int X = 1;) غير مسموحة على حقول المثيل.

المخرجات:

19.90 EUR
0.00 (none)
0.00 (none)
0.00 (none)

تُظهر Money أيضًا لماذا تهمّ القيمة الافتراضية: Money المصفّرة عملتها null، ويجب أن تتعامل شيفرتك معها، لأن المصفوفات وdefault(T) والحقول غير المهيّأة كلها تنتج واحدة.

خفّفت الإصدارات الأحدث هذه القواعد:

// C# 10: explicit parameterless constructors and field initializers
public struct Settings
{
    public int Retries = 3;
    public Settings() { }
}

// C# 11: fields you do not assign in a constructor are zeroed automatically,
// instead of being a compile error.

المُنشئ بلا معاملات في C# 10 يُنفَّذ لـ new Settings() لكن لا لـ default(Settings) ولا لعناصر المصفوفات، التي تظل أصفارًا كلها. هذا الانقسام يفاجئ الناس، فاستخدمه بحذر.

readonly struct (C# 7.2)

تعليم البنية نفسها بـ readonly يجعل المترجم يفرض عدم القابلية للتغيير: يجب أن يكون كل حقل readonly وكل خاصية تلقائية للقراءة فقط.

public readonly struct Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius) { Celsius = celsius; }

    public Temperature WarmerBy(double delta) => new Temperature(Celsius + delta);   // returns a new value
}

إلى جانب توثيق القصد، يفيد ذلك الأداء: حين تُخزَّن بنية غير readonly في حقل readonly أو تُمرَّر كمعامل in، ينسخها المترجم قبل كل استدعاء دالة (لا يستطيع أن يعرف أن الدالة لن تعدّلها). أما readonly struct فلا تحتاج إلى مثل هذه النسخ الدفاعية. وفي C# 7.0 ما زال بإمكانك جعل كل حقل readonly، كما تفعل Money بخصائص للقراءة فقط: يمنحك ذلك عدم القابلية للتغيير، لكن لا النسخ الموفّرة، لأن المترجم لا يثق إلا ببنية مصرّح بها readonly.

المساواة

تقارن Equals على بنية حقلًا حقلًا افتراضيًا، وهذا سلوك القيمة الذي تريده. لكن التنفيذ الافتراضي (ValueType.Equals) قد يستخدم الانعكاس وهو بطيء، والمعامل == غير معرّف إطلاقًا: a == b على بنيتك هو الخطأ CS0019. نفّذ الاثنين حين ستُقارن البنية:

المخرجات:

True
True
True

تنفيذ IEquatable<T> مهم للمجموعات: تستدعي HashSet<T> وDictionary<TKey, TValue> وList<T>.Contains الدالة Equals(GridCell) مباشرة بدل تغليف كل قيمة لاستدعاء Equals(object). وتولّد بنى السجلات (أدناه) كل هذا عنك.

التغليف (Boxing)

تحويل بنية إلى object أو إلى نوع واجهة يغلّفها: ينسخ وقت التشغيل القيمة إلى كائن جديد في الكومة. ويصبح الغلاف والأصل مستقلين:

المخرجات:

2
0

يكلّف التغليف حجزًا في كل مرة، ولهذا كانت المجموعات القديمة غير العامة مثل ArrayList بطيئة مع أنواع القيم ولهذا حلّت الأنواع العامة محلها. ويعني أيضًا أن البنية القابلة للتغيير المستخدمة عبر واجهة تتغيّر في غلافها لا في الأصل، وهذا سبب إضافي لإبقاء البنى غير قابلة للتغيير.

البنية مقابل الصنف

البنية structالصنف class
النوعنوع قيمةنوع مرجعي
الإسناد والمعاملاتتنسخ البياناتتنسخ المرجع
يمكن أن تكون nullلا (T? يمكن)نعم
القيمة الافتراضيةكل الحقول مصفّرةnull
الوراثةلا شيء؛ يمكنها تنفيذ واجهاتصنف أساس واحد، وواجهات
==غير معرّف ما لم تحمّلهمساواة المراجع ما لم يُحمَّل
Equals الافتراضييقارن الحقوليقارن المراجع
الحجزمضمّنة (المكدّس أو الكائن المحتوي)الكومة، يجمعها جامع النفايات
مناسبة لـالقيم الصغيرة غير القابلة للتغييرالكيانات، والحالة الكبيرة أو المشتركة

متى تستخدم بنية

اختر بنية حين تتحقق كل هذه: النوع قيمة منطقية واحدة (إحداثية، مبلغ مالي، لون، نطاق تواريخ)، وصغير (إرشاد Microsoft نحو 16 بايت، أي أربعة int تقريبًا)، وغير قابل للتغيير، ولا تحتاج إلى الوراثة. تؤتي البنى ثمارها حين تنشئ منها أعدادًا كبيرة جدًا، مثل ملايين النقاط في مصفوفة، لأنها تتجنّب حجزًا في الكومة ومدخلًا لدى جامع النفايات لكل عنصر.

واختر صنفًا لكل ما له هوية (عميل، طلب)، وكل ما هو كبير، وكل ما يُعدَّل من عدة أماكن، وكلما لم تكن متأكدًا. البنية الكبيرة القابلة للتغيير تمنحك كلفة النسخ مع ارتباك تعديل النسخ.

record struct (C# 10)

أضافت C# 10 النوع record struct، الذي يولّد مساواة القيمة و== وToString ودعم with لبنية في سطر واحد:

public readonly record struct Coordinate(double Lat, double Lng);

var home = new Coordinate(38.72, -9.14);
var same = new Coordinate(38.72, -9.14);
Console.WriteLine(home == same);            // True
Console.WriteLine(home);                    // Coordinate { Lat = 38.72, Lng = -9.14 }
var north = home with { Lat = 38.80 };

للـ record struct العادية خصائص موضعية قابلة للتغيير؛ وتجعلها readonly record struct للتهيئة فقط، وهذا عادة ما تريده. تغطي صفحة السجلات نسخة الصنف وتُظهر الأعضاء التي يكتبها المترجم.

أخطاء شائعة

  • تعديل بنية عبر نسخة. list[0].X = 1 (CS1612)، أو متغيّر foreach (CS1654)، أو نتيجة موصّل get لخاصية. أعد كتابة النسخة المعدّلة، أو اجعل البنية غير قابلة للتغيير.
  • البنى الكبيرة. كل إسناد واستدعاء ينسخها كلها. بعد بضعة حقول يكون الصنف أسرع عادة.
  • نسيان الافتراضي المصفّر. تنشئ المصفوفات وdefault(T) قيم بنى دون تشغيل مُنشئك، فقد تظل الحقول التي تتحقق منها هناك صفرًا أو null.
  • المقارنة بـ == دون تعريفه. CS0019. نفّذ IEquatable<T> والمعاملات، أو استخدم record struct.
  • البنى القابلة للتغيير خلف الواجهات. يعني التغليف أن التغيير يحدث لنسخة لا تنظر إليها.

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

ما هي البنية (struct) في C#؟

البنية نوع قيمة يعرّفه المستخدم: struct Point { public int X; public int Y; }. يحمل متغيّر من نوع بنية البيانات نفسها لا مرجعًا إلى كائن، فينسخ الإسناد والتمرير إلى دالة القيمة كلها. int وdouble وDateTime وGuid كلها بنى.

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

الصنف نوع مرجعي: تتشارك المتغيّرات كائنًا واحدًا، وnull مسموحة. أما البنية فنوع قيمة: لكل متغيّر نسخته الخاصة، ولا يمكن أن تكون null (ما لم تستخدم Point?)، ولا يمكنها الوراثة ولا أن يُورث منها، وقيمتها الافتراضية كل الحقول مصفّرة. تناسب البنى القيم الصغيرة غير القابلة للتغيير؛ وتناسب الأصناف الكيانات ذات الهوية والسلوك.

متى أستخدم بنية بدل صنف في C#؟

حين يمثّل النوع قيمة صغيرة واحدة (إحداثية، مبلغ مالي، نطاق تواريخ)، ويكون مثاليًا غير قابل للتغيير، ويُقارن بمحتواه، ويُنشأ بأعداد كبيرة حيث يهمّ تجنّب الحجز في الكومة. وتضيف إرشادات Microsoft حجمًا يقارب 16 بايت أو أقل. إن كان للنوع هوية، أو حقول كثيرة، أو يُعدَّل عبر مراجع، فاستخدم صنفًا.

لماذا أحصل على "Cannot modify the return value because it is not a variable"؟

هذا هو الخطأ CS1612، ويأتي عادة من list[0].X = 5 على List<Point> من البنى. يعيد المفهرس نسخة من البنية، فسيضيع تغيير النسخة، ويرفض المترجم ذلك. انسخ العنصر إلى متغيّر، وغيّره، وأسنده مجددًا: var p = list[0]; p.X = 5; list[0] = p;. لا تعاني المصفوفات من هذه المشكلة لأن array[0] تشير إلى العنصر نفسه.

ما هي readonly struct في C#؟

تصرّح readonly struct (C# 7.2) بأن لا عضو في البنية يعدّل حالتها: يجب أن تكون كل الحقول readonly والخصائص التلقائية للقراءة فقط. يفرض المترجم ذلك، ويتيح له تجنّب النسخ الدفاعية حين تُمرَّر البنية بـ in أو تُخزَّن في حقل readonly. ويجب أن تكون معظم البنى readonly.

Coddy programming languages illustration

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

ابدأ الآن