לפני כמה פוסטים
הבנו
מה המשמעות של אופרטור ההשוואה (==) כשנעשה בו שימוש בקונטקסט של
Reference Types. למעשה הגענו למסקנה שבמימוש הבסיס הוא למעשה עונה לנו על
השאלה "האם 2 הרפרנסים שיש לי מצביעים לאותו אובייקט?", כלומר יש לנו כאן
השוואה של כתובות בזכרון. אם הן זהות, קיבלנו true; אחרת, false.
אבל
רק רגע, בנקודה הזאת אנחנו נזכרים לרגע בפונקציה ReferenceEquals שנמצאת
תחת מחלקת הבסיס Object. כמו שמשתמע מהשם, גם התפקיד שלה הוא לענות בדיוק
על אותה השאלה.
אז אם 2 הדרכים הללו למעשה "עושות אותו הדבר", מדוע יש
צורך בהפרדה הזאת? או השאלה היותר חשובה ומתבקשת: האם ומה ההבדל ביניהן?
(ספויילר: הבדל גדול).
במחשבת תחילה, אפשר לחשוב שאופרטור ההשוואה הוא פשוט סוג של "keyword" נוח
שנתנו במקום שנצטרך לקרוא בכל פעם ל-ReferenceEquals. כלומר, היינו מצפים
שקריאה לאופרטור ההשוואה למעשה תפנה אותנו ל-ReferenceEquals.
אז בואו נחקור את הטענה הזאת. אז פתחנו את Reflector, והלכנו לבדוק את
המימוש של Object.ReferenceEquals. ואיזה פלא, אנחנו רואים את הדבר הבא:
אז רגע, מה הולך כאן?נראה שהכל קורה כאן הפוך, ולמעשה ReferenceEquals הוא זה שמפעיל את אופרטור ההשוואה ולא ההפך?
אז זהו, שלא בדיוק. זה אחד המקרים שבאמת מדגים למה לא תמיד אפשר להסתמך
בעיניים עצומות על התרגום של Reflector, ועדיף לפנות לקוד ה-IL המקורי. כי
במקרה הזה למשל, Reflector פשוט מטעה אותנו, וכדי שנוכל להבין מה באמת
קורה כאן, אנחנו צריכים לפנות לרגע לקוד ה-IL המקורי.
אז כשאנחנו פונים אליו, אנחנו מקבלים את הקוד הבא:
מה
שמעניין אותנו כאן זה שכדי להשוות בין 2 המופעים, משתמשים בפקודה CEQ -
Compare Equal. מה שהפקודה הזאת יודעת לעשות, זה להשוות בין 2 ערכים
שמעבירים לה. במקרה הזה, 2 הערכים האלה הם למעשה הרפרנסים, כתובות הזכרון,
של המופעים שלנו. כלומר, יש כאן ממש השוואה בין 2 חתיכות זכרון, וזה הכל.
אז אחרי שראינו את זה, מעניין אותנו לראות מה יקרה כשננסה להשוות בין אובייקטים באמצעות אופרטור ההשוואה. והנה התוצאה:
// original code
object a = new object();
object b = new object();
bool x = a == b;
--- Generated IL ---
והנה שוב, אנחנו רואים שמתבצעת קריאה ל-CEQ. כך שבינתיים, אנחנו מקבלים התנהגות זהה לחלוטין בין אם מדובר באופרטור ההשוואה ו-ReferenceEquals.
הדברים מתחילים להיות מעניינים באמת ברגע שאנחנו מתחילים לדרוס את את
אופרטור ההשוואה. שבנקודה הזאת אנחנו כבר יכולים להראות התנהגות שונה בין
2 האפשרויות.
אז ניצור טיפוס חדש בשם Foo ונעניק מימוש חדש לאופרטור ההשוואה שלו. לאחר
מכן נקמפל את אותו הקוד מהדוגמה הקודמת ונסתכל על קוד ה-IL החדש שקיבלנו:
הפעם אנחנו רואים שבמקום הקריאה ל-CEQ למעשה מפעילים את המימוש החדש לאופרטור ההשוואה.
מה זה אומר לגבינו?
למעשה הגענו להבנה שכשרוצים לבדוק רפרנסים, יש להשתמש
ב-ReferenceEquals בלבד, ולא להתפתות לקצר ולהשתמש באופרטור ההשוואה. גם
אם היום אף אחד לא "דרס" אותו, אנחנו לא יכולים להסתכן ולסמוך על זה
שכשנשתמש בו באמת נשווה מצביעים וזה הכל. תמיד בעתיד יכול לבוא אדם (או
אפילו אתם בעצמכם), ויחליטו מסיבה כלשהיא לתת מימוש מיוחד לאופרטור
ההשוואה. לא משנה הסיבה. רק מה, אף אחד לא שם לב שאיפשהו בקוד עשיתם את
ההנחה שהאופרטור משווה כתובות. ופתאום פוף! שום דבר לא עובד.
אפשר להתפלא, אבל זה באג שתמיד מופיע כל כמה זמן מחדש. בדרך כלל כאשר
דורסים את Equals. כי אחרי הכל, זהו פחות או יותר המקום היחיד בקוד שלכם
שבאמת קיימת סיבה אמיתית להשוות רפרנסים אמיתית. בדרך כלל תעדיפו להשתמש
בהשוואה "המיוחדת" שכתבתם בעצמכם דרך אופרטור ההשוואה במקום להשוות
רפרנסים (בהנחה ובאמת היתה סיבה טובה לכך שהוחלט לעשות זאת מלכתחילה).
כך שהרבה פעמים אפשר לראות את הקוד הבא:
public class Foo
{
private int m_x;
public override bool Equals(object obj)
{
Foo other = obj as Foo;
if (other == null)
return false;
bool result = m_x.Equals(other.m_x);
return result;
}
}
בדוגמה הזאת, ביום שיחליטו להעניק מימוש חדש לאופרטור ההשוואה של Foo, כל
קריאה ל-Equals תגרור הפעלה של אופרטור ההשוואה. ובמידה ובתוך המימוש החדש
ישנה קריאה ל-Equals (כמו ברוב המקרים), אזי שתווצר לנו פה סדרה של קריאות אינסופיות בין Equals לאופרטור ההשוואה. כך עד שלפחות יתפוצץ ה-Call Stack ותזרק StackOverflowException.
במקום זה, הדרך הנכונה והבטוחה היא לקרוא ל-ReferenceEquals. כך שגם במידה
ויעניקו מימוש חדש לאופרטור ההשוואה, בדיקת הרפרנסים תתפקד ותצליח בכל
זאת. ללא קשר לאיזו לוגיקה מוזרה החליטו להכניס לאופרטור.