Liran Chen's Blog

.Net Internals, Development, Multithreading - and More!

July 2009 - Posts

Brain Teasing With Strings
יש לי חידה קטנה בשבילכם.
מדובר בדוגמאת קוד קטנה שמציגה התנהגות מעט.. לא צפוייה. אפשר למצוא אותה מסתובבת באינטרנט בכל מיני וריאציות משונות, אבל בשום מקום שנתקלתי בה, לא באמת סיפקו איזשהו הסבר אמיתי להתנהגות הזאת.
אז בלי יותר מדי הקדמות, מה לדעתכם יהיה הפלט עבור התוכנית הבאה?

static void Main()

{

    char[] a = new char[0];

    string b = new string(a);

    string c = new string(new char[0]);

    string d = new string(new char[0]);

 

    Console.WriteLine(object.ReferenceEquals(a, b));

    Console.WriteLine(object.ReferenceEquals(b, c));

    Console.WriteLine(object.ReferenceEquals(c, d));

}




בדרך כלל אפשר לקבל תשובות שונות מאנשים שונים לשאלה הזאת.. הרי מצד אחד, אנחנו משתמשים במילת המפתח new על Reference Types, "וכידוע" כשמתמשים ב-new אנחנו תמיד מקצים אובייקט חדש על ה-Heap. אבל רגע.. מצד שני, אנחנו גם יודעים שאם אנחנו יוצרים מחרוזת עם איזשהיא Hardcoded String, היא למעשה תצביע לאזור קבוע בזכרון בו נשמרות כל המחרוזות הקבועות. אבל, אנחנו גם יודעים שמאחר ומחרוזות הן Immutable, אז כשאנחנו באים להשוות בין מחרוזות אנחנו למעשה משווים את הערכים שלהן, ולא את המצביעים ... אבל, אנחנו משתמשים כאן ב-ReferenceEquals , כלומר שאנחנו בכל זאת אמורים תמיד להשוות את הכתובות אליהם אנחנו מצביעים (כמו שהוסבר בפוסט הזה). בקיצור, יש כאן בלאגן לא קטן.
אז אחרי כל ההתפלספות הזאת, בואו נבדוק את הפלט:
False
True
True
הממ.. טוב, זה היה די מפתיע.
אז מה יש לנו כאן בעצם? מצד אחד, a לא שווה ל-b. שזה הגיוני, אחרי הכל מדובר באובייקטים שונים, מטיפוסים שונים. מצד שני, אנחנו גם רואים ש-b שווה ל-c, וגם כן ל-d. זה יכול להיות די מבלבל, הרי אם אנחנו זוכרים מה שאמרנו לפני רגע לגבי Hardcoded Strings, אנחנו יודעים שהתנאי הזה מתקיים אך ורק עבור מחרוזות שמקודדות ישירות לתוך הקוד. אבל, זהו וודאי לא המקרה שלנו כאן. למעשה, מה שעשינו כאן זה ליצור שני מערכים שונים מטיפוס char באורך 0. גם אם היינו משווים אותם עם ReferenceEquals היינו מקבלים תשובה שמדובר במופעים שונים. כלומר, בפועל יצרנו כאן בקוד 3 מערכים שונים, אבל בכל זאת הקצנו רק מחרוזת אחת.

אז בשלב הזה כבר מתחילה להתעורר השאלה "למה?" או "איך?".
ובכן, "בדרך כלל" (עוד מעט נבין למה) כשאנחנו יוצרים מחרוזת חדש עם בנאי שמקבל איזשהו מערך או אובייקט אחר שמייצג מחרוזת, באמת נוצרת לנו מחרוזת חדשה וייחודית. כלומר, c ו-d יהיו מחרוזות שונות לחלוטין.
אבל בכל זאת, אנחנו רואים שזאת לא ההתנהגות שאנחנו מקבלים. הסיבה לכך היא במימוש הפנימי של המחלקה String, שהמימוש "האמיתי" שלה נמצא עמוק בתוך המימוש, תחת השם SString, כלומר Safe String.
מה שקורה שם, זה שבתוך הבנאי בודקים את האובייקט שהועבר, ורואים האם הוא שווה ל-Null או שהוא באורך 0. אם כן, שוכחים לגמרי ממנו וגורמים למופע החדש להצביע למחרוזת ריקה שהוכנה מבעוד מועד. את ההתנהגות הזאת אנחנו יכולים לראות בפונקציות Set ו-Clear (הפונקציה Set נקראת על ידי הבנאי, המימוש נלקח מ-SSCLI).

