Menu

Lambda ב-C#: תחביר, Func ו-Action, closures ו-LINQ

איך עובדים ביטויי lambda ב-C#: התחביר של => על כל צורותיו, שמירת lambdas ב-Func וב-Action, העברה שלהן למתודות ול-LINQ, statement lambdas, closures על משתנים שנלכדו, והבאג של לכידה בלולאת for שאין ב-foreach.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

ביטוי lambda הוא פונקציה אנונימית קטנה שנכתבת בתוך הקוד עם האופרטור => ("הולך אל"). הפרמטרים נכתבים לפניו, והתוצאה אחריו:

פלט:

60.0
12
ORDER SHIPPED!
96.0, 180.0

ל-lambda אין טיפוס משלה. היא הופכת ל-delegate (אובייקט שמצביע על פונקציה) מכל טיפוס delegate שההקשר מצפה לו: כאן Func<decimal, decimal>, Predicate<T> עבור List.FindAll, EventHandler עבור אירוע. delegates מסוג Func מחזירים ערך ו-delegates מסוג 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;
}

ל-expression lambda יש ביטוי אחד אחרי =>, והערך שלו הוא התוצאה. ל-statement lambda יש בלוק בסוגריים מסולסלים, שיכול להכיל כל פקודה, וחייב לבצע return של ערך בכל מסלול אם ה-delegate מחזיר ערך (אחרת CS1643, Not all code paths return a value in lambda expression).

גרסאות חדשות יותר הוסיפו עוד צורות, כולן אופציונליות:

  • C# 9: פרמטרי discard, כך שכמה פרמטרים שלא בשימוש יכולים כולם להיות _ ((_, _) => Save()), ו-lambdas מסוג static שאסור להן ללכוד משתנים.
  • C# 10: "טיפוס טבעי", כך ש-var square = (int x) => x * x; מסיק Func<int, int>. לפני כן, var f = x => x * 2; נכשל; גם ב-C# 10 הוא עדיין נכשל בלי טיפוס לפרמטר, עם CS8917 The delegate type could not be inferred.
  • C# 12: ערכי ברירת מחדל לפרמטרים, (int x, int y = 1) => x + y.

העברת lambdas למתודות

כל מתודה שמקבלת פרמטר delegate מקבלת lambda. כך מעבירים התנהגות ולא רק נתונים:

פלט:

report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf

Filter לא יודעת מה המשמעות של "keep"; מי שקורא מחליט עם lambda. List.Sort עובדת באותה צורה: ה-lambda היא השוואה שמחזירה ערך שלילי, אפס או חיובי, ו-Sort קוראת לה כמה פעמים שהיא צריכה.

Lambdas ב-LINQ

LINQ הוא המקום שבו רוב קוד C# פוגש lambdas. כל אופרטור שאילתה מקבל אחת: Where תנאי, Select הטלה, OrderBy מפתח, Sum ערך לסכום:

פלט:

Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45

ה-lambdas לא רצות במקום שבו הן נכתבו. LINQ שומר אותן וקורא להן כשעוברים על השאילתה עם foreach, ToList() או Sum(), מה שנקרא ביצוע נדחה (deferred execution). העמוד על LINQ מכסה את האופרטורים עצמם.

Closures: משתנים שנלכדו

lambda יכולה להשתמש במשתנים מקומיים ובפרמטרים של המתודה שבה היא נכתבה. היא לוכדת את המשתנה ולא את הערך הנוכחי שלו, והמשתנה ממשיך לחיות כל עוד ה-lambda חיה:

פלט:

1
2
3
1
50.0

כל קריאה ל-MakeCounter יוצרת count חדש, ולכן שני המונים בלתי תלויים. ה-lambda של ההנחה קראה את rate כשהיא רצה, לא כשהיא נכתבה, ולכן היא החילה 50%. הקומפיילר מממש את זה כשהוא מעביר משתנים שנלכדו למחלקה נסתרת שהמתודה וה-lambda חולקות.

