111. IEqualityComparer

הכרנו בעבר את Equals בשבוע השווה (טיפים 76-80),

לפעמים יש לנו מחלקה שכבר ממומש לה Equals, אבל אנחנו מעוניינים להשוות איברים בצורה אחרת.

לדוגמה, נניח שאנחנו עובדים עם מחרוזות. השוואה של מחרוזות היא Case-sensitive, אבל לפעמים אנחנו מעוניינים להשוות מחרוזות לא Case-Sensitive.

או דוגמה אחרת, כאשר אנחנו משווים שני מספרים, אנחנו מתייחסים אל הסימן שלהם. אולי נרצה לפעמים להשוות מספרים בלי להתייחס לסימן שלהם.

בשביל כל הבעיות האלה, הומצא ממשק ששמו הוא IEqualityComparer.

לממשק יש שתי פונקציות. אחת משווה בין שני אובייקטים, והשנייה מחזירה HashCode של אובייקט נתון.

כך אנחנו יכולים לדרוס את ההתנהגות ואת הצורה בה אנחנו מגדירים מתי שוויון מתקיים.

קיימת, כמובן, גרסה גנרית וגרסה לא גנרית. הגרסה הלא גנרית קיימת בעיקר מסיבות היסטוריות ומשווה שני objectים. הגרסה הגנרית היא מה שיותר מעניין אותנו.

לדוגמה, נניח ואנחנו מעוניינים להשוות שתי מחרוזות בשיטה החדשה, נצטרך לממש משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
public class StringIgnoreCaseSensitiveComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.InvariantCultureIgnoreCase);
}
public int GetHashCode(string obj)
{
return obj.ToUpper().GetHashCode();
}
}

ואז נוכל להשוות שתי מחרוזות כך:

1
2
3
4
5
6
StringIgnoreCaseSensitiveComparer stringComparer = new StringIgnoreCaseSensitiveComparer();
if (stringComparer.Equals("Hello World", "HELLO WORLD"))
{
// True
}

מה היתרון? כרגע לא נראה שיש בדיוק יתרון בעבודה כזו, אך נראה בהמשך שעבודה כזאת עשויה לפשט לנו תהליכים, מאחר והרבה מקומות בFramework יודעים לעבוד עם IEqualityComparer.

במטרה לחסוך לנו זמן, יש מספר מחלקות שאנחנו יכולים להשתמש בהם בבואנו לכתוב IEqualityComparer משלנו. אחת מהן היא EqualityComparer<T>, בה אנחנו רק צריכים לממש את Equals וGetHashCode בדרך שאנחנו רוצים להשוות איברים. ההבדל בין ירושה ממחלקה זו, למימוש IEqualityComparer<T> הוא EqualityComparer<T> מממש גם את הגרסה הלא גנרית של IEqualityComparer.

בנוסף, יש את המימוש הדיפולטי של EqualityComparer<T>, שראינו אותו בעבר (טיפ מספר 79):

1
2
3
4
5
6
IEqualityComparer<string> stringComparer = EqualityComparer<string>.Default;
if (stringComparer.Equals("Hello World", "HELLO WORLD"))
{
// False
}

הוא פשוט משווה שתי מחרוזות באמצעות הEquals וGetHashCode הדיפולטיים של הטיפוס.

אנחנו עוד נראה שימושים של IEqualityComparer<T>.

שבוע שווה

שתף

110. A lot of nullable operators

בהמשך לשבוע האולי טוב,

פגשנו בעבר (טיפ מספר 3) את האופרטור ?? המחזיר את הביטוי הראשון מבין השניים שאינו null

מסתבר שאפשר לשרשר את האופרטור הזה מספר פעמים וכך למצוא את הביטוי הראשון מבין רבים שאינו null:

1
Person leader = mother ?? jacob ?? jack ?? hurley;

זה מחזיר לנו את הראשון מבין mother ,jacob ,jack ,hurley שאינו null ועשוי לחסוך לנו קוד במקרים מסוימים. זה מתרגם כמובן למשהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Person leader;
if (mother != null)
{
leader = mother;
}
else
{
if (jacob != null)
{
leader = jacob;
}
else
{
if (jack != null)
{
leader = jack;
}
else
{
leader = hurley;
}
}
}