void SString::Set(const WCHAR *string)

{

    if (string == NULL || *string == 0)

        Clear();

    else

    {

        Resize((COUNT_T) wcslen(string), REPRESENTATION_UNICODE);

        wcscpy_s(GetRawUnicode(), GetBufferSizeInCharIncludeNullChar(), string);

    }

 

    RETURN;

}


void SString::Clear()

{

    SetRepresentation(REPRESENTATION_EMPTY);

 

    if (IsImmutable())

    {

        // Use shared empty string rather than allocating a new buffer

        SBuffer::SetImmutable(s_EmptyBuffer, sizeof(s_EmptyBuffer));

    }

    else

    {

        SBuffer::TweakSize(sizeof(WCHAR));

        GetRawUnicode()[0] = 0;

    }

 

    RETURN;

}


Don't Leave Home Without It

עבור ציבור משתמשי פיירפוקס, ישנו אוסף לא מבוטל של Plugin'ים שהשימוש בהם נהפך כבר ממש לבגדר חובה. רשימת החובה שלי כוללת את: Gmail Manager, IETab, DownThemAll!, AdBlockPlus.
בנוסף לארבעת אלה, לאחרונה נתקלתי בפלאגין חדש שאוטומטית נכנס לרשימת ה-Must Have הזאת. הפלאגין שאני מדבר עליו הוא Lazarus שדואג לשחזר טפסים שתוכנם נאבד אחרי קריסה של פיירפוקס או כל אסון טבע אחר.
כמו שהפתיח באתר הפלאגין אומר, במידה ואי פעם כתבתם איזשהוא פוסט, הודעה ארוכה, או כל דבר אחר באינטרנט, לחצתם על "שלח" ופתאום נתקלתם במסך שגיאה, אתם בוודאי מבינים עד כמה הפלאגין הזה יכול להציל אתכם. מה שבהכרח גורם לך להרהר איך אף אחד לא חשב על זה קודם, הרי מדובר בפתרון הכרחי לבעיית ה"אוי.. לא. זה לא-- לעזאזל!" המוכרת.

באופן אישי, הוא הספיק כבר להציל אותי מספר פעמים מאובדן עשתונות טוטאלי, כך שאני כבר חב לו תודה. ולמען האמת, בפעם הראשונה שבאמת מנסים לשחזר איתו איזה טופס אבוד, ולא בתור איזו "בדיקה" לראות אם הוא באמת עובד, מוציאים בסוף כזאת אנחת רווחה.. פשוט תענוג.

אז למה אתם מחכים? לכו ותתקינו אותו כבר.

 
Posted: Jul 17 2009, 03:10 PM by Liran Chen | with 1 comment(s)
Filed under: , ,
Log Your Assertions
שימוש ב-Assert יכול להיות פתרון נוח ושימושי כשרוצים לוודא שה-State שאנחנו נמצאים בו כרגע, באמת מתאים למה שאנחנו מצפים. אנחנו יכוללים להשתמש בו בשביל לבדוק כל מיני מקרי קצה ביזארים, כל מיני מצבים ש"אין מצב" שיקרו (על פי הלוגיקה הקיימת בקוד). כל מיני מצבים שהמשך הקוד שלנו מסתמך על זה שהם לא אפשריים, ולא יקרו בזמן הוא רץ. בדרך כלל נקרא ל-Assert דרך המחלקה הסטאטית Debug, שעושה שימוש ב-ConditionalAttribute עם הערך "DEBUG", כך שאנחנו יודעים שכל הבדיקות האלו לא יעלו לנו כלום כשנצא לגרסת ה-Release, כך שאנחנו גם לא צריכים "להתקמצן" על כל Assert קטן בקוד שיכול לעזור לנו למצוא כל מיני מצבים לא הגיוניים בתוכנית שלנו.