המלכודת של לכידה בלולאה

לכידה של משתנה לולאה היא המקום שבו closures נושכים. ללולאת for יש משתנה אחד לכל הלולאה, ולכן כל lambda שנוצרת בה חולקת את המשתנה הזה:

פלט:

3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach

כל שלוש ה-lambdas בלולאה הראשונה רצו אחרי שהלולאה הסתיימה, כש-i היה 3. העתקה של i למשתנה שמוצהר בתוך הגוף נותנת לכל lambda משתנה משלה. foreach עושה את זה נכון בעצמה: מאז C# 5, משתנה האיטרציה שלה הוא משתנה חדש בכל סיבוב. לפני C# 5 היה בה אותו באג, ולכן תשובות ישנות ברשת עדיין ממליצות על העותק גם עבור foreach.

אותה מלכודת חלה על כל דבר שרץ מאוחר יותר: event handlers, טיימרים, Task.Run, ושאילתות LINQ שנבנות בלולאה ועוברים עליהן אחריה.

Lambdas, מתודות אנונימיות ופונקציות מקומיות

Lambdas החליפו את התחביר של מתודות אנונימיות מ-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 מציעה גם פונקציות מקומיות, שיש להן שם, שיכולות להיות רקורסיביות, ושלא מקצות delegate אלא אם ממירים אותן לכזה. השתמשו ב-lambda כשמעבירים התנהגות למשהו (LINQ, Sort, אירוע); השתמשו בפונקציה מקומית או במתודה פרטית כשאתם קוראים לה בעצמכם.

הבחנה נוספת מופיעה עם ספקי LINQ כמו Entity Framework: lambda שמושמת ל-Expression<Func<T, bool>> לא מתקמפלת לקוד בכלל, אלא למבנה נתונים שמתאר את הקוד, והספק מתרגם אותו ל-SQL. התחביר זהה; טיפוס הפרמטר של המתודה שקוראים לה קובע מה מקבלים.

שאלות נפוצות

מה זה ביטוי lambda ב-C#?

פונקציה אנונימית שנכתבת עם האופרטור =>: x => x * 2 מקבלת x ומחזירה x * 2. ל-lambda אין שם משלה; היא מומרת לטיפוס delegate כמו Func<int, int> או Action<string>, ואפשר לשמור אותה במשתנה, להעביר אותה למתודה או להחזיר אותה ממתודה.

איך כותבים lambda עם כמה פרמטרים או בלי פרמטרים?

שימו את הפרמטרים בסוגריים: (a, b) => a + b לשניים, () => DateTime.Now לאף אחד. רק פרמטר יחיד יכול לוותר על הסוגריים (x => x + 1). אפשר גם לציין את הטיפוסים במפורש: (int a, int b) => a + b.

מה זה closure ב-lambda של C#?

lambda שמשתמשת במשתנה מקומי מהמתודה שסביבה לוכדת את המשתנה הזה, ולא תמונת מצב של הערך שלו. המשתנה חי כל עוד ה-lambda חיה, ואם אחד הצדדים משנה אותו מאוחר יותר, הצד השני רואה את השינוי. זה מה שגורם למונים ול-callbacks לעבוד, וזה מה שגורם לבאג הלכידה הקלאסי בלולאת for.

למה כל ה-lambdas בלולאת for רואות את אותו ערך?

ללולאת for יש משתנה לולאה אחד לכל הלולאה, ולכן כל lambda שנוצרת בתוכה לוכדת את אותו i ורואה את הערך הסופי שלו כשהיא רצה מאוחר יותר. העתיקו אותו למשתנה מקומי בתוך הגוף (int copy = i;) ולכדו את העותק. ל-foreach אין את הבעיה הזו מאז C# 5, כי היא יוצרת משתנה חדש בכל איטרציה.

למה var f = x => x * 2 לא מתקמפל?

הקומפיילר לא יכול לדעת איזה טיפוס delegate אתם רוצים או מה הטיפוס של 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;.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל