סיכום שיעור (12.2.26)
פתחנו בחזרה ממוקדת על שיעור קודם: מערך עצמים, לולאות, ומה עובד נקי כשאין null. מיד אחר כך הזכרנו את התבנית הפשוטה של “מתחילים מהאיבר הראשון ומחפשים טוב ממנו” בהנחת מערך מלא, ואז גרסה עם null. שאלתם שוב על MaxBy - usage of Lambda expressions -, וחזרנו על כך שזה חד משמעית אסור בשימוש בבגרות למרות שהוא נוח. הוא גם קשה להבנה בשלב זה.
אחרי החימום חזרנו ל־Store/Customer, ושם הבהרנו שוב את השימוש ב-current: הוא מצביע לתא הריק הראשון, ולכן ההכנסה היא קודם תא נוכחי ואחר כך הגדלה (++current) כמו שהודגם. דרך השאלות בכיתה זוקק הניסוח: current הוא גם “מספר לקוחות”, וגם אינדקס לתא הפנוי הבא.
מכאן התפתח דיאלוג חשוב: אם מוחקים תא באמצע, האם להשאיר “חור” (null) או לשמור רצף. השאלה התחילה בתרחיש מחיקה מתוך האמצע, והפתרון שנבנה בכיתה היה הזזה שמאלה כדי לשמור מערך “contiguous” ללא חורים באמצע. תוך כדי, ננעלו שתי נקודות קצה קלאסיות: current - 1 כדי לא לגלוש ל־out of range, ואיפוס התא האחרון לאחר ההזזה ל־null.
חשוב לשמר את ההקשר לבגרות: לא נתקלתי בבגרות בשאלה שמבקשת ממש “למחוק + להזיז + לצמצם” (בטח שלא להסיק במשתמע שכך צריך לבצע את המחיקה). בפונקציה כזאת בדיוק, אבל כן חשוב להכיר כי זה מחדד מאוד הבנה של current והמנגנון הבסיסי והנקי.
הקוד של RemoveCust
internal class Customer
{
private string name;
private string telNum;
private int age;
public Customer(string name, string telNum)
{
this.name = name;
this.telNum = telNum;
}
public int GetAge() => age;
public string GetName() => name;
public string GetTelNum() => telNum;
}
internal class Store
{
private Customer[] arrCust new Customer[100]; // אפילו עדיף
private int current; // מחזיק את האינדקס של התא הריק הראשון
public Store()
{
//arrCust = new Customer[100]; // = האיתחול שהודגם בשיעור
current = 0;
}
public void AddCus(Customer customer)
{
//arrCust[current++] = customer; // short version as was on 5.2 lesson
arrCust[current] = customer; // current is the index of the first empty cell
// current is also the number of customers
current++; // current is the index of the next empty place
}
public Customer RemoveCust(int ind)
{
if (ind >= current || ind < 0)
return null;
Customer customer = arrCust[ind];
//0 1 2 (current == 3 take for example a full array of Length 3 to detect and avoid index out of range situation)
//A B C
for (int i = ind; i < current - 1; i++) // index out of range exception : תמיד צריך להיזהר מזה
arrCust[i] = arrCust[i + 1];
// B C C (after the loop ends)
arrCust[current - 1] = null;
// B C null
current--;
return customer;
}
בשלב התרגול עברנו לשאלה 3 מהמבחן (Interesting) והודגש שהתבנית של שבוע שעבר למערך מלא עצמים לא מספיקה כמו שהיא, כי כאן יש סינון price >= 10. סביב 00:37:43 הייתה הבהרה חשובה של ניסוח השאלה (“10 או יותר” לא מבטיח ש-10 קיים), ומהנקודה הזאת נבנה פתרון lowP = null שמתעדכן רק כשנכנסים לחלק הרלוונטי בלי ליפול ל־NullReference. אחר כך אחד המורים הציע בצדק לשפר קריאות ולהוציא את תנאי >= 10 לשכבה חיצונית כדי לא לחזור עליו פעמיים, והשיפור התקבל בכיתה ועדכנו את פתרון הבחינה.
public static string Interesting(Product[] arr)
{
Product lowP = null;
foreach (Product p in arr)
{
if (p.GetPrice() >= 10)
if (lowP == null || p.GetPrice() < lowP.GetPrice())
lowP = p;
}
return lowP.GetName();
}
באמצע התרגול עלתה תקלה מאוד נפוצה: “פתחתי קובץ שהורדתי, אני רואה אותו ב־Visual Studio, אז הוא בטוח חלק מהפרויקט”. כאן נעצרנו בדיוק כדי לתקן תפיסה: עצם זה שהקובץ פתוח בעורך לא אומר שהוא בפרויקט, ולכן חייבים לבצע את השלבים קליק ימני על הפרויקט ⟵ Add ⟵ Existing Item. זו נקודה חשובה מאוד להוראה פרקטית.
במקביל הודגמה גם הסיטואציה של “עובד לי ב־VS אבל לא עובר ב־CodeClassroom”: השגיאה נקראה בלייב ונמצאה בעיית casing (Getday מול GetDay) בדיוק במקום שבו הטסטים מצפים לשם אחר. מכאן הגיעה החזרה על הקיצור gs, על PascalCase, ועל הכלל “כל מילה חדשה מתחילה באות גדולה” כולל ההקשר לג׳אווה. לכן ההמלצה נשארה חד־משמעית: לעבוד עם snippet gs כדי למנוע טעויות שמכשילות טסטים במערכת ההגשות.
לפני סטטי (ואז שוב אחריו) נפתחה סטייה מבוקרת ל־C# native properties: הצגת prop/propfull, ההבנה שהשמה לתכונה מפעילה setter מאחורי הקלעים, והמעבר מ־prop ל־propfull כשצריך ולידציה בשלב מאוחר יותר. הודגש גם ההבדל הטרמינולוגי בין field ל־property בסביבות 52:24, ולמה דוחים את זה פדגוגית עד שהתלמידים מקבעים קודם את Java-style getter/setter לצרכי הבגרות.
לדיוק פדגוגי: למה בכל זאת מלמדים קודם Java-style
- זה שומר “שפה אחידה” למורים שמלמדים גם C# וגם Java.
- זה מונע בשלב מוקדם בלבול בין שדה פרטי לבין גישה חיצונית.
- חשוב שתהיה הפרדה ארוכה בזמן לפני שמלמדים תלמידים את האמת לגבי C#, כדי שהזיכרון הראשוני שהם צריכים לבגרות יתקבע.
בחלק של static הודגש שהמוקד הפעם הוא לא מחלקה סטטית, אלא שדה סטטי (מה שאליו מתייחסים כתכונה במשרד החינוך): בדוגמת nextAccountNumber ההכרזה ואז ההסבר שזה ערך שקיים פעם אחת ברמת המחלקה ולא לכל עצם. מהלך הדוגמה עם כמה עצמים חיזק איך המספר רץ 1000, 1001, 1002 כי כולם חולקים אותו משתנה, הודגש שהסטטי זמין אפילו לפני יצירת עצם. אפשר לראות שהסטטי כבר זמין בפניה אליו דרך שם המחלקה.
מכאן הגיע הדיוק הקריטי: ב־C# גישה לסטטי היא דרך ClassName.Member בלבד. זה הודגם שוב ושוב, כולל ניסוי שבו ==גישה דרך עצם פשוט לא מתקבלת== כמותר/אסור, וסוכם במפורש: תכונה סטטית ניגשים אליה בשם המחלקה ולא דרך עצם. הדגמת Console.ForegroundColor חיברה את העיקרון לדוגמה (native C# static property) מוכרת.
ואז נסגר המעגל של public static void Main: נשאלה השאלה “למה כל הזמן static ב־Main?” ב־01:13:21 נבנתה תשובה מדורגת,: Main הוא entry point יחיד, ולכן הוא סטטי; בתוך Main לא קיים עדיין עצם של Program, ולכן אי אפשר לקרוא ישירות לפעולה לא סטטית בלי ליצור עצם. בהמשך גם הודגם ששם המחלקה לא חשוב, ו־Main יכול לזוז למחלקה אחרת כל עוד נשאר Main יחיד בפרויקט.
דיוק קצר למורי Java: ההבדל שהודגש בשיעור
בשיעור עלתה שוב ושוב ההבחנה שב־C# פונים לסטטי דרך שם מחלקה בלבד, ואילו ב־Java לעיתים אפשר גם דרך reference של עצם (גם אם זה לא מומלץ סגנונית).
// C#
// obj.StaticProp = 1; // לא תקין
MyType.StaticProp = 1; // תקין
// Java
obj.staticField = 1; // מתקמפל, אבל לא מומלץ
MyType.staticField = 1; // הצורה המומלצת
מבחינת הוראה: כדאי להמשיך להתעקש בכיתה על ClassName.Member גם ב־Java, כדי לשמור עקביות מחשבתית עם המשמעות של static.
יצירת פרוייקט בדיקות - הדגמה
הרחבת מורים (מחוץ לחומר): מתי אין צורך ב־Main, ומתי הוא כן חובה. יצרנו פרוייקט ספרייה, ויצרנו פרוייקט בדיקות
ההסבר על static Main הורחב כאן לפרקטיקה של בניית פתרונות אמיתיים. זו סטייה מחומר הליבה, אבל חשובה מאוד למורים.
- על הצד ה”שלילי” (כשאין
Main): יצרנו Class Library חדשה בתוך אותו Solution, כדי להמחיש שפרויקט כזה מייצר DLL נוסף ולא נקודת כניסה להרצה. - המשכנו ליצירת פרויקט בדיקות NUnit, ואז הודגש במפורש שבפרויקט בדיקות לא אמור להיות
Main. - כדי שה־tests יוכלו לגשת לקוד, הודגם גם שלב החיבור בין הפרויקטים דרך Add Project Reference.
- על הצד ה”חיובי” (כשכן צריך
Main): בפרויקט שמטרתו הרצה (.exe) חייבת להיות נקודת כניסה אחת; זה סוכם שוב סביב single entry point. - כדי להפוך את זה למוחשי, נפתח חלון
cmdמתוך התיקייה הנכונה (באמצעות הקלדתcmdמשורת הכתובת ב־Explorer-סייר הקבצים) והודגם הרצה מה־CMD. - מאותה הרצה הודגם גם איך שולחים פרמטרים חיצוניים ל־
Main(args)ואיך קוראים אותם מתוךargsבפועל.