يتيح الانعكاس (reflection) لبرنامج فحص الأنواع أثناء عمله: أي خصائص للصنف، وما قيمها، وأي دوال موجودة، وأي سمات مرفقة. والسمات (attributes) هي النصف الآخر: وسوم تصريحية بين أقواس مربعة، مثل [Obsolete] أو [JsonPropertyName("id")]، لا تعني شيئًا بذاتها حتى يقرأها المترجم أو شيفرة ما بالانعكاس. أدوات التسلسل وأدوات ORM ومكتبات التحقق وأطر الاختبار وتوجيه ASP.NET كلها تعمل بهذه الطريقة.
نادرًا ما تحتاج معظم شيفرة التطبيقات إلى الانعكاس مباشرة، لكن معرفة طريقة عمله تشرح كثيرًا من سلوك الأطر.
كائنات Type: typeof وGetType
كل شيء يبدأ من System.Type. هناك طريقتان للحصول على واحد:
المخرجات:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
الفرق الذي يجب تذكّره: typeof(X) تسمّي نوعًا تعرفه حين تكتب الشيفرة، وobj.GetType() تسأل كائنًا عمّا هو حقًا. مقارنة GetType() == typeof(Employee) تطابق تام يفشل مع الأصناف المشتقة، وهذا عادة ليس ما تريده؛ أما is وIsAssignableFrom فتحترمان الوراثة. وnameof تبدو مشابهة لكنها ليست انعكاسًا إطلاقًا: يستبدلها المترجم بثابت نصي.
استدعاء GetType() على مرجع null يرمي NullReferenceException، لأنه لا كائن لتسأله. وعلى نوع قيمة قابل لـ null مغلّف تعيد النوع الأساسي: ((int?)5).GetType() هي System.Int32.
قراءة الخصائص وضبطها
تسرد GetProperties() الخصائص العامة لنوع ككائنات PropertyInfo، يستطيع كل منها قراءة القيمة وكتابتها على مثيل معين:
المخرجات:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
ثلاثة أمور يُظهرها هذا:
- تعيد
GetValueقيمةobject، فتعود أنواع القيم مغلّفة وتحوّلها لتستخدمها. - تعيد
GetPropertyباسم غير موجودnull، ويرمي الاستدعاء التالي عليهاNullReferenceException. افحص قبل الاستخدام. - يصل
BindingFlags.NonPublic | BindingFlags.Instanceإلى الأعضاء الخاصة. وهذا مشروع في الأدوات والاختبارات، لكنه يتجاوز التغليف وينكسر بصمت حين يُعاد هيكلة الصنف.
هذه الحلقة هي في جوهرها طريقة عمل مصدّر CSV أو أداة تسلسل من كائن إلى JSON: امشِ على الخصائص، واقرأ كل قيمة، ونسّقها.
استدعاء الدوال وإنشاء الكائنات بالاسم
تجد GetMethod دالة، وتستدعيها Invoke بمصفوفة وسائط. وتنشئ Activator.CreateInstance كائنًا من Type، وهكذا تبني أنظمة الإضافات وحاويات حقن الاعتماديات أنواعًا تُختار وقت التشغيل:
المخرجات:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
يقصر DeclaredOnly القائمة على الأعضاء المصرّح بها في الصنف نفسه؛ ودونه تعيد GetMethods أيضًا ToString وEquals وGetHashCode وGetType من object. وتحتاج Type.GetType("Name") إلى الاسم المؤهّل بفضاء الأسماء، وللأنواع في تجميعات أخرى إلى اسم التجميع أيضًا ("MyApp.Plugins.Csv, MyApp.Plugins").
إن رمت الدالة المستدعاة استثناءً تغلّفه Invoke في TargetInvocationException؛ والأصلي في InnerException الخاص به.
السمات: وسوم يقرؤها المترجم والأطر
تُكتب السمة بين أقواس مربعة قبل الشيء الذي تصفه. يعرّف الإطار كثيرًا منها؛ بعضها يغيّر ما يفعله المترجم:
public class OrderService
{
[Obsolete("Use PlaceOrderAsync instead.")]
public void PlaceOrder(Order order) { }
// Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
// [Obsolete("...", true)] makes it error CS0619 instead.
[Conditional("DEBUG")]
public void Trace(string message) => Console.WriteLine(message);
// Calls to Trace are removed entirely from builds without the DEBUG symbol.
}
[Flags] enum Channels { None = 0, Email = 1, Sms = 2 } // changes how ToString formats combinations
[Serializable] class Snapshot { } // marks a type for legacy binary serialization
وأخرى تقرؤها المكتبات وقت التشغيل: [JsonPropertyName] و[JsonIgnore] تقرؤهما System.Text.Json، و[Required] و[MaxLength] يقرؤهما التحقق من النماذج في ASP.NET Core وEntity Framework، و[HttpGet("orders/{id}")] يقرؤها توجيه ASP.NET، و[Fact] و[Test] تقرؤهما مشغّلات الاختبارات. السمة نفسها لا تفعل شيئًا؛ الشيفرة التي تبحث عنها هي التي تفعل.
يُختصر الاسم ObsoleteAttribute إلى [Obsolete] عند التطبيق: عرفًا ينتهي اسم كل صنف سمة بـ Attribute، وتتيح لك C# حذف اللاحقة.
التصريح بسمة مخصصة وقراءتها
السمة المخصصة صنف مشتق من Attribute. تحدد [AttributeUsage] ما يمكن تطبيقها عليه. تصبح معاملات المُنشئ وسائط موضعية، وتصبح الخصائص العامة القابلة للضبط وسائط مسمّاة:
المخرجات:
Username must be at most 20 characters
Email is required
Keep the city code short
0
هذه نسخة مصغّرة مما يفعله التحقق من النماذج في ASP.NET Core مع System.ComponentModel.DataAnnotations. يجب أن تكون وسائط السمات ثوابت وقت الترجمة (أرقام، نصوص، typeof(...)، قيم تعدادات، أو مصفوفات منها)، لأنها تُخزَّن في البيانات الوصفية للتجميع. وGetCustomAttribute<T>() دالة توسيع في System.Reflection؛ وتوجد أيضًا IsDefined(typeof(T)) حين لا تحتاج إلا إلى معرفة هل السمة موجودة.
كلفة الانعكاس
يقايض الانعكاس السرعة والأمان بالمرونة:
- السرعة. البحث عن عضو بالاسم واستدعاؤه عبر
InvokeأوGetValueأبطأ بكثير من الاستدعاء المباشر، ويغلّف أنواع القيم. للاستخدام المتكرر ابحث عنPropertyInfoأوMethodInfoمرة واحدة واحتفظ به، أو حوّله إلى مفوّض بـDelegate.CreateDelegateأوMethodInfo.CreateDelegateواستدعِ ذلك. - الأمان. الاسم المكتوب خطأ أو التوقيع المتغيّر يُترجم بلا مشكلة ويفشل وقت التشغيل. فضّل
nameof(Product.Price)على النص"Price"حيثما استطعت، كي تُلتقط إعادة التسمية. - التقليم وAOT. تزيل التطبيقات المقلّمة وتطبيقات Native AOT الأعضاء التي لا يبدو أن شيئًا يستخدمها، والانعكاس يخفي الاستخدام عن ذلك التحليل. والمكتبات الحديثة (
System.Text.JsonوGeneratedRegexوالتسجيل) تنتقل إلى مولّدات المصدر، التي تؤدي العمل نفسه وقت الترجمة.
استخدم الانعكاس لأجزاء البرنامج التي لا تعرف أنواعها مسبقًا حقًا: الإضافات، والأدوات العامة، وأدوات التسلسل، ومساعدات الاختبار. وحين تكون الأنواع معروفة تكون الشيفرة العادية أو الأنواع العامة أو الواجهات أسرع ويفحصها المترجم.
أخطاء شائعة
GetType() == typeof(Base)لاختبار صنف أساس. يفشل مع الأنواع المشتقة. استخدمisأوIsAssignableFrom.- عدم فحص
null. تعيدGetPropertyوGetMethodوType.GetTypeالقيمةnullحين لا يطابق شيء. - الانعكاس في حلقة ساخنة دون تخزين مؤقت. خزّن
MemberInfoأو ترجم مفوّضًا. - التقاط الاستثناء الخطأ من
Invoke. الاستثناء الحقيقي هوInnerExceptionفيTargetInvocationException. - نصوص سحرية لأسماء الأعضاء. استخدم
nameof.
الأسئلة الشائعة
ما هو الانعكاس في C#؟
الانعكاس (reflection) قدرة البرنامج على فحص الأنواع وقت التشغيل: سرد خصائص صنف ودواله، وقراءة القيم وضبطها بالاسم، واستدعاء الدوال، وإنشاء المثيلات، وقراءة السمات. يعيش في System.Reflection ويبدأ من كائن Type. أدوات التسلسل وأدوات ORM وحاويات حقن الاعتماديات وأطر الاختبار مبنية عليه.
ما الفرق بين typeof وGetType في C#؟
تُحسم typeof(Customer) وقت الترجمة من اسم نوع ولا تحتاج إلى كائن. أما obj.GetType() فتُستدعى على مثيل وقت التشغيل وتعيد النوع الفعلي للكائن، الذي قد يكون أكثر اشتقاقًا من النوع المصرّح للمتغيّر: في Animal a = new Dog(); تكون a.GetType() هي Dog. واستدعاء GetType() على مرجع null يرمي NullReferenceException.
كيف أحصل على قيمة خاصية بالاسم في C#؟
تعيد obj.GetType().GetProperty("Price") كائن PropertyInfo (أو null إن لم توجد خاصية عامة بهذا الاسم)، وتقرؤها .GetValue(obj) كـ object. وتكتبها .SetValue(obj, value). خزّن PropertyInfo مؤقتًا إن كنت تفعل ذلك في حلقة، لأن البحث هو الجزء المكلف.
كيف أنشئ سمة مخصصة في C#؟
صرّح بصنف مشتق من System.Attribute، وسمّه باللاحقة Attribute، وحدّد أين يمكن استخدامه بـ [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. طبّقها بالشكل [MaxLength(50)] واقرأها بـ property.GetCustomAttribute<MaxLengthAttribute>().
ماذا تفعل السمة Obsolete في C#؟
تجعل [Obsolete("Use PlaceOrderAsync instead")] على عضو المترجمَ يصدر التحذير CS0618، مع رسالتك، في كل موضع استدعاء. وتحوّل [Obsolete("...", true)] التحذير إلى الخطأ CS0619. هكذا تتقاعد المكتبات واجهة API دون كسر المستدعين بين ليلة وضحاها.
هل الانعكاس بطيء في C#؟
مقارنة بالاستدعاء المباشر، نعم: إيجاد عضو بالاسم واستدعاؤه عبر MethodInfo.Invoke أو PropertyInfo.GetValue أبطأ عادة بعشرات إلى مئات المرات، ويغلّف أنواع القيم. وهو مقبول عند البدء وللإعدادات والاستخدام العرضي. أما للمسارات الساخنة فخزّن MemberInfo مؤقتًا، أو ابنِ مفوّضًا مرة واحدة، أو استخدم الأنواع العامة أو مولّد مصدر بدلًا منه.