רשימת תפוצה

חפש באתר:

Loading

פייסבוק
5to9-תמונות מ
שיחות בפורום

 

 

« מחפשים כותבים | Main | תמונות מאירוע ההשקה של- iloop »
יום שלישי
ספט292009

Objective-C tutorial, part 2

במאמר הראשון למדנו על התחביר של אובג'קטיב סי. בחלקו השני של המאמר נתעמק מעט יותר ביכולות הדינאמיות של השפה. חלק מהדוגמאות במאמר זה מתייחסות לסי פלוס פלוס (c++), כדוגמה לשפה סטטית ולשם הקיצור מוצגים הסברים שהם מעט פשטניים ביחס למגבלות השפה. כוונת המאמר איננה לדבר סרה בשפה הפופולארית אלא להדגים, על דרך הניגוד, את יתרונות הגישה הדינאמית של אובג'קטיב סי.

דינאמיות

אובג'קטיב סי נחשבת לשפה דינאמית. מה משמעות הדבר? פורמלית, הדינאמיות של שפת תכנות מתייחסת למרחב הפעולה שמשאיר המהדר (compiler) לזמן הריצה (runtime) מכאן נובע גם שהדינאמיות איננה הגדרה מוחלטת אלא יחסית. כל שפה נמצאת במקום מסויים על הציר שבין דינאמיות לסטטיות.

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

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

מידת הדינאמיות של שפה היא עניין של איזון עדין בין הרצון בגמישות ונוחות לרצון לקוד מהיר ואמין המתאים לישומים מסחריים (בניגוד לישומים אקדמאיים, למשל). סוד קסמה של אובג'קטיב סי טמון במידה רבה באיזון שהיא משיגה בין דינאמיות לסטטיות.

אובייקטים אנונימיים

כפי שהוזכר במאמר הקודם, ההתיחסות (reference) לאובייקטים באובג'קטיב סי נעשית תמיד באמצעות מצביע (pointer). נניח למשל שיש לנו כיתה הקרויה Person. נוכל להתייחס אליה באמצעות מצביע שיוגדר כך:

Person* myPerson;

חשוב לזכור שעצם ההכרזה על myPerson לא מגדירה אובייקט מסוג Person אלא רק מצביע על אובייקט כזה ועד שלא נבצע השמה (assignment) למשתנה, לא נוכל לשלוח אליו הודעות. ההשמה יכולה להיות מאובייקט קיים או אובייקט חדש שניצור. איזה מין אובייקטים נוכל להשים למשתנה? כל אובייקט מסוג Person או מאחת מתתי-הכיתות של Person (אם ישנן).

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

id myPerson;

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

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

@interface Array {

/* some sort of mechanism for actually storing the pointers */

}

- (unsigned)count;

- (void)addObject:(id)objec;

- (id)objectAtIndex:(unsigned)index;

@end

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

הודעות ומתודות

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

Person* myPerson = new Person;

myPeson->walk();

כאשר המהדר מהדר את שורות הקוד הללו הוא מתרגם אותם לקפיצה לכתובת המתודה walk בזכרון (בפועל ההתרחשות מעט יותר מתוחכמת אך לא שונה באופן עקרוני). מכיוון שכך על המהדר לדעת בזמן ההידור היכן בדיוק ממוקמת walk ומכאן שהוא חייב גם לדעת מהי הכיתה של האובייקט ואיך נראה הקוד שלה. בניגוד לשם המתודה, כתובת המתודה בזכרון היא פרט מידע שרירותי לחלוטין הנקבע ברוב המקרים באופן אוטומטי לחלוטין שאין לכותב הקוד כל שליטה בו. דבר זה מוביל למצב המעניין הבא:

נניח למשל שהגדרנו שתי כיתות בסי פלוס פלוס. הראשונה היא כיתה המגדירה אדם אשר יודע לדבר וללכת:

class Person {

public:

void talk();

void walk();

};

כיתה שניה מגדירה כלב היכול לנבוח וללכת:

