101. let keyword pitfalls

הכרנו את המילה השמורה let.

אחד הpitfallים הנפוצים הוא הדבר הבא, נניח שאנחנו רוצים לבצע שאילתא על דברים שהם לא null.

אפשר לכתוב את זה כך:

1
2
3
4
5
IEnumerable<Person> children =
from person in family
let age = person.Age
where (person != null) && (age <= 12)
select person;

אלא שזה מתקמפל אבל לא כל כך עובד.

מה הבעיה? אם יש לנו nullים בתוך family, אנחנו נעוף. למה?

כזכור, הקוד מתקמפל לקוד הבא:

1
2
3
4
IEnumerable<Person> children =
family.Select(person => new {person, age = person.Age})
.Where(pair => (pair.person != null) && (pair.age <= 12))
.Select(pair => pair.person);

מה שחשוב זו השורה של הSelect.

מה הבעיה כאן? הדבר הראשון שמתבצע זה Select, ורק אחריו כל השאר. לכן אנחנו ניגשים תחילה עבור כל איש באוסף ומחשבים את לperson.Age.

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

איך נפתור את זה? אפשר בצורה הבאה:

1
2
3
4
5
6
IEnumerable<Person> children =
from person in family
where (person != null)
let age = person.Age
where (age <= 12)
select person;

בתחילה נסנן את כל האנשים שהם null, ורק אחר כך נחשב את כל שאר הדברים.

הערה:

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

שתף

100. Multiple interface type

ובכן היום זה הטיפ היומי ה100,

לכבודו החלטתי לתת טיפ שהוא לא קשור לLINQ, אבל משהו נחמד שנראה לי מגניב מספיק כדי להיות בטיפ ה100.

נניח שיש לנו פונקציה שרוצה לקבל טיפוס שממש שני ממשקים. נקשה ונאמר שגם אין ממשק שיורש משני הממשקים.

