تعبير lambda دالة مجهولة صغيرة تُكتب في مكانها بالمعامل => ("يذهب إلى"). المعاملات قبل السهم، والنتيجة بعده:
المخرجات:
60.0
12
ORDER SHIPPED!
96.0, 180.0
ليس لتعبير lambda نوع خاص به. يصبح مفوّضًا (delegate، كائن يشير إلى دالة) من أي نوع مفوّض يتوقعه السياق: Func<decimal, decimal> هنا، وPredicate<T> لـ List.FindAll، وEventHandler لحدث. مفوّضات Func تعيد قيمة ومفوّضات Action لا تعيد؛ وتسرد صفحة Func وAction العائلة كاملة.
أشكال الصياغة
x => x * x // one parameter: parentheses optional
(x, y) => x + y // several parameters
() => DateTime.Now.Year // no parameters
(int x, string s) => s.Length > x // explicit parameter types
order => // statement lambda: a block with return
{
decimal subtotal = order.Qty * order.Price;
return subtotal > 100m ? subtotal * 0.9m : subtotal;
}
lambda التعبير فيها تعبير واحد بعد =>، وقيمته هي النتيجة. أما lambda العبارات ففيها كتلة بين أقواس معقوصة، يمكن أن تحتوي على أي عبارات ويجب أن تعيد قيمة بـ return في كل مسار إن كان المفوّض يعيد قيمة (وإلا CS1643، Not all code paths return a value in lambda expression).
أضافت الإصدارات الأحدث أشكالًا أخرى، كلها اختيارية:
- C# 9: معاملات المُهمَل، فيمكن أن تكون عدة معاملات غير مستخدمة كلها
_((_, _) => Save())، وتعابير lambda من نوعstaticممنوعة من التقاط المتغيّرات. - C# 10: "نوع طبيعي"، فتستنتج
var square = (int x) => x * x;النوعFunc<int, int>. قبل ذلك كانتvar f = x => x * 2;تفشل؛ وحتى في C# 10 ما زالت تفشل دون نوع للمعامل، بالخطأ CS8917The delegate type could not be inferred. - C# 12: قيم افتراضية للمعاملات،
(int x, int y = 1) => x + y.
تمرير تعابير lambda إلى الدوال
أي دالة تأخذ معامل مفوّض تقبل تعبير lambda. هكذا تمرّر سلوكًا لا مجرد بيانات:
المخرجات:
report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf
لا تعرف Filter معنى "الإبقاء"؛ المستدعي يقرّر بتعبير lambda. وتعمل List.Sort بالطريقة نفسها: تعبير lambda مقارنة تعيد سالبًا أو صفرًا أو موجبًا، وتستدعيه Sort كلما احتاجت.
تعابير lambda في LINQ
LINQ هو المكان الذي تلتقي فيه معظم شيفرات C# بتعابير lambda. كل معامل استعلام يأخذ واحدًا: Where شرطًا، وSelect إسقاطًا، وOrderBy مفتاحًا، وSum قيمة للجمع:
المخرجات:
Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45
لا تُنفَّذ تعابير lambda حيث تُكتب. يخزّنها LINQ ويستدعيها حين يُعدَّد الاستعلام بـ foreach أو ToList() أو Sum()، وهذا ما يُسمّى التنفيذ المؤجّل. تغطي صفحة LINQ المعاملات نفسها.
الإغلاقات: المتغيّرات الملتقطة
يمكن لتعبير lambda استخدام المتغيّرات المحلية والمعاملات للدالة التي كُتب فيها. يلتقط المتغيّر لا قيمته الحالية، ويعيش المتغيّر ما دام تعبير lambda موجودًا:
المخرجات:
1
2
3
1
50.0
كل استدعاء لـ MakeCounter ينشئ count جديدًا، فيكون العدّادان مستقلين. وقرأ تعبير lambda للخصم rate حين نُفّذ، لا حين كُتب، فطبّق 50%. ينفّذ المترجم ذلك بنقل المتغيّرات الملتقطة إلى صنف مخفي تتشاركه الدالة وتعبير lambda.
فخ الالتقاط في الحلقات
التقاط متغيّر حلقة هو حيث تلدغ الإغلاقات. لحلقة for متغيّر واحد للحلقة كلها، فيتشارك ذلك المتغيّر كل تعبير lambda يُنشأ فيها:
المخرجات:
3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach
نُفّذت تعابير lambda الثلاثة في الحلقة الأولى بعد انتهاء الحلقة، حين كانت i تساوي 3. نسخ i إلى متغيّر مصرّح به داخل الجسم يمنح كل تعبير lambda متغيّره الخاص. أما foreach فتفعل الصواب وحدها: منذ C# 5 يكون متغيّر التكرار فيها متغيّرًا جديدًا في كل مرور. وقبل C# 5 كان فيها الخطأ نفسه، ولهذا ما زالت إجابات قديمة على الإنترنت توصي بالنسخ لـ foreach أيضًا.
الفخ نفسه ينطبق على كل ما يُنفَّذ لاحقًا: معالجات الأحداث، والمؤقتات، وTask.Run، واستعلامات LINQ المبنية في حلقة والمعدَّدة بعدها.
تعابير lambda والدوال المجهولة والدوال المحلية
حلّت تعابير lambda محل صياغة الدوال المجهولة في C# 2، التي ما زالت تُترجم:
Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; }; // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0; // lambda
ولدالة مساعدة تُستخدم داخل دالة واحدة فقط تقدّم C# 7.0 أيضًا الدوال المحلية، التي لها اسم ويمكن أن تستدعي نفسها ولا تحجز مفوّضًا ما لم تحوّلها إلى واحد. استخدم تعبير lambda حين تمرّر سلوكًا إلى شيء ما (LINQ أو Sort أو حدث)؛ واستخدم دالة محلية أو دالة خاصة حين تستدعيها بنفسك.
يظهر تمييز آخر مع مزوّدي LINQ مثل Entity Framework: تعبير lambda المُسند إلى Expression<Func<T, bool>> لا يُترجم إلى شيفرة إطلاقًا بل إلى بنية بيانات تصف الشيفرة، يترجمها المزوّد إلى SQL. الصياغة متطابقة؛ ونوع معامل الدالة التي تستدعيها هو ما يحدد أيهما تحصل عليه.
الأسئلة الشائعة
ما هو تعبير lambda في C#؟
دالة مجهولة تُكتب بالمعامل =>: تأخذ x => x * 2 القيمة x وتعيد x * 2. ليس لتعبير lambda اسم خاص به؛ يُحوَّل إلى نوع مفوّض مثل Func<int, int> أو Action<string> ويمكن تخزينه في متغيّر أو تمريره إلى دالة أو إعادته منها.
كيف أكتب lambda بعدة معاملات أو بلا معاملات؟
ضع المعاملات بين أقواس: (a, b) => a + b لاثنين، و() => DateTime.Now لغياب المعاملات. لا يمكن حذف الأقواس إلا لمعامل واحد (x => x + 1). ويمكنك أيضًا ذكر الأنواع صراحة: (int a, int b) => a + b.
ما هو الإغلاق في lambda في C#؟
تعبير lambda الذي يستخدم متغيّرًا محليًا من الدالة المحيطة به يلتقط ذلك المتغيّر، لا لقطة من قيمته. يعيش المتغيّر ما دام تعبير lambda موجودًا، وإن غيّره أي طرف لاحقًا يرى الطرف الآخر التغيير. هذا ما يجعل العدّادات ودوال الاستدعاء الراجع تعمل، وما يسبّب خطأ الالتقاط التقليدي في حلقة for.
لماذا ترى كل تعابير lambda في حلقة for القيمة نفسها؟
لحلقة for متغيّر حلقة واحد للحلقة كلها، فيلتقط كل تعبير lambda يُنشأ داخلها i نفسه ويرى قيمته الأخيرة حين يُنفَّذ لاحقًا. انسخه إلى متغيّر محلي داخل الجسم (int copy = i;) والتقط النسخة. لا تعاني foreach من هذه المشكلة منذ C# 5، لأنها تنشئ متغيّرًا جديدًا في كل تكرار.
لماذا لا تُترجم var f = x => x * 2؟
لا يستطيع المترجم معرفة نوع المفوّض الذي تريده ولا نوع x، فيبلّغ بـ CS8917 The delegate type could not be inferred (قبل C# 10، CS0815). صرّح بالنوع (Func<int, int> f = x => x * 2;) أو، في C# 10 وما بعده، حدّد نوع المعامل: var f = (int x) => x * 2;.