class Dog {

public:

void bark();

void walk();

};

נוכל עכשיו לכתוב, למשל:

Person* myPerson=new Person;

Dog* myDog=new Dog;

 

myPerson->walk();

myDog->walk();

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

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

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

@interface Person {

}

- (void)talk;

- (void)walk;

@end

 

כיתת הכלבים תראה כך:

@interface Dog {

}

- (void)bark;

- (void)walk;

@end

נניח גם שיש לנו כיתת מערך כפי שהגדרנו קודם:

@interface Array {

...

}

- (unsigned)count;

- (void)addObject:(id)objec;

- (id)objectAtIndex:(unsigned)index;

@end

נוכל ליצור מערך ולהכניס אליו אובייקטים משני הסוגים:

Array* myArray=[[Array alloc] init]; // 1

 

for (i=0; i < 5; i++) { // 2

id anObject; // 3

anObject=[[Dog alloc] init]; // 4

[myArray addObject:anObject]; // 5

anObject=[[Person alloc] init]; // 6

[myArray addObject:anObject]; // 7

}

(לטובת הבהירות הושמט כאן מעט קוד הנוגע לניהול זכרון). הלולאה חוזרת 5 פעמים ובכל פעם יוצרת אובייקט Dog (שורה 4), מוסיפה אותו למערך (שורה 5), יוצרת אובייקט Person (שורה 6) ומוסיפה אותו למערך (שורה 7).

עכשיו נוכל לעבור על המערך ולשלוח הודעת walk לכל אחד מהאובייקטים המאוכסנים בו:

for (i=0; i < [myArray count]; i++) // 1

[[myArray objectAtIndex:i] walk]; // 2

מאחר וobjectAtIndex מחזירה id ניתן לשלוח לערך החזרה שלה כל הודעה שנרצה. 

 

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

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

עוצמה ואחריות

היכולת לשלוח כל הודעה לכל אובייקט מעניקה לנו עוצמה רבה אך עם העוצמה באה גם סכנה גדולה. מה קורה אם נשלח לאובייקט הודעה שאיננו מכיר? מה שקורה הוא שמורמת חריגה (exception), כלומר - שגיאה בזמן ריצה. אם ניקח את הקוד שכתבנו זה עתה, מספיק שנכניס לתוך המערך אלמנט מכיתה שאיננה Dog או Person ושאיננה מיישמת את walk כדי לגרום להרמת חריגה. כמובן שהקוד שכתבנו הוא פשוט וקצר מספיק כדי שנוכל לוודא שלעולם לא נכניס אובייקט השייך לכיתה אחרת לתוך המערך אך כמובן שקשה להקפיד על הכללים הללו כאשר הקוד גדל והופך מסובך יותר. יתר על כן, יתכן מצב בו נשנה את כיתת Dog או Person כך שלא יכללו את walk או שנוסיף למתודה פרמטר או נשנה את שמה גם במצב כזה לא נקבל הודעת שגיאה בזמן ההידור וביצוע הקוד יגרום להרמת חריגה בזמן הריצה.

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

for (i=0; i < [myArray count]; i++) {

id currentObject=[myArray objectAtIndex:i];

if ([currentObject respondsToSelector:@selector(walk)])

[currentObject walk];

}

אנו בודקים אם אובייקט יכול להגיב להודעה באמצעות המתודה respondsToSelector. המתודה הזאת מקבלת כפרמטר ערך מסוג SEL (קיצור של selector) והשימוש במבנה:

@selector(methodName)

הוא הדרך שלנו לתרגם את שם המתודה בקוד לערך מסוג SEL. מתודה המקבלת פרמטר תקבל נקודותיים בסוף השם, כך שהביטוי:

@selector(methodName:)

הוא המתאים למתודה המוגדרת כך:

- (void)methodName:(id)parameter;

בשביל מתודה בעלת שני פרמטרים:

- (void)methodName:(id)parama anotherParameter:(id)paramb;

 

נכתוב:

@selector(methodName:anotherParameter)

וכן הלאה.

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

ניקח לדוגמה משחק ובו כל מיני עצמים שונים (חיות, אנשים עצים ואבנים) אשר מסוגלים לכל מיני פעולות. חלק מהעצמים יכולים ללכת, חלק לא, חלק יכולים להלחם, חלק לא וכן הלאה. נניח גם שהמשחק מתנהל באמצעות מתן פקודות לעצם הבחור. כלומר, קודם אנו בוחרים בעצם כלשהו ולאחר מכן אנו נותנים לעצם פקודה באמצעות בחירה בתפריט או לחיצה על כפתורים. כאשר העצם הבחור איננו יכול לקיים פקודה מסויימת האפשרות לתת את הפקודה צריכה להיות בלתי מאופשרת - פריט התפריט או הכפתור נראים אפורים. הבה נראה כיצד אפשר יהיה ליישם כזאת פונקציונליות באמצעות respondsToSelector. נתמקד כמובן רק בפונקציה אחת (walk) אבל אין כל בעיה להרחיב זאת לכל מספר של פונקציות שרוצים.

ראשית הנה ההכרזה על הכיתה הראשית:

@interface GameController {

id selection; // 1

Button* walkButton; // 2

}

- (void)setSelection:(id)anObject; // 3

- (void)walkButtonAction; // 4

@end

בשורה 1 אנו מגדירים מצביע אל העצם הבחור. הסוג id מאפשר לנו להצביע על כל סוגי העצמים. שורה 2 מגדירה חיבור אל הכפתור בממשק, שורה 3 מגדירה מתודה לה קוראים כאשר רוצים לקבוע את העצם הבחור. בשורה 4 מוגדרת מתודה לה קוראים כאשר לוחצים על הכפתור.

לא נדון כאן בכל הקוד אשר דואג שwalkButton אכן יחובר לכפתור הממשי בממשק ונתמקד בשתי המתודות שהגדרנו. ראשית setSelection קריאה למתודה הזאת צריכה לבצע שני דברים, ראשית היא צריכה לקבוע את הערך של selection לאובייקט שהועבר כפרמטר ושנית היא צריכה לכוונן את הממשק לבחירה החדשה באמצעות קריאה לadjust:

- (void)setSelection:(id)anObject {

selection=anObject;

[self adjust];

}

 

המתודה adjust היא לב העניין. צריך לכוונן את מצב הכפתור כך שיהיה מאופשר כאשר העצם הבחור יכול ללכת ובלתי מאופשר כאשר הבחירה איננה יכולה ללכת:

- (void)adjust { // 1

if ([selection respondsToSelector:@selector(walk)]) // 2

[walkButton setEnabled:YES]; // 3

else // 5

[walkButton setEnabled:NO]; // 6

}

בשורה 2 אנחנו בודקים האם האובייקט הבחור יכול להגיב להודעה walk, אם כן, אנחנו הופכים את הכפתור למאופשר (שורה 2), אם לא, הכפתור נהיה בלתי מאופשר (שורה 5).

לסיום מתודת walkButtonAction אשר נקראית כאשר לוחצים על הכפתור (הקוד שאחראי על החיבור בין הכפתור לקריאה למתודה הזאת לא נכלל כאן מטעמי קיצור)

- (void)walkButtonAction {

[selection walk];

}

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

סיכום

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

 

PrintView Printer Friendly Version

EmailEmail Article to Friend

Reader Comments (4)

תודה על הפוסט

ג, ספטמבר 29, 2009 | Unregistered Commenterאורן

תודה! הכתבה ברורה ומסבירה את הנקודות החשובות.
מתי ההמשך? :)

ב, דצמבר 7, 2009 | Unregistered Commenterסשה

תודה

א, פברואר 7, 2010 | Unregistered CommenterItay

ההסבר הכי ברור והכי מדויק שנתקלתי בו.

ג, יולי 19, 2011 | Unregistered Commenterהראל

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>