1
2
3
4
5
6
7
8
9
public interface IShape
{
int NumberOfEdges { get; }
}
public interface IColored
{
Color Color { get; }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void MyMethod(IShape shape)
{
IColored colored = shape as IColored;
if (colored == null)
{
throw new ArgumentNullException
("shape",
"Expected a colored type");
}
else
{
Console.WriteLine("Number Of Edges: {0}, Color: {1}",
shape.NumberOfEdges,
colored.Color);
}
}

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

עם מספיק יצירתיות, אפשר לחשוב על הפתרון הזה:

1
2
3
4
5
6
7
public static void MyMethod<TColoredShape>(TColoredShape shape)
where TColoredShape : IColored, IShape
{
Console.WriteLine("Number Of Edges: {0}, Color: {1}",
shape.NumberOfEdges,
shape.Color);
}

ואז נוכל לקרוא למתודה פשוט כך:

נניח שיש לנו טיפוסים כאלה:

1
2
3
4
5
6
7
8
9
public class Triangle : IShape
{
// ...
}
public class ColoredTriangle : IShape, IColored
{
// ..
}

אז נוכל לקרוא למתודה ככה:

1
2
3
4
ColoredTriangle coloredTriangle =
new ColoredTriangle();
MyMethod(coloredTriangle);

אבל לא ככה:

1
2
3
Triangle triangle = new Triangle();
MyMethod(triangle);

מניב את שגיאת הקימפול

The type ‘Triangle’ cannot be used as type parameter ‘TColoredShape’ in the generic type or method ‘MyMethod(TColoredShape)’. There is no implicit reference conversion from ‘Triangle’ to ‘IColored’.

סופ"ש שאילתא מובנית שפה טוב

שתף

99. let keyword

בהמשך לשבוע שאילתא מובנית שפה,

נכיר היום את הkeyword ששמו let.

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

ומצד שני, אנחנו לא רוצים לפצל את השאילתא לשתי שאילתות (מה שאנחנו מקבלים באמצעות המילה into)

נוכל לעשות זאת באמצעות הKeyword let:

1
2
3
4
5
6
IEnumerable<string> children =
from person in family
let age = person.Age
where age < 12
select string.Format("{0} is {1} years old",
person.FirstName, age);

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

נוכל להשתמש בKeyword let, גם לפני וגם אחרי המילה where.

הקוד מתקמפל למשהו כזה:

1
2
3
4
5
6
IEnumerable<string> children =
family.Select(person => new {person, age = person.Age})
.Where(pair => pair.age < 12)
.Select(pair => string.Format("{0} is {1} years old",
pair.person.FirstName,
pair.age));

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

1
2
3
4
5
6
7
IEnumerable<string> children =
from person in family
select new {person, age = person.Age}
into pair
where pair.age < 12
select string.Format("{0} is {1} years old",
pair.person.FirstName, pair.age);

הKeyword הוא בעיקר טוב כדי לגשת לפונקציות כבדות פעם אחת בלבד.

המשך יום שאילתא מובנית שפה טוב

שתף

98. query syntax into keyword

אחד הKeywordים הפחות מוכרים בLINQ הוא into,

נניח יש לנו שליפה כזאת:

1
2
3
4
5
IEnumerable<string> childrenWithShortName =
from person in family
where (person.Age <= 12) &&
(person.FirstName.Length <= 3)
select person.FirstName;

זו שאילתא שמוצאת את כל הילדים שיש להם שם קצר

שימו לב לסחבת - אנחנו ניגשים לאותו Property כמה פעמים, מה שלא הכי יפה. (ראו גם טיפ 48 כדי להבין למה חשוב לא לעשות את זה)

במקום זאת נוכל לפצל את השאילתא לשתי שאילתות, באמצעות הkeyword ששמו into:

1
2
3
4
5
6
7
IEnumerable<string> childrenWithShortName =
from person in family
where person.Age <= 12
select person.FirstName
into first
where first.Length <= 3
select first;

מה שעשינו זה הכנסנו את הProperty ששמו FirstName לתוך משתנה ששמו first, וכך אנחנו ניגשים אליו רק פעם אחת, כפי שמציע טיפ מספר 48.

מדובר כאן ביותר מסמנטיקה, הintoמפצל את השאילתא לשתי שאילתות. למה הכוונה? אחרי הintoלא נוכל לגשת למשתנים בScope הקודם של השאילתא, לדוגמה לperson:

1
2
3
4
5
6
7
8
IEnumerable<string> childrenWithShortName =
from person in family
where person.Age <= 12
select person.FirstName
into first
where (first.Length <= 3) &&
(person.LastName == "Cohen") // This line doesn't compile
select first;

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

1
2
3
4
IEnumerable<string> childrenWithShortName =
family.Where(person => person.Age <= 12)
.Select(person => person.FirstName)
.Where(first => first.Length <= 3);

המשך יום שאילתא מובנית שפה טוב

שתף

97. Select many query syntax

[נכתב ע”י שני אלחרר]

ראינו בפעמים הקודמות את הExtension Method – SelectMany שיודעת לשטח IEnumerableים נוספים, וגם הכרנו את הquery syntax של linq.

ניתן להשתמש בquery syntax של linq על SelectMany, הדרך לעשות זאת היא כך :

1
2
3
4
5
IEnumerable<Person> allChildren =
from family in families
from person in family
where person.Age <= 12
select person;

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

1
2
3
IEnumerable<Person> allChildren =
families.SelectMany(persons => persons)
.Where(person => person.Age <= 12);

אבל למעשה, לא כך המצב.

אחרי הfrom השני עדיין יש לנו גישה למשתנה family, כלומר אנחנו יכולים לעשות משהו כזה:

1
2
3
4
5
IEnumerable<Person> allChildren =
from family in families
from person in family
where (person.Age <= 12) && (family.Count() >= 3)
select person;

לכן השאילתא הזאת

1
2
3
4
5
IEnumerable<Person> allChildren =
from family in families
from person in family
where person.Age <= 12
select person;

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

1
2
3
4
5
6
IEnumerable<Person> allChildren =
families.SelectMany
(family => family,
(family, person) => new {family, person})
.Where(pair => pair.person.Age <= 12)
.Select(pair => pair.person);

וזו שציינתי למעלה למשהו כזה:

1
2
3
4
5
6
7
IEnumerable<Person> allChildren =
families.SelectMany
(family => family,
(family, person) => new {family, person})
.Where(pair => (pair.person.Age <= 12) &&
(pair.family.Count() >= 3))
.Select(pair => pair.person);

אני אזכיר שSelectMany הוא Extension Method ומקבל שני delegateים – הראשון קובע איך לייצר IEnumerableים חדשים מאיברים של IEnumerable קיים, והשני קובע איך ליצור איבר מהIEnumerable החדש והאיבר המקורי.

משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>
(this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
{
foreach (TSource current in source)
{
foreach (TCollection transform in collectionSelector(current))
{
yieldreturn resultSelector(current, transform);
}
}
}

בשתי הדוגמאות הSelectMany מקבל אותם פרמטרים – מה שהוא עושה זה יוצר IEnumerable חדש של זוגות: איש והמשפחה אליה הוא שייך. שימו לב שהוא משתמש בטיפוסים אנונימיים שהכרנו כבר.

שתף

96. LINQ query syntax - where pitfall

נחשפנו פעם שעברה לQuery Syntax של LINQ.

נציג כעת Pitfall נפוץ בעבודה עם Query syntax.

לפעמים אנחנו כותבים שאילתא כזאת:

1
2
3
4
IEnumerable<Person> belowAverage =
from person in family
where person.Age <= AverageAge(family)
select person;

זו שאילתא שמוצאת את כל האנשים במשפחה שהגיל שלהם הוא קטן או שווה לממוצע של הגיל של המשפחה.

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

1
2
IEnumerable<Person> belowAverage =
family.Where(person => person.Age <= AverageAge(family));

עכשיו אם נזכר איך עובד Where:

1
2
3
4
5
6
7
8
9
10
11
12
public static IEnumerable<T> Where<T>
(this IEnumerable<T> enumerable,
Func<T, bool> predicate)
{
foreach (T current in enumerable)
{
if (predicate(current))
{
yield return current;
}
}
}

אז בעצם מתבצע קוד כזה:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<Person> FilterBelowAverage
(IEnumerable<Person> family)
{
foreach (Person person in family)
{
if (person.Age <= AverageAge(family))
{
yield return person;
}
}
}

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

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

איך מתגברים על הpitfall הזה?

פשוט מחשבים את מה שסטטי לפני השאילתא:

1
2
3
4
5
6
int averageAge = AverageAge(family);
IEnumerable<Person> belowAverage =
from person in family
where person.Age <= averageAge
select person;

כך החישוב מתבצע פעם אחת לפני הרצת השאילתא, במקום להתבצע עבור כל איטרציה…

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

שתף

95. LINQ query syntax

עד עכשיו ראינו בעיקר כל מיני Extension methods שיש במחלקה הסטטית Enumerable בnamespace של System.Linq.

אנחנו יכולים לכתוב כל מיני שאילתות כאלה:

1
2
3
4
5
IEnumerable<Person> family;
IEnumerable<string> childrenNames =
family.Where(person => person.Age <= 12)
.Select(person => person.FirstName);

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

לסינטקס כזה קוראים Fluent syntax.

קיימת גם אפשרות לכתוב syntax שמזכיר טיפה יותר שאילתת SQL שנראה ככה:

1
2
3
4
IEnumerable<string> childrenNames =
from person in family
where person.Age <= 12
select person.FirstName;

שמבצע בדיוק את מה שראינו מעלה.

לכל הכתיב הזה קוראים query syntax ולפעמים LINQ.

הסינטקס לא מוגבל רק לExtension Methods של System.Linq, אלא הוא מריץ Extension Methods כמו בכתיב למעלה. אם ניצור Extension Method משלנו בשם Select או Where, הוא יגש אליו

למשל אם ניצור Extension Methods כאלה

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static IEnumerable<TResult> Select<T, TResult>
(this IEnumerable<T> enumerable,
Func<T, TResult> selector)
{
Console.WriteLine("Hello world");
return new TResult[] {};
}
public static IEnumerable<T> Where<T>
(this IEnumerable<T> enumerable,
Func<T, bool> predicate)
{
Console.WriteLine("Hello world");
return new T[] {};
}

נראה שהשאילתא שרשמנו קודם נכנסת לקוד שלנו ומדפיסה Hello world.

כך שמי שטוען שהמימושים של LINQ לא יעילים, מוזמן לממש אותם בעצמו 😃

סופ"ש שפה מובנית שאילתא טוב

שתף

94. SelectMany extension method

נניח שיש לנו אוסף של משפחות ואנחנו רוצים למצוא את כל הילדים בה.

אז יש לנו משפחה שמיוצגת ע”י טיפוס היורש מIEnumerable, ואוסף של משפחות הוא IEnumerable של כאלה.

נניח למשל שמשפחה היא פשוט IEnumerable:

1
2
3
4
5
IEnumerable<IEnumerable<Person>> families;
IEnumerable<IEnumerable<Person>> children =
families.Select
(family => family.Where(person => person.Age <= 12));

אלא שchildren הוא IEnumerable<IEnumerable<Person>>, ולא IEnumerable<Person> כפי שהיינו מצפים.

בכדי לפתור את זה, קיים הExtension Method ששמו SelectMany המאפשר לנו לשטח Enumerable<IEnumerable<T>> לIEnumerable<T>.

שימוש:

1
2
3
4
5
6
IEnumerable<IEnumerable<Person>> childrenEnumerable =
families.Select
(family => family.Where(person => person.Age <= 12));
IEnumerable<Person> children =
childrenEnumerable.SelectMany(person => person);

או פשוט כך:

1
2
3
IEnumerable<Person> children =
families.SelectMany
(family => family.Where(person => person.Age <= 12));

המימוש הוא משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
public static IEnumerable<TResult> SelectMany<TSource, TResult>
(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
foreach (TSource current in source)
{
foreach (TResult transform in selector(current))
{
yield return transform;
}
}
}

שימו לב: הוא מקבל פונקציה שממפה כל איבר לIEnumerable<TResult> ואז מחזיר את כל האיברים בכל הטרנספורמציות הנ"ל.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>
(this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
{
foreach (TSource current in source)
{
foreach (TCollection transform in collectionSelector(current))
{
yield return resultSelector(current, transform);
}
}
}

המשך יום שפה מובנית שאילתא טוב

שתף

93. Where extension method

בהמשך לשבוע שפה מובנית שאילתא,

קיים גם Extension Method שנקרא Where, המאפשר לסנן איברים של IEnumerable

הוא מקבל IEnumerableוdelegate מסוג Func (פרדיקט), ומוצאת כל האיברים שמקיימים את הפרדיקט.

למשל הדוגמה הבאה מוצאת את כל הילדים במשפחה שהגיל שלהם הוא לכל היותר 12:

1
2
3
4
IEnumerable<Person> family = GetFamilyMembers("Banani");
IEnumerable<Person> children =
family.Where(person => person.Age <= 12);

המימוש הוא משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
public static IEnumerable<T> Where<T>
(this IEnumerable<T> enumerable,
Func<T, bool> predicate)
{
foreach (T current in enumerable)
{
if (predicate(current))
{
yield return current;
}
}
}

גם כאן יש overload שמבוסס אינדקס:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static IEnumerable<T> Where<T>
(this IEnumerable<T> enumerable,
Func<T, int, bool> predicate)
{
int index = 0;
foreach (T current in enumerable)
{
if (predicate(current, index))
{
yield return current;
}
index++;
}
}

למשל נוכל למצוא את כל האנשים שהגיל שלהם קטן או שווה ל12 והם נמצאים באינדקסים הזוגיים:

1
2
3
IEnumerable<Person> results =
family.Where((person, index) =>
(index%2 == 0) && person.Age <= 12);

המשך יום שפה מובנית שאילתא טוב

שתף

92. Select extension method

בהמשך לטיפ של אתמול,

הפונקציה הכי בסיסית שיושבת בSystem.Linq במחלקה הסטטית Enumerable נקראת Select.

פונקציה זאת מקבלת IEnumerableוdelegate מסוג Func ומחזירה IEnumerable שנוצר ע”י הפעלת הdelegate על כל האיברים של הIEnumerable.

לדוגמה:

1
2
3
4
IEnumerable<Person> family = GetFamilyMembers("Banani");
IEnumerable<string> names =
family.Select(person => person.FirstName);

הדוגמה יוצרת לנו IEnumerable<string>שמכיל את השמות הפרטיים של כל האנשים במשפחה.

נוכל גם לעשות דברים יותר מתוחכמים:

1
2
3
4
5
IEnumerable<string> names =
family.Select(person =>
string.Format("{0} {1}",
person.FirstName,
person.LastName));

מחזיר לנו את כל השמות המלאים של כל האנשים במשפחה.

המימוש הוא משהו בסגנון:

1
2
3
4
5
6
7
8
9
public static IEnumerable<TResult> Select<T, TResult>
(this IEnumerable<T> enumerable,
Func<T, TResult> selector)
{
foreach (T current in enumerable)
{
yield return selector(current);
}
}

שימו לב שהמימוש הוא באמצעות yieldreturn ולכן הוא lazy – הSelect מחושב on the fly כאשר כל פעם שמתבצע MoveNext, מתבצעת איטרציה של הפונקציה. (ראו גם טיפים 54 ו55)


כדי שלא תגידו שלא לומדים כלום מהטיפ היומי, הנה משהו שפחות מוכר:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
public static IEnumerable<TResult> Select<T, TResult>
(this IEnumerable<T> enumerable,
Func<T, int, TResult> selector)
{
int index = 0;
foreach (T current in enumerable)
{
yield return selector(current, index);
index++;
}
}

לדוגמה:

1
2
3
4
5
6
IEnumerable<string> names =
family.Select((person, index) =>
string.Format("{0}. {1} {2}",
index,
person.FirstName,
person.LastName));

יום שפה מובנית שאילתא טוב

שתף