שיהיה סוף שבוע בטוח טוב

שתף

109. The maybe monad and nested null checking - Do extension method

בהמשך לשבוע האולי טוב,

לפעמים אנחנו מעוניינים לבצע איזשהי פעולה בין בגישה לProperty פנימי, במידה ואחד השדות בדרך הוא לאnull.

למשל,

1
2
3
4
5
6
7
8
9
10
11
if ((company != null) &&
(company.Chairman != null))
{
company.Chairman.Address =
GetAddressFromDB(company.Chairman);
if (company.Chairman.Address != null)
{
zipCode = company.Chairman.Address.ZipCode;
}
}

כאן אנחנו ממלאים את הכתובת של company.Chairman במידה והוא לא null, וממשיכים לנסות לגשת בסופו של דבר לZipCode.

נוכל לעשות את זה בגישה דומה למה שראינו בשאר השבוע בצורה הבאה:

נגדיר Extension Method כזה בשם Do לצורך העניין:

1
2
3
4
5
6
7
8
9
10
11
public static TSource Do<TSource>
(this TSource source,
Action<TSource> action)
where TSource : class
{
if (source != null)
{
action(source);
}
return source;
}

זה Extension Method שמקבל אובייקט וdelegate, ומבצע את הDelegate במידה והאובייקט אינו null.

כעת נוכל לכתוב את הקוד למעלה בצורה הבאה:

1
2
3
4
5
ZipCode zipCode =
company.With(x => x.Chairman)
.Do(x => x.Address = GetAddressFromDB(x))
.With(x => x.Address)
.With(x => x.ZipCode);

כמה הערות על סדרת הטיפים הזאת:

  • היא מבוססת על מאמר שקראתי בבלוג devtalk.net שנקרא בשם דומה (Chained null checks and the Maybe monad). ממליץ לקרוא למי שהתעניין בסדרה. יש שם גם דוגמה של כותב הבלוג מהעולם האמיתי.
  • אני לא אומר שמעכשיו תכתבו את כל הקוד שלכם בצורה הזאת, וגם לא אמרתי את זה בשום מקום. אחת המטרות של הטיפ היומי היא לחשוף את קוראיו לרעיונות מעניינים ולהרחיב אופקים. בקיצור, אני לא אומר אם להשתמש בכתיבה הזאת או לא. אם אתם רואים שכתיבה כזו יכולה להתאים לכם לפרויקט ולחסוך לכם עבודה, שווה לנסות.

המשך יום אולי טוב

שתף

108. The maybe monad and nested null checking - If extension method

בהמשך לשבוע האולי טוב,

לעתים אנחנו מעוניינים לבצע איזשהו ביטוי רק במידה ואיזשהו תנאי מתקיים, למשל:

1
ZipCode zipCode = company.Workers[0].Address.ZipCode;

מה הבעיה כאן? כרגיל הכל יכול להיות null, אבל בנוסף לכך ייתכן שבמערך Workers אין תוצאות.

השיטה הקלאסית לפתור בעיה כזו היא:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZipCode zipCode = null;
if (company != null)
{
if ((company.Workers != null) && (company.Workers.Length > 0))
{
Person worker = company.Workers[0];
if ((worker != null) && (worker.Address != null))
{
zipCode = worker.Address.ZipCode;
}
}
}

כמות הקוד שאנחנו מבצעים כאן רק כדי לגשת לProperty היא די מכובדת.

גם לבעיה זו יש פתרון בגישה הדומה לפתרונות של הבעיות הקודמות שראינו. ניצור Extension Method כזה:

1
2
3
4
5
6
7
8
9
10
11
12
public static TSource If<TSource>
(this TSource source,
Func<TSource, bool> predicate)
where TSource : class
{
if ((source != null) && predicate(source))
{
return source;
}
return null;
}

זה מחזיר לנו null אם הפרדיקט אינו מתקיים, אחרת מחזיר את האובייקט שקיבלנו לפונקציה.

כעת נוכל לכתוב את הקוד הקודם בגישה החדשה בצורה כזאת:

1
2
3
4
5
ZipCode zipCode = company.With(x => x.Workers)
.If(x => x.Length > 0)
.With(x => x[0])
.With(x => x.Address)
.With(x => x.ZipCode);

