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

دوال التوسيع في C#: الصياغة وIEnumerable وLINQ والقواعد

كيف تعمل دوال التوسيع في C#: دالة ساكنة في صنف ساكن مع this على المعامل الأول، واستدعاؤها كدالة مثيل، وتوسيع string والتعدادات وIEnumerable<T>، وكيف بُني LINQ منها، ولماذا تفوز دوال المثيل، والمستقبِل null، وموجّهة using التي تحتاجها.

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

تضيف دالة التوسيع (extension method) دالة إلى نوع موجود دون تعديله أو الوراثة منه أو تغليفه. تكتب دالة ساكنة عادية، وتعلّم معاملها الأول بـ this، ويستخدمها المستدعون بصياغة النقطة كأن النوع كان يملكها دائمًا:

المخرجات:

Extension methods let yo...
12
short
Extension...

تُترجم title.Truncate(24) إلى StringExtensions.Truncate(title, 24) بالضبط. صياغة النقطة مجرد تسهيل: ما زالت الدالة ساكنة، وما زالت تعيش في StringExtensions، ولا تستطيع استخدام إلا الأعضاء العامة في string.

قواعد التصريح بها

  • يجب أن تكون الدالة static وأن تعيش في صنف static وغير عام وغير متداخل داخل صنف آخر. الصنف غير الساكن أو العام يعطي CS1106، Extension method must be defined in a non-generic static class؛ والمتداخل يعطي CS1109، Extension methods must be defined in a top level static class.
  • توضع this على المعامل الأول فقط، ونوع ذلك المعامل هو النوع الذي يُوسَّع. والمعاملات الأخرى عادية.
  • يمكن للدالة نفسها أن تكون عامة (this IEnumerable<T> source)، مع أن الصنف لا يمكنه ذلك.
  • لا ترى إلا ما تراه أي شيفرة خارجية: الأعضاء العامة (وداخل التجميع نفسه، الداخلية internal). وتبقى الحقول الخاصة خاصة.

العُرف أن يُسمّى الصنف بما يوسّعه (StringExtensions وEnumerableExtensions) وأن تُجمع في فضاء أسماء مثل MyApp.Extensions.

توسيع الواجهات وIEnumerable<T>

توسيع واجهة يضيف الدالة إلى كل نوع ينفّذها. لذا يمنح توسيع IEnumerable<T> الدالة للمصفوفات والقوائم والمجموعات ومجموعات مفاتيح القواميس ونتائج استعلامات LINQ دفعة واحدة:

المخرجات:

9.5
50
65
f0 f3 f6

EveryNth عامة: يُستنتج T من المستقبِل، فتعمل مع النصوص هنا ومع أي نوع عناصر آخر. ولأنها تستخدم yield return فهي كسولة مثل معاملات LINQ المدمجة ويمكن سلسلتها معها.

LINQ مبني على دوال التوسيع

Where وSelect وOrderBy وSum وFirst وبقية LINQ دوال توسيع على IEnumerable<T>، معرّفة في الصنف الساكن System.Linq.Enumerable. لهذا يجب أن تكون using System.Linq; في أعلى الملف قبل أن تُترجم list.Where(...)، ولهذا يعمل LINQ على كل نوع مجموعة دون أن ينفّذ أي منها دالة Where:

المخرجات:

72, 88, 95
72, 88, 95

السطران الشيفرة نفسها. تحوّل صياغة التوسيع الاستدعاءات المتداخلة التي تُقرأ من الداخل إلى الخارج إلى سلسلة تُقرأ بترتيب حدوث الخطوات، وهذا السبب الرئيسي لوجود دوال التوسيع: أُضيفت في C# 3 مع LINQ. تغطي صفحة LINQ المعاملات.

يجب استيراد فضاء الأسماء

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

error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

