קיים ממשק שנקרא IEnumerable. לממשק זה פונקציה אחת:
1
2
3
4
publicinterfaceIEnumerable
{
IEnumerator GetEnumerator();
}
שמחזירה ממשק אחר שנקרא IEnumerator:
1
2
3
4
5
6
7
8
9
10
11
publicinterfaceIEnumerator
{
boolMoveNext();
object Current
{
get;
}
voidReset();
}
הממשק הזה מאפשר לנו לעשות 3 פעולות:
להתקדם אחד קדימה (ולקבל תשובה אם הצלחנו להתקדם אחד קדימה)
לראות מה האיבר הנוכחי
לחזור להתחלה
החליטו שכל מי שרוצה שיוכלו לעשות foreach עליו, צריך לממש IEnumerable.
איך זה עובד?
כאשר אנחנו כותבים קוד מהסוג הבא:
1
2
3
4
foreach (Person person in people)
{
// Do stuff
}
מה שקורה באמת זה משהו כזה:
1
2
3
4
5
6
7
IEnumerator enumerator = people.GetEnumerator();
while (enumerator.MoveNext())
{
Person person = (Person) enumerator.Current;
// Do stuff
}
כך שבעצם foreach זה כולה syntactic sugar 😃
עכשיו, אפשר להסביר כל מיני דברים, למשל:
איך הוא יודע שהCollection שלי השתנה באמצע הforeach?
ובכן, למשל בList<T>, הפונקציה GetEnumerator, מחזירה איזשהו IEnumerator שמחזיק reference לList. בנוסף יש לשניהם Member שנקרא Version, שכל פעם שמתבצעת פעולה של שינוי על הרשימה (הוספה/הסרה/עדכון וכו’), הוא גדל ב1.
ואז בכל MoveNext, הEnumerator בודק ששני הVersionים שווים, ובמידה ולא, זורק Exception.
Person admin = facebook.GetDepartment("Administration").People[0];
admin.Name = "Mark";
admin.LastName = "Zuckerberg";
ולמרחיקי הלכת!:
1
2
3
4
5
6
7
8
9
Organization facebook;
Department administartion =
facebook.GetDepartment("Administration");
Person admin = administartion.People[0];
admin.Name = "Mark";
admin.LastName = "Zuckerberg";
מהשיקולים הבאים:
במקרה הטוב, לגישה לשני הMemberים הראשונים אין side effects, וכל מה שקורה כאן זה שנכתב יותר IL, מה שגם משפיע על הביצועים, אבל בצורה זניחה. זה אמנם לא נראה כך, אבל בשיטה הראשונה שהצגתי, כתבנו הרבה יותר קוד, שהרי התבצע אותו קוד מספר פעמים! שתי הכתיבות שהצגתי יוצרות פחות IL.
במקרה הפחות טוב, לגישה לGetDepartment , לPeople ולאינדקס במקום אפס, יש side effects, ולכן קורות תופעות לוואי מספר פעמים.
נוצר לנו קוד שיותר קל לתחזק אותו, ברור יותר ששתי השורות האלה הן אחת עם השנייה. הדבר גם עוזר לקריאות בד"כ.
הדבר הכי חשוב, לדעתי, הוא שהדבר הזה מאפשר לנו לדבג יותר בקלות את הקוד, עם ביטויים פחות ענקיים בWatch, או בConditional Breakpoint וכו’. לא יודע כמה מכם נתקלו בתופעה שConditional Breakpoint מסובך גורם לVisual Studio לבטל אותו.
בכל אופן כדאי להשתמש בזה איפה שמתאים.
לדוגמה, אם היינו אולי משנים רק את השם הפרטי בדוגמה זו, אולי היה עדיף שנשאיר את זה בשורה אחת.
באופן כללי בבואכם לכתוב delegateים משלכם חשבו על הנקודות הבאות:
האם קיים delegate כזה כבר (לרבות Func וAction)
אם לא, אולי אני עושה משהו לא בסדר (אולי אני מקבל יותר מדי פרמטרים, אולי זה לא נכון שאני מקבל פרמטרים מסוג params/ref/out)
במידה ועברתם את שלב 2, חשבו אם אפשר להשתמש בGenerics כדי לאפשר Reuse לdelegate שלכם ולא להשתמש בו באופן חד פעמי
זכרו שקיימים covariance וcontravariance ברמת השפה בין delegateים וmethod groups, כך שdelegate פחות/יותר ספציפי יכול לשמש יותר method groups ממה שנראה לעין.
ראינו בפעמים הקודמות מה זה Anonymous delegates ואיך משתמשים בהם.
הבעיה העיקרית שאנחנו נתקלים בה היא שכתיבה של Anonymous delegate היא קצת זוועתית, למשל אפילו בדוגמה הכי פשוטה שראינו ביום ראשון, היינו צריכים לכתוב משהו כזה:
1
2
3
Console.WriteLine(First(numbers,
delegate(int number)
{ return (number % 2 == 0); })); // Prints 15
או כזה:
1
2
3
4
5
Predicate<int> isOdd =
delegate(int number)
{ return (number%2 == 0); };
Console.WriteLine(First(numbers, isOdd)); // Prints 15
בC# 3.0 הגיעו למסקנה שזה פוגע בקריאות וגורם לאנשים לסלוד משימוש בAnonymous delegates.
לכן באו והציגו את הפתרון באמצעות Lambda Expressions.
מה זה Lambda Expression? מדובר על גישה כללית שמגיעה מתחום הלוגיקה המתמטית שנקראת תחשיב למבדא.
אחת המטרות היא לצמצם את כתיבת הגדרת פונקציות בצורה פורמלית.
בC# זה בא לידי ביטוי כך:
נוכל לכתוב במקום
1
2
3
Predicate<int> isOdd =
delegate(int number)
{ return (number%2 == 0); };
כך:
1
2
3
Predicate<int> isOdd =
(int number) =>
{ return (number%2 == 0); };
אלא שאפשר עדיין להתווכח ולומר שהביטוי לא קריא.
גילו שמסתבר שככל שמציינים פחות, הביטוי נהיה יותר קריא. למשל מאחר והחתימה של הdelegate נראית כך:
1
publicdelegatebool Predicate<T>(T item);
אין צורך שנציין את הסוג של הארגומנט בשני הצדדים, אלא הקומפיילר יגלה את זה implicity בזמן בקימפול.
אז הביטוי נהפך להיות כזה:
1
2
3
Predicate<int> isOdd =
(number) =>
{ return (number%2 == 0); };
בנוסף, בגלל שמדובר בפרמטר אחד, מותר לנו להשמיט את הסוגריים סביבו:
1
2
3
Predicate<int> isOdd =
number =>
{ return (number%2 == 0); };
לבסוף, בגלל שיש לנו שורת קוד אחת, והיא של return, נוכל גם להסיר את הסוגריים ואת הreturn:
1
2
Predicate<int> isOdd =
number => (number%2 == 0);
הביטוי נהיה מאוד פשוט והרבה יותר קריא!
קצת על כתיבה של Lambda expressions:
בד"כ לא נצטרך לציין את סוגי הפרמטרים, אלא רק לתת להם שמות. אם מדובר על יותר מפרמטר אחד, נצטרך לשים סביב הפרמטרים סוגריים:
למשל אם יש לנו את הdelegate הזה:
1
publicdelegateintAddDelegate(int first, int second);
נוכל להציב לתוכו את הביטוי הבא:
1
AddDelegate add = (x, y) => x + y;
אם מדובר על delegate שלא מקבל פרמטרים, גם נצטרך לשים סוגריים:
1
2
3
publicdelegatestringStringDelegate();
StringDelegate stringDelegate =
() => DateTime.Now.ToString();
שימו לב לסוגריים הריקים.
גם אם יש לנו חתימה שלא מחזירה ערך נוכל להשתמש בצורת כתיבה של שורה אחת:
1
2
3
4
publicdelegatevoidPrintDelegate(string output);
PrintDelegate printDelegate =
x => Console.WriteLine(x);
לבסוף, אם יש לנו מימוש של יותר משורה אחת, עדיין נוכל להשתמש בLambda Expression, אבל עם סוגריים מסולסלים:
1
2
3
4
5
6
AddDelegate add =
(x, y) =>
{
double z = Math.Sqrt(x*x + y*y);
return Convert.ToInt32(z);
};
שימו לב שזה עדיין יותר קריא מהכתיב של anonymous methods.
הרבה אנשים עשויים לחשוב (ולעתים, בצדק) שמדובר בסה”כ בקיצור מרושל לכך שלא היה למתכנת כוח לכתוב מתודה שתעשה בשבילו את העבודה.
אני אדגים עכשיו משהו שקל לעשות באמצעות anonymous delegates, ויותר מסובך לעשות בלי.
נניח שאנחנו רוצים לכתוב פונקציה שתדפיס את כל המחרוזות הבינאריות בגודל מסוים. זו משימה שנראית מאוד פשוטה, והנה קוד שרושם את כל המחרוזות הבינאריות בגודל 5:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
publicstatic IEnumerable<string> BinaryStrings()
{
List<string> result =
new List<string>();
for (int i5 = 0; i5 <= 1; i5++)
{
for (int i4 = 0; i4 <= 1; i4++)
{
for (int i3 = 0; i3 <= 1; i3++)
{
for (int i2 = 0; i2 <= 1; i2++)
{
for (int i1 = 0; i1 <= 1; i1++)
{
result.Add(string.Format("{0}{1}{2}{3}{4}",
i1,
i2,
i3,
i4,
i5));
}
}
}
}
}
return result;
}
כפי שאנחנו רואים, פשוט יש לנו 5 לולאות מקוננות אחת בתוך השנייה ואנחנו מדפיסים בכל פעם את הערכים של כולם משורשרים.
אז מה הבעיה?
נניח שאנחנו רוצים בצורה זו להדפיס את כל המחרוזות הבינאריות לפי גודל של פרמטר שקיבלנו.
נצטרך ליצור מספר לולאות מקוננות בגודל המספר שקיבלנו.
בעייתי קצת, לא?
שתי אופציות קלאסיות לפתור את הבעיה שאני מעלה עכשיו, הן הבאות. הראשונה היא להשתמש בייצוג הבינארי של המספר ואז לרוץ עד $ 2^n $. (פתרון איטרטיבי)