או לחלופין ככה:

1
2
3
4
ZipCode zipCode = company.If(x => (x.Workers != null) && (x.Workers.Length > 0))
.With(x => x.Workers[0])
.With(x => x.Address)
.With(x => x.ZipCode);

באופן דומה, נוכל ליצור Extension Method מקביל בשם Unless:

1
2
3
4
5
6
7
8
9
10
11
12
public static TSource Unless<TSource>
(this TSource source,
Func<TSource, bool> predicate)
where TSource : class
{
if ((source == null) || predicate(source))
{
return null;
}
return source;
}

שמחזירה את האובייקט שקיבלנו לפונקציה, רק אם הפרדיקט לא מתקיים, ואחרת null.

באמצעותה נוכל לכתוב את אותו הדבר בצורה הבאה:

1
2
3
4
5
ZipCode zipCode = company.With(x => x.Workers)
.Unless(x => x.Length == 0)
.With(x => x[0])
.With(x => x.Address)
.With(x => x.ZipCode);

המשך יום אולי טוב

שתף

107. The maybe monad and nested null checking - Return extension method

בהמשך לשבוע האולי טוב,

ראינו פעם שעברה שאפשר להשתמש בExtension method ששמו With בשביל לבצע גישות בטוחות למאפיינים של אובייקט.

מה אם אנחנו רוצים להחזיר ערך דיפולטי כאשר לא ניתן לגשת לערך המקורי?

מצד אחד נראה שנוכל פשוט לבדוק האם התוצאה שחזרה היא null או לא, אלא שהנחה זו אינה נכונה.

1
2
3
ZipCode zipCode = company.With(x => x.Chairman)
.With(x => x.Address)
.With(x => x.ZipCode);

הבעיה היא שהערך האחרון בהיררכיה יכול להיות באמת null, ואנחנו נהיה מעוניינים להחזיר null באמת, ולא ערך דיפולטי במקרה זה.

ניתן לפתור את הבעיה באמצעות Extension method אחר, שנקרא לו לצורך העניין Return:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static TResult Return<TSource, TResult>
(this TSource source,
Func<TSource, TResult> accessor,
TResult defaultValue)
where TSource : class
where TResult : class
{
if (source == null)
{
return defaultValue;
}
else
{
return accessor(source);
}
}

כעת נוכל להשתמש בה בשיטה הבאה:

1
2
3
4
ZipCode zipCode =
company.With(x => x.Chairman)
.With(x => x.Address)
.Return(x => x.ZipCode, new ZipCode(12345));

קריאה זו מחזירה לנו את הZipCode במידה וניתן לגשת אליו בדרך הסטנדרטית, ובמידה ולא, היא מחזירה לנו ZipCode דיפולטי (12345).

המשך יום אולי טוב

שתף

106. The maybe monad and nested null checking - With extension method

מדי פעם אנחנו נתקלים בבעיה הנפוצה הבאה:

יש לנו אובייקט שאנחנו מעוניינים לגשת לProperty של Property (של Property…) שלו, למשל:

1
ZipCode zipCode = company.Chairman.Address.ZipCode;

(ראינו משהו כזה גם בטיפ 48)

מה הבעיה?

הבעיה היא שיכול להיות שמשהו פה בדרך הוא null.

במקרה כזה, אנחנו בד"כ נתקלים בטיפול כזה:

1
2
3
4
5
6
if ((company != null) &&
(company.Chairman != null) &&
(company.Chairman.Address != null))
{
zipCode = company.Chairman.Address.ZipCode;
}

כמובן, זה נהיה מסובך ככל שהקינון גדול יותר (אם למשל יש לנו קינון מסדר יותר גדול), וככל שאנחנו מעוניינים לבצע לוגיקה יותר מסובכת.

לפעמים אנחנו גם צריכים לטפל במקרים שבהם אחד הדברים בשרשרת הוא null, ולהכניס ערך דיפולטי.

ראינו בעבר את הoperator שהתווסף בC# 2.0 ?? (טיפ מספר 3), אבל הוא פותר את הבעיה באופן חלקי. נוכל להשתמש בו רק בשביל לבדוק האם התוצאה האחרונה היא null ובמידה וכן, לתת ערך דיפולטי אחר.