ברגע שה-Assert שלנו יכשל, יופיע MessageBox גדול ויפה שיציג לנו את ההודעה שרצינו להדפיס במידה וה-Assert נכשל, ובנוסף לכך את ה-StackTrace הרלוונטי. הבעיה מתחילה כשאנחנו חס וחלילה.. סוגרים את החלון הזה. ופוף! הכל נעלם. אנחנו אולי יודעים בערך איזה Assert בקוד נכשל, אבל מן הסתם אנחנו לא זוכרים את ה-StackTrace, או חשוב מכך, את הזמן שבו ה-Assert קרה (כך שנוכל אחר כך להתאים אותו ביחס לשאר הלוגים שהדפסנו במהלך הריצה, למעשה להבין פחות או יותר מה היה יכול להשפיע על הכישלון הזה).
צריל לזכור ש-Assert כושל לא יגרור זריקת Exception. כל מה שהוא יעשה זה לעבור על כל ה-TraceListener'ים שהוצמדו ל-Debug, ופשוט לקרוא לפונקצית ה-Fail שלהם. כברירת מחדל ה-Listener היחיד שמוצמד הוא ה-DefaultTraceListener. שרק מדפיס ל-Output את ההודעה, ובנוסף מציג את ההודעה בחלון ה-MessageBox המוכר (במידה והערך הנמצא בשדה AssertUiEnabled מאפשר לו לעשות זאת).
מכאן, שלא משנה בכמה Try-Catch'ים עטפנו את הקוד שלנו, הכשלון הזה לא יתפס, ואנחנו יכולים למצוא עכשיו את עצמנו מגששים באפלה אחר סיבות אפשריות לכישלון בבדיקה.
אולם, המצב לא חייב להיות כך. ואנחנו יכולים לדאוג שכל כשלון כזה יטופל כמו כל שגיאה אחרת, וישמר אוטומטית בלוג של התוכנית שלנו. נוסף על כך, אם נרצה נוכל גם להכריח את התוכנית להסגר, במידה ואנחנו לא מעוניינים שהיא תמשיך לעבוד במצב זומבי שכזה, בו אנחנו בין כה וכה לא יודעים מה קורה איתה, ומצפים שהיא תקרוס בקרוב בגלל איזו תופעת לוואי שנגרמה כתוצאה מה-State הלא צפוי אליו היא נכנסה.
הפתרון הוא פשוט למדי, כל מה שעלינו לעשות זה ליצור TraceListener חדש משלנו, ולהשתמש בו. אנחנו יכולים ליצור מחלקה חדש שתרש מהמחלקה האבסטרקטית TraceListener, ותדרוס את המימוש של הפונקציה Fail. בתוך המימוש החדש, נשאר לנו רק לשמור ללוג את ההודעה + ה-StackTrace שמעניין אותנו, וזה הכל. מעכשיו כל Assert כושל שיתרחש אצלנו, ישמר אוטומטית ללוג.
מאחר וה-Listener'ים מופעלים בצורה סינכרונית, אחד אחרי השני. אפשר וכדאי לדאוג שה-Listener שלנו יהיה הראשון שמופעל, כך שלא נבזבז זמן על זה שחלון ה-MessageBox מוצג, או שבטעות מישהו יסגור את התוכנית לפני שנספיק לשמור את השגיאה ללוג שלנו (באותה מידה אפשר גם לדאוג שה-Listener שלנו יהיה היחיד שקיים, ואז בסוף ה-Fail לזרוק למשל שגיאה ... בכל אופן, לא חסרות דרכים להתמודד עם הבעיה).

class Program

{

    static void Main()

    {

        FooListener foo = new FooListener();

        Debug.Listeners.Add(foo);

 

        Debug.Assert(false, "whoops..");

    }

}

 

class FooListener : TraceListener

{

    public override void Write(string message) { }

    public override void WriteLine(string message) { }

 

    public override void Fail(string message)

    {

        Console.ForegroundColor = ConsoleColor.Red;

 

        // use your preferred logger here..

        string stackTrace = Environment.StackTrace;

        Console.WriteLine("Assertation Failed. Message: {0}, StackTrace: {1}", message, stackTrace);

    }

}

Code Admiration

