Oct 07
זהו חלקו השני של המאמר "פינות" ב-VBScript
בואו נחזור לדוגמה מההקדמה בחלק הראשון של המאמר (בנוגע להעברת אובייקטים כפרמטרי ByVal). בבחינה מחודשת, אנו עשויים לחשוב שאין הבדל בין שיטות ההעברה השונות (ByVal ו-ByRef) של אובייקטים לפונקציות ופרוצדורות. הרי כל שינוי שיתבצע באובייקט בתוך הפונקציה ישפיע על משתנים המצביעים לאובייקט גם מחוץ לפונקציה (היות וכל המשתנים מצביעים לאותו אובייקט ראשי בזכרון) – כך שאין כל שינוי אם אנו מעבירים את המצביע כ-Ref או כ-Val. אבל, בפועל יש הבדל אחד בין שתי השיטות שמתברר כמשמעותי ביותר (ואף קריטי במקרים מסויימים).
הבא נבחן את שתי הפרוצדורות הבאות:
Sub DoIt(ByVal oDictionary)
oDictionary.Add "Did It", True
Set oDictionary = Nothing
End Sub
Sub DoIt2(ByRef oDictionary)
oDictionary.Add "Did It 2", True
Set oDictionary = Nothing
End Sub
Set oDic = CreateObject("Scripting.Dictionary")
Call DoIt (oDic)
MsgBox oDic.Count ‘What will this print?
Call DoIt2 (oDic)
MsgBox oDic.Count ‘What will this print?
oDictionary.Add "Did It", True
Set oDictionary = Nothing
End Sub
Sub DoIt2(ByRef oDictionary)
oDictionary.Add "Did It 2", True
Set oDictionary = Nothing
End Sub
Set oDic = CreateObject("Scripting.Dictionary")
Call DoIt (oDic)
MsgBox oDic.Count ‘What will this print?
Call DoIt2 (oDic)
MsgBox oDic.Count ‘What will this print?
מה יקרה פה בעצם? בפרוצדורה הראשונה, oDictionary יהיה עותק של המצביע במשתנה oDic, וכפי שראינו, שניהם מצביעים לאותו אובייקט המילון בזיכרון. הערך החדש יתווסף לאובייקט המילון, והשינוי הזה ישוקף באמצעות אובייקט oDic (מחוץ לפונקציה).
ומה באשר לפקודה Set oDictionary=Nothing? אנו קוטעים את הקשר בין oDictionary לבין אובייקט המילון בזיכרון. אולם חשוב לזכור ש-oDic עדיין מצביע לאובייקט המילון, היות ו-oDictionary הוא משתנה מצביע עצמאי וחדש. כלומר, פקודת Set oDictionary=Nothing משפיעה על oDictionary כמשתנה, לא כ"צינור", ולכן אין לה שום השפעה על אובייקט המילון עצמו או על המשתנה oDic. כך שכאשר אוסף האשפה יסרוק את הזיכרון לאובייקטים להריסה, הוא ידלג על אובייקט המילון (היות ועדיין יש מצביע פעיל שמצביע עליו). ולכן פקודת MsgBox oDic.Count תודיע על הערך 1.
ומה קורה בפונקציה השניה? היות ו-oDic הועבר ByRef, המשתנה המקומי oDictionary הוא שיקוף שלו בתוך הפונקציה, ולכן אין ספק שהערך החדש במילון ישוקף גם מחוץ לפונקציה. ומה קורה בפקודה Set oDictionary=Nothing? ובכן, כפי שציינו, oDictionary אינו העתק של oDic אלא ממש oDic עצמו – כלומר הפקודה Set oDictionary=Nothing בעצם זהה לפקודה Set oDic=Nothing. ועכשיו אנחנו בצרה רצינית.
הפקודה Set oDictionary=Nothing למעשה קטעה את הקשר בין המשתנים היחידים שהחזיקו מצביע לאובייקט המילון, לאובייקט המילון עצמו. כלומר, נתקענו עם אובייקט שחי בזיכרון וצורך משאבי מערכת, בלא שיש לנו דרך כלשהי לגשת אליו ולהשתמש בו. אם אנחנו ממש ברי מזל, אובייקט המילון לא החזיק אף מצביע לאובייקט חיצוני, ואז אוסף האשפה יהרוס אותו כאשר הוא יסרוק את הזיכרון. המידע במילון יאובד לנצח, אבל לפחות משאבי המערכת ישוחררו.
אין לזלזל בחשיבות שבשחרור משאבי מערכת. רק תארו לעצמכם את המשמעות באובייקט Scripting.FileSystemObject שנועל קובץ כל עוד הוא קיים, או אובייקט DB המחזיק קשר פתוח למאגר הנתונים. ישנן תוצאות חמורות בהרבה מאובדן מידע, והעברת מצביעים כ-ByRef עלולה לגרום להן. ורק על מנת להשלים את התמונה, פקודת MsgBox oDic.Count האחרונה תזרוק שגיאה, היות ו-oDic כבר לא מצביע לאף אובייקט התומך בפעולת .Count . כך ששוב, חשוב להזהר שלא להשאב לקטסטרופות ByRef מסוג זה.
החלק הזה עשוי להראות כפינה הכי זניחה וטכנית בכל המאמר, אולם לדעתי היא קריטית, וכאשר היא מתרחשת, VBScript יציג התנהגות כל-כך לא עקבית, שאם לא תכירו את הנושא, אתם עלולים להשתגע. ראשית, נציג את הדרכים השונות בהן ניתן לקרוא לפרוצדורות ופונקציות (כן, יש יותר מאחת). ניקח פרוצדורה מוכרת כדוגמה:
Sub DoIt2(ByRef oDictionary)
Set oDictionary = Nothing
End Sub
Set oDictionary = Nothing
End Sub
בדוגמה קודמת ראינו מה עתיד לקרות בעת קריאה לפרוצדורה הזו. הפרוצדורה תקטע את הקשר בין המצביע שנשלח אליה כפרמטר ובין אובייקט המילון המצוי בזיכרון. אולם מסתבר כי הדברים מורכבים יותר, ואנו עתידים לראות כי אופן הקריאה לפרוצדורה יכול להשפיע על התוצאה. העתיקו את שתי הדרכים ל-QTP ונסו בעצמכם:
Set oDic = CreateObject("Scripting.Dictoinary")
DoIt2(oDic)
Msgbox oDic.Count ‘Surprisingly, this will output 0
Call DoIt2(oDic)
Msgbox oDic.Count ‘Only now will oDic be nullified, and we’ll get an error.
DoIt2(oDic)
Msgbox oDic.Count ‘Surprisingly, this will output 0
Call DoIt2(oDic)
Msgbox oDic.Count ‘Only now will oDic be nullified, and we’ll get an error.
מה בדיוק קורה פה? נראה שאף על פי שציינו מפורשות על העברת פרמטר כ-ByRef, בקריאה הראשונה oDic עבר כ-ByVal. אין בכך שום הגיון – אם הקריאה הייתה לא חוקית, היא הייתה אמורה פשוט להיכשל. אבל במקום להיכשל, VBScript עושה משהו גרוע עוד יותר – מתנהג באופן לא עקבי.
אחרי מחקר רשת מעמיק, לבסוף מצאתי את התשובה בבלוג של Eric Lippert - Fabulous Adventures In Coding. מסתבר שבעוד ש-DotIt2 oDic ו-Call DoIt2(oDic) מתנהגים באופן זהה, מסתבר ש-DotIt2(oDic) שונה באופן מהותי. האחרון ישלח את oDic כ-ByVal ללא קשר לאופן הגדרת הפרמטר בפונקציה / פרוצדורה. בתמצות, העקרון הבסיסי המושל בהתנהגות VBScript בנושא: פרמטרים הינם תמיד ByRef, חוץ משני מקרים: אם המשתמש ציין בחתימת הפונקציה כי הפרמטר צריך לעבור ByVal, או אם המשתנה נשלח לפונקציה כשהוא עטוף בסוגריים "מיותרים".
אבל אם זה המצב, למה הסוגריים ב-DotIt2(oDic) נחשבים ל"מיותרים"? נראה כי VBScript מפענח את הקריאה DotIt2(oDic) כקריאה ללא סוגריים, כאשר הפרמטר הראשון (oDic) עטוף בסוגריים מיותרים. אני לא באמת מתיימר להבין למה VBScript מפענח את הקריאה באופן הזה (אם כי Eric מסביר את העניין), אבל לפחות עכשיו אתם יודעים על החוקיות, ותוכלו להתחמק מהמקרים הבעייתיים. הדרך לפצות על סוגריים "מיותרים" היא להשתמש במילה Call, כפי שרואים בדוגמה Call DoIt2(oDic).
להלן דוגמאות קצרות למקרים הבעייתיים והלא בעייתיים (נלקחו מהבלוג של Eric). בדוגמאות הנ"ל יש להניח ש-DoIt מקבלת משתנה אחד או שניים בהתאם לדוגמה.
לא בעייתי (כל הפרמטרים יועברו בהתאם למה שהוגדר בפונקציית DoIt):
Doit x
Call Doit (x)
Call Doit (x, y)
DoIt x, y
Var = DoIt(x)
Var = DoIt(x, y)
Call Doit (x)
Call Doit (x, y)
DoIt x, y
Var = DoIt(x)
Var = DoIt(x, y)
בעייתי (Y יועבר בהתאם למה שהוגדר ב-DoIt, X יועבר כ-ByVal בכל מקרה):
DoIt(x)
Call DoIt ((x))
z = DoIt((x))
DoIt (x), y
DoIt ((x)), y
Call DoIt ((x), y)
z = DoIt ((x), y)
Call DoIt ((x))
z = DoIt((x))
DoIt (x), y
DoIt ((x)), y
Call DoIt ((x), y)
z = DoIt ((x), y)
בשני חלקי המאמר ראינו איך "פינות" שנראות כחסרות משמעות עלולות להוביל לבאגים חבויים בזרימת הקוד ותוצאות הפעולות בתסריט. מעבר לכך, ראינו כי התנהגות התסריט עלולה להיראות לא עקבית לחלוטין, עובדה ההופכת על מלאכת ה-Debug והמעקב לכמעט בלתי אפשרית. אני מקווה כי לאחר שהבנתם את החוקים המושלים ב-VBScript במקרים אלו, תוכלו לטפל בהם בקלות יחסית.
שמור את הקטע לשימוש עתידי
הדפס את הפוסט
A PDF Version Of This Post



