נתקלתי אתמול בבאג חמוד
-
@dovid וכל שאר המומחים לא לגלות
אתמול נתקלתי בבאג חמוד שב"ה אחרי חצי שעה הצלחתי לפתור, והחלטתי לשתף
כדי לפשט ולהראות את הבאג, הנה דוגמה קצרה:var list = new List<Action>(); for (int i = 0; i < 3; i++) { list.Add(() => Console.WriteLine(i)); } list.ForEach(x => x());
ציפיתי שהפלט יהיה:
0
1
2אבל במקום זה, קיבלתי:
3
3
3 -
@dovid וכל שאר המומחים לא לגלות
אתמול נתקלתי בבאג חמוד שב"ה אחרי חצי שעה הצלחתי לפתור, והחלטתי לשתף
כדי לפשט ולהראות את הבאג, הנה דוגמה קצרה:var list = new List<Action>(); for (int i = 0; i < 3; i++) { list.Add(() => Console.WriteLine(i)); } list.ForEach(x => x());
ציפיתי שהפלט יהיה:
0
1
2אבל במקום זה, קיבלתי:
3
3
3 -
-
אגב אם תשתמש ב foreach
var list = new List<Action>(); foreach (var i in Enumerable.Range(0, 3)) { list.Add(() => Console.WriteLine(i)); } list.ForEach(x => x());
התוצאה כן תהיה
0
1
2for - הלמבדה לוכדת את אותו משתנה ולא את הערך ברגע היצירה, ולכן תקבל תמיד את הערך האחרון (3).
foreach - כל איטרציה מקבלת משתנה חדש. -
בחזרה לנושא המקורי
משהו יכול להסביר מה זה שונה מ foreach או מ:for (int i = 0; i < 3; i++) { var j = i; list.Add(() => Console.WriteLine(j)); }
שזה מפיק:
[2 ,1 ,0]
הרי גם
i
וגםj
הם לוקאליים, אז למה הלמדה משתמשת פעם אחת בערך ופעם אחת ברפרנס? -
D dovid פיצל נושא זה
-
@קומפיונט
בגלל שj
הוא לוקלי, אבלi
לא, הוא מוצהר מחוץ ללולאה.
אז יש לך רפרנס שונה ל-j
בכל ריצה. ורק אחד ל-i
לכל הריצות.@חגי אתה מתכוונן שלמשתנה i יש סקופ יותר ארוך מ - j, אבל בפועל שניהם משתנים לוקאליים.
אחרי מחשבה אני משער שזה התנהגות מכוונת של הקומפיילר, ברגע שחוזרים כמה פעמים על יצירת למדה הקומפיילר מחליט האם ליצור אובייקט למדה חדש בכל פעם, או ליצור אובייקט למדה אחד בהתחלה ולעדכן אותו בכל פעם.
במקרה של
j
הקומפיילר מזהה שיש שימוש במשנה מהסקופ הנוכחי אז הוא יוצר אובייקט למדה חדש, אבל במקרה שלi
הוא יוצר את הלמדה פעם אחת בסקופ החיצוני ומעדכן אותו בכל שינוי.אפשר לאמת את זה על ידי בדיקת כתובות האובייקטים ברשימה.
-
@קומפיונט "לוקלי" זה בדיוק אורך הסקופ (לא מכיר משמעות אחרת למילה לוקלי בC#).
בכל איטרציה נוצר משתנה חדש j. פונקציית הלמבדה שנוצרת בכל איטרציה לוכדת את הרפרנס למשתנה שונה בכל פעם (שלכולם קוראים j אבל הם מקומות נפרדים בזיכרון).
בכל מקרה לכידה של משתנים בסקופ היא תמיד רפרנס למשתנה ואף פעם לא העתקת ערך - שינוי של הערך במשתנה ישפיע תמיד על הפונקציה הלוכדת (וגם לכן זה מעכב את איסופם באספן הזבל כל עוד יש מצביע חי לפונקציה).מלבד עצם העובדה שמדובר בהתנהגות מתוכננת ותקינה, סגנון הבדיקה שלך גם חוטא לשפת C#, זו לא שפה שנוצרה ואז תועדה אלא להיפך, ובתיעוד אין זכר למימושים חסכניים של מהדרים או מפרשים. המחקר הטכני שלך מעניין, אבל מדובר פה בהתנהגות הכרחית מעצם התיעוד של השפה בלי קשר למימוש כזה או אחר מאחורי הקלעים, זה כללי המשחק הרשמיים של השפה.
-
@קומפיונט "לוקלי" זה בדיוק אורך הסקופ (לא מכיר משמעות אחרת למילה לוקלי בC#).
בכל איטרציה נוצר משתנה חדש j. פונקציית הלמבדה שנוצרת בכל איטרציה לוכדת את הרפרנס למשתנה שונה בכל פעם (שלכולם קוראים j אבל הם מקומות נפרדים בזיכרון).
בכל מקרה לכידה של משתנים בסקופ היא תמיד רפרנס למשתנה ואף פעם לא העתקת ערך - שינוי של הערך במשתנה ישפיע תמיד על הפונקציה הלוכדת (וגם לכן זה מעכב את איסופם באספן הזבל כל עוד יש מצביע חי לפונקציה).מלבד עצם העובדה שמדובר בהתנהגות מתוכננת ותקינה, סגנון הבדיקה שלך גם חוטא לשפת C#, זו לא שפה שנוצרה ואז תועדה אלא להיפך, ובתיעוד אין זכר למימושים חסכניים של מהדרים או מפרשים. המחקר הטכני שלך מעניין, אבל מדובר פה בהתנהגות הכרחית מעצם התיעוד של השפה בלי קשר למימוש כזה או אחר מאחורי הקלעים, זה כללי המשחק הרשמיים של השפה.