רוב המפתחים נוטים לרכוש לעצמם עם הזמן כל מיני מנהגים רעים אליהם הורגלו במשך השנים. לפעמים זה משהו פעוט ולא חשוב, ולפעמים זה משהו שבאמת יכול להשפיע לרעה על העבודה. מה שבטוח, זה שכולנו, בלי יוצאים מן הכלל, חוטאים לפעמים.
הנקודה שרציתי להעלות היא הנושא של הערצת קוד. תעצרו לרגע ותשאלו את עצמכם האם אתם יכולים להזדהות עם התרחיש הבא: הרגע סיימתם לקודד איזשהיא משימה ארוכה ומתישה, שדרשה מכם לא מעט מחשבה ומאמץ, אך בסופו של דבר הגעתם לתוצר שהוא מבחינתכם הוא שיא השלמות. אז אחרי שסיימתם, אתם סוקרים את הקוד, מסתכלים על ה-Design החכם, מביטים בקוד היפה, אבל בעיקר עסוקים בלטפח לעצמכם את האגו. בלי יותר מדי יומרנות אנחנו חושבים לעצמנו שחתיכת הקוד הזאת שהרגע סיימנו לכתוב, ללא ספק צריכה להיות פסגת היצירה האנושית. "זה פשוט כל כך.. יפה! רק בא לי להתמוגג מול המסך. הקוד הזה שווה ערך ל.. שירה".
טוב, אז אולי מעט הגזמתי, אבל הבנתם את הרעיון. סביר להניח שאתם מכירים את ההרגשה שאני מדבר עליה, אחרי הכל, הרגשתם אותה כבר לא פעם. זה פשוט משהו שקורה לכולם. ולמען האמת, די הגיוני. אחרי הכל, ידוע שאין דבר שמפתחים אוהבים יותר מאשר לטפח לעצמם את האגו..

ועכשיו לעניין.
הערצת קוד יכולה להוות מכשול ונקודת חולשה בתהליך הפיתוח. כי בכל פעם שאנחנו מביטים בקוד שלנו בהערצה צרופה, אנחנו נוטים להיות סלחניים ואופטימים מדי. אוטומטית, אנחנו יוצאים מתוך נקודת ההנחה שהקוד הזה תמיד יעבוד, לא יהיו בו באגים, ולא נצטרך לשנות בו דבר לעולם. אחרת, הוא לא היה נפלא כמו שאנחנו חושבים שהוא. אנחנו מרשים לעצמנו לבדוק אותו פחות ברצינות מאשר קוד אחר שהיינו כותבים. אנחנו נוהגים לא להכנס לפרטים הקטנים, ולחשוב על כל מקרי הקצה הביזארים שעלולים לצוץ. זה לא צריך לקרות במודע, אבל זה קורה. ובצורה הזאת אנחנו חושפים את עצמנו לבאגים ותקלות רבות יותר מבדרך כלל.
לכן, המתודולוגיה שאני תמיד מעודד את עצמי ללכת לקראת כשמדובר בקידוד, היא סקפטיות. כשאני כותב תוכנית שאחר כך מתעופפת בגלל שגיאה, אני לא מתפלא איך היא קרסה, אלא אני מתפלא איך היא בכלל עבדה מלכתחילה. כשאני מסיים לכתוב קטע קוד, הדבר האחרון שאני מרשה לעצמי לעשות זה לסמוך על עצמי. אני יוצא מנקודת ההנחה שסביר להניח "שמשהו" כאן לא עובד. ומה אתם יודעים, בדרך כלל זה גם נכון. אחרי הכל, כמה פעמים באמת כתבתם קוד שהיה נקי ב-100% מבאגים מוזרים ומקרי קצה היסטרים? תמיד יש "משהו" שמסתתר בין השורות הקוד. אז בכל פעם שאני מסיים לקודד, אני אוטומטית יודע שמונח לפני עכשיו באג חדש. האלטרנטיבה היא להתחיל ולהריץ UnitTest'ים, ו-Sanity עד ש"המשהו" הזה יצוף מעל פני המים ויחשוף את עצמו דרך קריסה טוטאלית כזאת או אחרת של המערכת. אישית, אני מעדיף (וגם ממליץ), לחסוך את הזמן הזה. ופשוט לשנן היטב שהקוד שזה עתה סיימתם לכתוב, הוא באמת רחוק מלהיות שיא היצירה האנושית. הוא לא כל כך יפה, או חכם. יש בו עוד הרבה נקודות לשיפור, ויש כאן עוד כמה באגים שעדיין לא שמתם לב אליהם.
אני לא חושב שאפשר להיפטר לחלוטין מהערצת קוד, ולפי דעתי, זה גם נורמלי ובסדר לחלוטין. פשוט צריך לזכור ולנסות להוריד למינימום את הנזקים שהיא יכולה לגרום להם. סקפטיות היא דרך טובה. כי אם לומר את האמת, וכולנו הרי כבר יודעים את זה, רוב הסיכויים שמה שכתבתם אתמול בעבודה ... לא באמת עובד.