LINQ, חלק ב' — GroupBy ו-FirstOrDefault
מה זה?
“כמה תלמידים יש בכל מחלקה?” — Where/Select/OrderBy לבד לא עונים על זה; צריך לקבץ תלמידים לפי מחלקה. GroupBy פותר בדיוק את זה. ובכיוון ההפוך — “מצא לי תלמיד בשם דנה” עם Where(...).First() קורס אם אין תלמיד כזה; FirstOrDefault פותר את זה בבטחה, ומחזיר null (או ערך ברירת מחדל) במקום לזרוק חריגה.
מילות מפתח שחשוב לזכור
• GroupBy — מקבץ איברים לפי מפתח (s => s.ClassName), מחזיר קבוצות שכל אחת מכילה את המפתח ואת האיברים ששייכים אליו
• First — מחזיר את האיבר הראשון שעונה על תנאי; זורק חריגה אם אין אף איבר תואם
• FirstOrDefault — כמו First, אך מחזיר null (לטיפוסי Reference) או ערך ברירת מחדל (לטיפוסי Value) אם אין התאמה, במקום לזרוק
• Count() — סופר איברים באוסף/בקבוצה, אפשר עם או בלי תנאי סינון
List<Student> students = new List<Student>
{
new Student("Dana", "A") , new Student("Avi", "B"), new Student("Rotem", "A")
}; // assume a ClassName field was added
// GroupBy — grouping by class
var byClass = students.GroupBy(s => s.ClassName);
foreach (var group in byClass)
{
Console.WriteLine($"Class {group.Key}: {group.Count()} students");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
// Class A: 2 students (Dana, Rotem)
// Class B: 1 students (Avi)// First vs FirstOrDefault
Student found = students.FirstOrDefault(s => s.Name == "Dana");
if (found != null)
{
Console.WriteLine($"Found: {found.Name}");
}
Student notFound = students.FirstOrDefault(s => s.Name == "Does not exist");
Console.WriteLine(notFound == null); // true — didn't crash, just null
// Student crash = students.First(s => s.Name == "Does not exist"); // InvalidOperationException!הסבר עיקרי
GroupBy מחזיר “קבוצות של קבוצות” — כל איבר בתוצאת GroupBy הוא בעצמו אוסף (IGrouping<TKey, TElement>) עם .Key (ערך הקיבוץ, למשל “A”) ורשימת האיברים ששייכים אליו. זו הסיבה שצריך foreach מקונן (על הקבוצות, ואז על האיברים בכל קבוצה) כדי להדפיס הכל — בדיוק כמו שהראנו בדוגמה.
למה FirstOrDefault הוא ברירת המחדל הבטוחה — בדיוק כמו ש-int.TryParse עדיף על int.Parse לקלט לא-מובטח (מהיחידה הבסיסית), FirstOrDefault עדיף על First בכל פעם שלא בטוח ב-100% שיש התאמה. First שימושי רק כשידוע בוודאות שהאיבר קיים (למשל, בדיוק בדקנו את זה קודם) — אחרת, זריקת InvalidOperationException היא הפתעה לא נעימה.
הקשר בין GroupBy, Count, ו-הפרויקט המסכם — קיבוץ נתונים ואז ספירה בתוך כל קבוצה הוא בדיוק הדפוס שמופיע בסיווג נתונים (Classification) — הפרויקט המסכם של היחידה (Naive Bayes) יסתמך על ספירת תדירויות בתוך קטגוריות, בדיוק כמו GroupBy + Count() שתרגלנו כאן.
יתרונות
GroupBy הופך קיבוץ וספירה לפי קטגוריה לכמה שורות במקום מבנה Dictionary ידני מסובך; FirstOrDefault מונע קריסות על “לא נמצא” בלי try/catch מכביד; Count() (עם או בלי תנאי) נותן ספירה מהירה בלי לולאה ידנית.
חסרונות
GroupBy על אוספים ענקיים דורש מעבר מלא על כל הנתונים (לא “עצל” כמו חלק מפעולות LINQ אחרות) — עלול להיות כבד לזיכרון על נתונים גדולים מאוד; קוד foreach מקונן על תוצאת GroupBy עלול להיות מבלבל למתחילים בהתחלה.
נקודות חשובות
• GroupBy מחזיר קבוצות עם .Key (ערך הקיבוץ) ואוסף האיברים בכל קבוצה
• First זורק חריגה אם אין התאמה; FirstOrDefault מחזיר null/ברירת מחדל בבטחה
• FirstOrDefault הוא הבחירה הבטוחה כברירת מחדל, בדיוק כמו TryParse על קלט
• Count() יכול לקבל תנאי אופציונלי (students.Count(s => s.Age >= 18)) לספירה מסוננת ישירות
טעויות נפוצות
• להשתמש ב-First כשלא בטוחים שיש התאמה, ולקבל InvalidOperationException בלתי-צפוי
• לשכוח לבדוק null אחרי FirstOrDefault ולנסות לגשת לשדה על תוצאה ריקה (NullReferenceException)
• לנסות לגשת לתוצאת GroupBy כאילו היא Dictionary רגיל, במקום foreach על קבוצות
סיכום
GroupBy מקבץ איברים לפי מפתח, ומחזיר קבוצות עם .Key ואוסף האיברים בכל אחת — שימושי לסטטיסטיקות וספירה לפי קטגוריה. FirstOrDefault שולף איבר בודד בבטחה, ומחזיר null/ערך ברירת מחדל אם אין התאמה, במקום לזרוק חריגה כמו First. שני הכלים האלה, יחד עם Where/Select/OrderBy מהשיעור הקודם, משלימים את ארגז הכלים הבסיסי של LINQ שנדרש לפרויקט המסכם של היחידה.
דוקומנטציה רשמית
Microsoft Learn — FirstOrDefault
תרגילים
תרגיל 1 — GroupBy בסיסי
המשימה: מתוך List<Student> עם שדה ClassName, קבצו לפי מחלקה עם GroupBy, והדפיסו לכל מחלקה כמה תלמידים יש בה.
בדיקה: כל מחלקה מוצגת עם המספר הנכון של תלמידים ששייכים אליה.
תרגיל 2 — FirstOrDefault בטוח
המשימה: כתבו פונקציה Student FindStudentByName(List<Student> students, string name) שמשתמשת ב-FirstOrDefault ומחזירה null אם לא נמצא, בלי לקרוס.
בדיקה: חיפוש שם קיים מחזיר את התלמיד הנכון; חיפוש שם לא קיים מחזיר null בלי חריגה.
פרויקט מסכם
המשימה: בנו “דוח סטטיסטיקות כיתה” שמשלב את כל כלי ה-LINQ מהשיעורים.
דרישות:
GroupByלפיClassName, ולכל קבוצה חשבו ממוצע ציונים (Average()) וספירת תלמידים (Count())- מיינו את הקבוצות לפי ממוצע יורד (
OrderByDescending) - השתמשו ב-
FirstOrDefaultכדי למצוא ולהדפיס בנפרד את התלמיד עם הציון הגבוה ביותר בכל המערכת (כולל טיפול במקרה של רשימה ריקה)
בדיקה: הדוח מציג את כל המחלקות ממוינות לפי ממוצע, כל אחת עם ממוצע וכמות תלמידים נכונים; התלמיד המצטיין מזוהה נכון בנפרד.
מה בפרק הבא
בפרק הבא — פרויקט מסכם: Naive Bayes — פרויקט הסיכום הראשון של הקורס, שמשלב CSV, LINQ, OOP ו-SOLID לבניית מסווג טקסט פשוט מאפס.