النسخ العميق في C#: لماذا هو صداع كل مطور؟
\n\nهل سبق لك أن نسخت كائنًا في C#، ثم اكتشفت بدهشة أن التعديل في "النسخة" يفسد الأصل؟ مألوف؟ هذه هي المشكلة الكلاسيكية للـ النسخ السطحي (shallow copy). عندما يحتوي الكائن على حقول مرجعية (فئات أخرى، قوائم، مصفوفات)، فإن MemberwiseClone() ينسخ المراجع فقط. في النهاية، يشير كل من الأصل والنسخة إلى نفس الكائن المتداخل.
في هذه المقالة، سنحلل الاستنساخ العميق في C# — عملية إنشاء نسخة مستقلة تمامًا من الكائن مع جميع الكائنات المتداخلة. سنقارن 5 طرق شائعة، ونكتب كودًا حقيقيًا، ونجري اختبار أداء ذهني، ونكتشف أي طريقة تختار لمشروعك.
\n\nما هو Deep Clone وكيف يختلف عن Shallow Clone؟
\n\nتخيل أن لديك فئة Person تحتوي على Address:
public class Address\n{\n public string City { get; set; }\n public string Street { get; set; }\n}\n\npublic class Person\n{\n public string Name { get; set; }\n public int Age { get; set; }\n public Address HomeAddress { get; set; }\n}\n\nفي النسخ السطحي (shallow clone) (العادي MemberwiseClone()) ستحصل على شخص جديد، لكن HomeAddress الخاص به سيشير إلى نفس كائن Address الموجود في الأصل. غيرت الشارع في النسخة — تغير الشارع أيضًا في الأصل. كارثة.
الاستنساخ العميق (Deep clone) يحل هذه المشكلة: فهو ينسخ بشكل متكرر جميع الكائنات المتداخلة، مما ينشئ بنية بيانات مستقلة تمامًا.
\n\nالطريقة 1: التنفيذ اليدوي عبر ICloneable ومنشئات النسخ
\n\nالطريقة الأكثر وضوحًا، ولكنها أيضًا الأكثر استهلاكًا للوقت، هي تنفيذ الواجهة ICloneable أو كتابة طريقة DeepClone() الخاصة بك.
مثال على التنفيذ:
\n\npublic class Address : ICloneable\n{\n public string City { get; set; }\n public string Street { get; set; }\n\n public object Clone()\n {\n return new Address { City = this.City, Street = this.Street };\n }\n}\n\npublic class Person : ICloneable\n{\n public string Name { get; set; }\n public int Age { get; set; }\n public Address HomeAddress { get; set; }\n\n public object Clone()\n {\n return new Person\n {\n Name = this.Name,\n Age = this.Age,\n HomeAddress = (Address)this.HomeAddress?.Clone()\n };\n }\n}\n\nالإيجابيات:
\n- \n
- تحكم كامل في عملية النسخ \n
- أقصى أداء \n
- كود مفهوم (بدون "سحر") \n
السلبيات:
\n- \n
- كمية هائلة من كود boilerplate \n
- سهولة نسيان تحديث طريقة Clone() عند إضافة حقل جديد \n
- لا يعمل مع الفئات الخارجية (مثل تلك من المكتبات) \n
متى تستخدم: فقط لـ DTOs البسيطة التي تحتوي على 2-3 حقول، حيث يكون الأداء حرجًا (مثل محركات الألعاب).
\n\nالطريقة 2: الانعكاس (Reflection) — الجندي العالمي
\n\nالانعكاس يسمح بكتابة طريقة استنساخ عميق عالمية تعمل مع أي كائن.
\n\nمثال على الكود:
\n\npublic static T DeepCloneByReflection<T>(T source)\n{\n if (source == null) return default;\n\n var type = source.GetType();\n \n // للأنواع القيمية والسلاسل النصية، نعيد القيمة فقط\n if (type.IsValueType || type == typeof(string))\n return source;\n\n // للمصفوفات\n if (type.IsArray)\n {\n var arr = source as Array;\n var clonedArr = Array.CreateInstance(type.GetElementType(), arr.Length);\n for (int i = 0; i < arr.Length; i++)\n clonedArr.SetValue(DeepCloneByReflection(arr.GetValue(i)), i);\n