أضف using لفضاء أسماء التوسيعات (أو، منذ C# 10، global using مرة واحدة للمشروع):

المخرجات:

EUR 49.90

والوجه الآخر مفيد: دالة التوسيع التي لا تريدها في كل مكان يمكن أن تعيش في فضاء أسماء لا تستورده إلا بعض الملفات.

دوال المثيل تفوز

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

المخرجات:

Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call

خسر التوسيع ذو الوسيط الواحد أمام دالة المثيل؛ واستُخدم ذو الوسيطين لأنه لا توجد دالة مثيل تأخذ نصًا. وهذا يعني أيضًا أن تحديث مكتبة يضيف دالة مثيل باسم توسيعك يحوّل استدعاءاتك بصمت إلى نسخة المكتبة. اختر أسماء توسيعات يُستبعد أن تتصادم.

الاستدعاء على null

لأن الاستدعاء في الحقيقة استدعاء ساكن، لا يرمي المستقبِل null استثناءً في موضع الاستدعاء. تتلقى الدالة null كوسيطها الأول وتقرّر ما يعنيه:

المخرجات:

True
AL
ArgumentNullException: fullName

إعادة missing.IsBlank() القيمة true مريحة، لكنها قد تفاجئ القرّاء الذين يتوقعون أن يرمي استدعاء عضو على null استثناءً. اقبل null فقط في الدوال التي يجعل اسمها ذلك بديهيًا (IsBlank وOrEmpty)؛ وفي غيرها ارمِ ArgumentNullException كما يفعل LINQ.

توسيع التعدادات

لا يمكن للتعدادات أن تملك دوالًا خاصة بها، وهذا يجعلها هدفًا طبيعيًا:

enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

static class OrderStatusExtensions
{
    public static bool IsFinal(this OrderStatus s) =>
        s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}

// usage: if (order.Status.IsFinal()) { ... }

أعضاء التوسيع في C# 14

حتى C# 14 لم يكن يمكن أن تكون إلا الدوال توسيعات. أضافت C# 14 كتل extension، التي تجمع أعضاء لمستقبِل واحد وتسمح بخصائص التوسيع والأعضاء الساكنة أيضًا:

// C# 14 and later
public static class StringExtensions
{
    extension(string s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // extension property
        public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
    }
}

تبقى دوال التوسيع التقليدية بمعامل this صالحة، وهي ما تستخدمه تقريبًا كل الشيفرات والمكتبات الموجودة.

متى تكتب واحدة

تناسب دوال التوسيع حين لا تملك النوع (string وDateTime وواجهات الإطار)، وحين تريد الدالة المساعدة نفسها على كل تنفيذ لواجهة، أو حين تُقرأ السلسلة أفضل من الاستدعاءات المتداخلة. وحين تملك الصنف أضف دالة مثيل حقيقية بدلًا من ذلك: يمكنها استخدام الحالة الخاصة وتظهر حيث يبحث عنها القرّاء. وتجنّب توسيع object، الذي يضع دالتك في قائمة الإكمال لكل قيمة في البرنامج.

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

ما هي دالة التوسيع في C#؟

دالة ساكنة يمكن استدعاؤها كأنها دالة مثيل لنوع آخر. تكتبها في صنف ساكن وتضع this قبل معاملها الأول: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. عندها تعمل name.IsBlank() على أي نص، دون تغيير string أو الوراثة منه.

لماذا لا يُعثر على دالة التوسيع عندي؟

دوال التوسيع لا تكون مرئية إلا حين يُستورد فضاء أسمائها. إن كان الصنف الساكن في MyApp.Extensions فأضف using MyApp.Extensions; إلى الملف الذي يستدعيها؛ وإلا يبلّغ المترجم بـ CS1061 قائلًا إن النوع لا يحتوي على تعريف ولا على دالة توسيع متاحة بهذا الاسم. وتحقّق أيضًا أن الصنف static وغير عام وغير متداخل.

هل يمكن لدالة توسيع أن تتجاوز دالة مثيل؟

لا. يبحث المترجم أولًا عن دالة مثيل قابلة للتطبيق ولا ينظر في دوال التوسيع إلا حين لا توجد. دالة التوسيع بالاسم والمعاملات نفسها لدالة مثيل تُتجاهل بصمت في كل موضع استدعاء يستخدم صياغة المثيل.

هل يمكن استدعاء دالة توسيع على null في C#؟

نعم. يُترجم الاستدعاء إلى استدعاء دالة ساكنة مع null كوسيط أول، فلا يُرمى NullReferenceException عند الاستدعاء نفسه. الدالة تقرّر ما تفعله: تعامل null كمدخل صالح (مثل دالة مساعدة بأسلوب IsNullOrEmpty) أو ترمي ArgumentNullException، كما تفعل دوال LINQ.

هل توجد خصائص توسيع في C#؟

ليس قبل C# 14. حتى ذلك الحين لا يمكن أن تكون إلا الدوال توسيعات، فتُكتب الخاصية المرغوبة كدالة (GetFullName()). أضافت C# 14 كتل التوسيع، التي يمكنها التصريح بخصائص توسيع وأعضاء ساكنة إضافة إلى الدوال.

Coddy programming languages illustration

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

ابدأ الآن