נראה השבוע גישה מעניינת לפתרון בעיה זו.


אז איך אפשר לפתור את הבעיה?

מהכותרת אפשר לנחש שExtension Methods יכולים להתאים כאן.

למה בעצם הם יכולים להתאים כאן?

קריאה לExtension Method היא קריאה לפונקציה סטטית. בניגוד לInstance methods, קריאה של פונקציה סטטית על null לא בהכרח זורקת Exception!

בד"כ אנחנו בודקים האם הExtension Method נקרא על null, ובמידה וכן זורקים Exception, אך אין זה חייב להיות כך.

כיצד נוכל לנצל יתרון זה? נוכל לכתוב Extension Method כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static TResult With<TSource, TResult>
(this TSource source,
Func<TSource, TResult> accessor)
where TSource : class
where TResult : class
{
if (source == null)
{
return null;
}
else
{
return accessor(source);
}
}

זוהי מתודה שמקבלת אובייקט וDelegate שמקבל את האובייקט ומחזיר אובייקט אחר. מה שהיא עושה זה מחזירה null במידה והאובייקט הוא null, ואחרת מבצעת את הDelegate.

נוכל להשתמש בה בצורה הבאה:

במקום לכתוב את קטע הקוד שכתבנו מעלה, נוכל לכתוב משהו כזה:

1
2
3
ZipCode zipCode = company.With(x => x.Chairman)
.With(x => x.Address)
.With(x => x.ZipCode);

זה מגניב ועוזר לקריאות.

עם זאת זה לא מושלם. מה שהיינו מצפים, זה שיוסיפו לנו אופרטור דומה ל?? המאפשר לנו לעשות את זה בצורה שקופה, משהו כזה:

1
ZipCode zipCode = company.?Chairman.?Address.?ZipCode;

אולי בגרסאות הבאות של C#, יעשו לנו טובה ויוסיפו את זה. בינתיים נאלץ להסתפק בדרכים אחרות.

ראו גם Maybe monad

הערה: אכן בC# 6.0 נוסף Syntax כנ"ל.

שבוע אולי טוב

שתף

105. Concat and Union extension methods

לפעמים יש לנו שני אוספים, ואנחנו רוצים לבצע פעולה על כל האיברים שלהם.

לשם כך אפשר להשתמש בExtension Method שנקראת Concat:

היא מקבלת שני IEnumerableים ומחזירה IEnumerable חדש שהוא השרשור שלהם, כלומר מכיל את כל האיברים של הראשון, ואז את כל האיברים של השני

דוגמת קוד:

במקום לכתוב משהו כזה:

1
2
3
4
5
6
7
8
9
foreach (Person child in cohenChildren)
{
// Do something
}
foreach (Person child in leviChildren)
{
// Do something (copy code and do the same)
}

נוכל לכתוב כך:

1
2
3
4
5
6
7
IEnumerable<Person> children =
cohenChildren.Concat(leviChildren);
foreach (Person child in children)
{
// Do something
}

ניתן כמובן לנחש איך זה ממומש.

קיים בלבול נפוץ בין Concat לUnion – הרבה פעמים משתמשים בUnion כאשר בעצם מתכוונים להשתמש בConcat.

Union עושה משהו דומה, אבל שונה – Union מבצע איחוד של האוספים כקבוצות מתמטיות – כלומר יוצר לנו IEnumerable חדש שיש בו את כל האיברים שנמצאים בקבוצה הראשונה או בקבוצה השנייה (ה"או" הוא או מתמטי) – אבל בגלל שמדובר בקבוצה, אף איבר לא יופיע בו יותר מפעם אחת, למשל:

1
2
3
4
5
IEnumerable<int> theNumbers =
new[] {4, 8, 15, 16, 23, 42};
IEnumerable<int> evenNumbers =
new[] {2, 4, 6, 8, 10};

אם נשתמש בConcat נקבל:

1
2
3
4
5
6
7
8
IEnumerable<int> numbers =
theNumbers.Concat(evenNumbers);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// 4 8 15 16 23 42 2 4 6 8 10

אם נשתמש לעומת זאת בUnion נקבל:

1
2
3
4
5
6
7
8
IEnumerable<int> numbers =
theNumbers.Union(evenNumbers);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
// 4 8 15 16 23 42 2 6 10

איך Union משווה ששני איברים זהים? עם Equals, כמובן.


משהו קצת מעצבן הוא שאי אפשר לעשות Concat עם איבר יחיד בצורה חלקה. ניתן להשתמש בפתרון הבא:

1
2
IEnumerable<Person> allChildren =
children.Concat(new[] { anotherChild });

מה שחשוב לציין על כל הExtension Methods האלה, זה שהן משתמשות בyield return, ולכן הן כולן Lazy, כלומר כאשר נשתמש עליהן על IEnumerableים, החישוב לא יתבצע עד שנריץ את הMoveNext.

הן לא הורסות לנו את הLaziness שמספקות לנו המתודות הבסיסיות של LINQ (Select, Where, SelectMany וכו’)

שתף

104. SkipWhile extension method

בדומה לטיפ של אתמול, נניח שיש לנו Log של שגיאות, ומעניין אותנו להתחיל לסרוק את הLog החל מהשגיאה הראשונה.

בניגוד למקרה של אתמול, בו רצינו לחתוך את האוסף של השגיאות ברגע בו מתחילה השגיאה הראשונה, במקרה זה הקוד הקלאסי שאנחנו צריכים לכתוב הוא יותר מסובך:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool firstError = false;
foreach (LogInfo logInfo in log)
{
if (!firstError && logInfo.Error)
{
firstError = true;
}
if (firstError)
{
// Start working from this point
}
}

גם כאן יש לנו Extension Method שמאפשר לנו לבצע פעולה זו בדרך נחמדה יותר, לו קוראים SkipWhile. הוא מקבל פרדיקט וחותך את האוסף, כך שיתחיל מהאיבר הראשון שלא מקיים את הפרדיקט.

נוכל לכתוב במקום את הקודם מעלה, את הקוד הבא:

1
2
3
4
5
6
7
IEnumerable<LogInfo> afterFirstError =
log.SkipWhile(logInfo => !logInfo.Error);
foreach (LogInfo logInfo in afterFirstError)
{
// Start working from this point
}

SkipWhile כמובן עובדת בצורה דומה למה שכתבנו למעלה.

כמו בTakeWhile, ורוב הדברים בLINQ, גם כאן יש overload שמתחשב גם באינדקס של האיבר:

1
2
3
IEnumerable<LogInfo> afterFirstError =
log.SkipWhile((logInfo, index) =>
!((logInfo.Error) || (index > 100)));

חותך את 100 האיברים הראשונים, או החל מהשגיאה הראשונה.

הערה מעניינת:

הReSharper יודע לזהות לולאות של TakeWhile ולהציע להפוך אותן לשימוש בExtension Method במקום, אבל הוא לא יודע לעשות את זה עם לולאות SkipWhile.

(למרות שלא קשה לכתוב Highlighting Pattern כזה)

שתף

103. TakeWhile extension method

לפעמים יש לנו אוסף גדול של איברים, ואנחנו מעוניינים לסנן אותם כל עוד מתקיים איזשהו תנאי,

למשל, למצוא בLOG את הכתיבות הראשונות לפני שמחשב קרס, אבל לא את אלה שאחריהן, כי מדובר בגודל רב של מידע.

הדרך הקלאסית:

1
2
3
4
5
6
7
8
foreach (LogInfo logInfo in log)
{
if (logInfo.Error)
{
break;
}
// Analyze log info...
}

גם כאן LINQ מספקת לנו Extension Method שחוסך את העבודה הזאת, שמו TakeWhile.

מה הוא עושה? הוא מקבל פרדיקט, ומוצא לנו את האיברים הראשונים באוסף שמקיימים אותו, שם הוא עוצר.

דוגמה לשימוש:

1
2
IEnumerable<LogInfo> beforeError =
log.TakeWhile(info => !info.Error);

ואז נוכל לכתוב בכיף את הקוד הבא:

1
2
3
4
foreach (var logInfo in beforeError)
{
// Analyze log info...
}

למה זה טוב?

ראשית זה מאפשר לנו לבצע פעולות כאלה בצורה הצהרתית, בדומה לשאר הדברים בLINQ.

