מה זה?

“כמה תלמידים יש בכל מחלקה?” — 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 — GroupBy

Microsoft Learn — FirstOrDefault


תרגילים

תרגיל 1 — GroupBy בסיסי

המשימה: מתוך List<Student> עם שדה ClassName, קבצו לפי מחלקה עם GroupBy, והדפיסו לכל מחלקה כמה תלמידים יש בה.

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

תרגיל 2 — FirstOrDefault בטוח

המשימה: כתבו פונקציה Student FindStudentByName(List<Student> students, string name) שמשתמשת ב-FirstOrDefault ומחזירה null אם לא נמצא, בלי לקרוס.

בדיקה: חיפוש שם קיים מחזיר את התלמיד הנכון; חיפוש שם לא קיים מחזיר null בלי חריגה.


פרויקט מסכם

המשימה: בנו “דוח סטטיסטיקות כיתה” שמשלב את כל כלי ה-LINQ מהשיעורים.

דרישות:

  1. GroupBy לפי ClassName, ולכל קבוצה חשבו ממוצע ציונים (Average()) וספירת תלמידים (Count())
  2. מיינו את הקבוצות לפי ממוצע יורד (OrderByDescending)
  3. השתמשו ב-FirstOrDefault כדי למצוא ולהדפיס בנפרד את התלמיד עם הציון הגבוה ביותר בכל המערכת (כולל טיפול במקרה של רשימה ריקה)

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

מה בפרק הבא

בפרק הבא — פרויקט מסכם: Naive Bayes — פרויקט הסיכום הראשון של הקורס, שמשלב CSV, LINQ, OOP ו-SOLID לבניית מסווג טקסט פשוט מאפס.