October 7th, 2007 at 8:39 pm
[…] עם קבצי PDF - פרק חדש מ-Scripting Quicktest Professional “פינות” ב-VBScript - חלק 2 Oct […]
August 2nd, 2008 at 1:00 am
ובכן, כדי לענות על השאלה ששאלת בכיתה (אני מקווה שהבנתי נכון את המאמרים, שלך ושל אריק ליפרט):
כאשר מגדירים משתנה פונקציה כ-ByRef אנחנו למעשה יוצרים שידוך בינו, המשתנה הלוקלי של הפונקציה, למשתנה הגלובלי שאיתו אנחנו בצענו את הקריאה לפונקציה. הם נהפכים להיות אחד ויחיד, כל פעולה שתעשה על משתנה הפונקציה הלוקלי תשפיע על המשתנה הגלובלי.
עכשיו… כדי להסביר את ההבדל בין התוצאות עם הסוגריים ובלי הסוגריים בקריאה עצמה:
mami (oDictionary) לעומת mami oDictionary:
כאשר קוראים עם סוגריים נקבל תוצאה: 1. הסיבה היא, לפי מה שהבנתי, שבהעדר המילה הספציפית Call לפני הקריאה ההתייחסות של VBS במקרה הזה היא שהסוגריים מציינים את המשתנה(!) הראשון בפונקציה (ולא את רשימת המשתנים) ולכן הם מעבירים אותו כ-ByVal. עכשיו, אם הוא הועבר ByVal הוא למעשה רק העתק(!) של המשתנה המקורי, אי-אפשר לשנות את המקור דרך הפונקציה, זו תהיה חריגה מגבולות הפונקציה ולכן מה שקורה הוא שהמקום בזיכרון אליו המשתנה הפנה מתמלא בערך, הקשר בין המשתנה הלוקלי(!) למקום בזיכרון נמחק אבל המקום עצמו עדיין קיים כי יש אליו מפנה: המשתנה הגלובלי ולכן אוסף הזבל לא הורס אותו.
מאידך, כאשר מורידים את הסוגריים זה קריאה “חוקית” של פונקציה שהוגדרה(!) כמעבירה את הנתונים ByRef.
מה שקורה הוא שהמשתנה הגלובלי והמשתנה הלוקלי משודכים יחדיו ולכן כאשר הפונקציה הורסת את הקשר בין המשתנה הלוקלי למקום בזיכרון שאליו הוא מצביע אין יותר מצביעים למקום ההוא בזיכרון בכלל מפני שהמשתנה הלוקלי הוא בעצם הגלובלי ולהיפך.
כאשר הפונקציה מסתיימת אוסף הזבל מגלה מקום בזיכרון בלי מצביעים והורס אותו ולכן הודעת השגיאה.
מקווה שהבנתי נכון את המאמרים.
(ואם לא… אנא מחקו את התגובה בהקדם האפשרי כדי שלא תטעה אחרים).
אגב, האם הבעיה הזו היא מה שידוע כבעיה של Virtual constructor?