שנית, קוד בלי הkeywordים break, continue וgoto הוא קוד בריא יותר, ולכן עדיף 😃

כמו רוב הדברים שכבר ראינו, גם פה יש overload שמקבל גם index בשביל שנוכל להכניס אותו לשיקול בתנאי, למשל:

1
2
3
IEnumerable<LogInfo> beforeError =
log.TakeWhile((info, index) =>
(!info.Error) && (index <= 1000));

מוצא את אלף האיברים הראשונים לפני השגיאה.

יותר מזאת, אם מעניין אותנו לעשות רק תנאים על האינדקס, למשל להשיג רק את 1000 התוצאות הראשונות, נוכל להשתמש בExtension Method ששמו Take, שעושה את זה, במקום להעביר כל פעם delegate שעושה רק את התנאי הזה על האינדקס.

שתף

102. Aggregate extension method

אחת המתודות הפחות מוכרות מSystem.Linq היא Aggregate.

נניח שאנחנו רוצים לחשב סכום גילאים של כל האנשים באיזשהו אוסף.

הדרך הקלאסית לעשות את זה היא באמצעות לולאה כזאת:

1
2
3
4
5
6
int ageSum = 0;
foreach (Person person in family)
{
ageSum = ageSum + person.Age;
}

אם ננתח את מה שקורה כאן:

ראשית אנחנו מאתחלים משתנה בערך התחלתי, וכל איטרציה של לולאה, אנחנו מחליפים את ערך המשתנה בסכום שלו ושל הגיל של האדם הנוכחי.

מסתבר שפעולה כזאת היא מאוד נפוצה, ואחת הפעולות הבסיסיות ב תכנות פונקציונאלי נקראת Reduce והיא מכלילה את מה שראינו עכשיו.

איך זה עובד? במקום לכתוב את מה שכתבנו עכשיו, נוכל לכתוב:

1
2
3
int ageSum =
family.Aggregate(0,
(current, person) => current + person.Age);

הפרמטר הראשון מציין את הערך ההתחלתי של "המשתנה", והפרמטר השני הוא delegate המציין במה להחליף את המשתנה בכל איטרציה.

בפועל מתבצעת הלולאה, ובסופו של דבר ageSum מקבל את הערך הסופי של המשתנה הזמני הנ"ל.

דוגמאות נחמדות הן כנ"ל:

נניח שיש לנו סל קניות, ובו כל איבר מציין סוג מוצר וכמות מאותו סוג:

1
2
3
4
5
6
public class ShoppingCartItem
{
// ...
public double Price { get; }
public int Amount { get; }
}

נניח שאנחנו רוצים לחשב את הסכום הסופי שיש לשלם על סל קניות זה.

נוכל לכתוב זאת כך:

1
2
3
4
5
double totalCost =
shoppingCart.Aggregate
(0.0,
(price, currentItem) =>
price + currentItem.Amount * currentItem.Price);

אם נניח שנרצה יותר מזה – למשל, בסוף התהליך לעגל את המחיר למספר השלם הקרוב ביותר למחיר זה, נוכל לעשות זאת באמצעות overload אחר של Aggregate:

1
2
3
4
5
6
double totalCost =
shoppingCart.Aggregate
(0.0,
(price, currentItem) =>
price + currentItem.Amount * currentItem.Price,
price => Math.Round(price));

הoverload הנ"ל מקבל גם delegate לביצוע בעת סיום הלולאה.

הערה:

הדוגמאות שנתתי אולי נראות כאילו תמיד נרצה לעשות סכום עם הדבר הזה, אבל זה לא נכון.

למשל, אפשר לחשב מינימום של כמות מוצרים שנקנתה:

1
2
3
4
5
int leastAmount =
shoppingCart.Aggregate
(0,
(amount, currentItem) =>
Math.Min(amount, currentItem.Amount));

ועוד ועוד, אין פה גבול ליצירתיות.

עוד הערה:

קיימות הExtension Methods בשמות Sum, Min, Max וכו’ המאפשרות לחשב סכום, מינימום ומקסימום מבלי להמציא את הגלגל.

הן, כמובן, עובדות בצורה דומה למה שעשינו כאן.

שתף