הפניות לפונקציות ואירועים בסי שארפ (delegates)


הפניה לפונקציות

דלגייטס

הבסיס

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

delegate string EncryptText (string Text);
string EncryptTextByReversing (string Text)
{
____char[] charArray = Text.ToCharArray();
____Array.Reverse(charArray);
____return new string(charArray);
}
string EncryptTextByIncrease (string Text)
{
____char[] charArray = Text.ToCharArray();
____for(int i = 0; i < charArray.Length; i++)
________charArray[i] = (char)(charArray[i] + 1);
____return new string(charArray);
}
EncryptText ET1 = EncryptTextByIncrease;
ET1("Hello There")

בקוד שבניתי בעצם הגדרתי,

  1. דלגייט בשם EncryptText שמוגדרת כפונקצייה המקבלת סטרינג, וגם מחזירה string. נעשה זאת באותה הצורה שנגדיר שפונקצייה תעשה זאת. הגדרות הפונקצייה - האובייקטים שלוקחת ומחזירה, נקראים בלשון מקצועית "חתימת הפונקצייה".
  2. פונקצייה להצפנת טקסט, EncryptTextByReversing, על ידי הפיכת סדר האותיות. חתימתה היא קבלת סטרינג, והחזרת אחד. כלומר זהה לדלגייט שהגדרנו.
  3. פונקצייה להצפנת טקסט, EncryptTextByIncrease, על ידי העלאת אינקדס הASCII של האותיות. חתימתה היא כמו של האחרונה.
  4. נגדיר עצם מסוג הדלגייט EncryptText בשם ET1. הגדרת עצם מסוג דלגייט כתובה בצורה זהה להגדרת עצם מקלאס מסויים, רק שנזין לתוך העצם פונקצייה מסויימת עם חתימה מתאימה. במקרה זה EncryptTextByIncrease.
  5. כעת, נוכל לקרוא למשתנה מסוג הדלגייט שהגדרנו, ET1, ולקרוא לו כמו שקוראים לפונקצייה. הפונקצייה שתופעל תהיה הפונקצייה שהוזנה לתוכו.

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

void PrintEncryptedTextBy (string Text, EncryptText EncryptionMethod)
{
____Console.WriteLine($"The Text - {Text}, Encrypted as - {EncryptionMethod(Text)}");
}
PrintEncryptedTextBy("Hello There",ET1);

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

כבר לא הבסיס

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

bool HasValidStringInside (string[] StrArr, Func<string,bool> Validator)
{
____foreach(string Str in StrArr)
________if(Validator(Str))
____________return true;
____return false;
}
HasValidStringInside(new string[] {"Kanye East","Tomer Ashtamker","Adi Biti"}, x => x.Contains("Shlomi"));

סוגים מוכנים מראש

אוקיי, יש פה כל מיני קונספטים חדשים, נעבור עליהם אחד אחד. ראשית, חתימת הפונקצייה שהגדרנו, HasValidStringInside, היא החזרה של בוליאני, וקבלה של מערך של סטרינגים ומשתנה מסוג Func<string,bool>. מה זה הסוג הזה? ומה הוא פותר? תדמיינו שיש לנו הרבה פונקציות עם חתימות שונות שנרצה ליצור משתנה המפנה אל כל אחת מהן, לדוגמא,

string SayHi () { return "Hi";}
int multiplyByOne (int Num) { return Num * 1;}
void PrintSomething () { Console.WriteLine(("Something"));}
____
delegate string Say ();
delegate int Multiply (int Num);
delegate void Print ();
____
Say Saying1 = SayHi;
Multiply Multiply1 = MultiplyByOne;
Print Print1 = PrintSomething;

מהר מאוד נוצר מצב לא אידיאלי שאנו נדרשים ליצור הרבה סוגי דלגייטס שונים לכלילת כל החתימות השונות (Say,Multiply,Print), כדי שנוכל לבסוף לאחסן הפניות לכל הפונקציות במשתנים שונים. כדי להמנע מהמצב, מוגדרים מראש בסי שארפ דלגייטס גנריים בהם נוכל להשתמש,

  • Action - דלגייט הבנוי להכיל פונקצייה הלא מחזירה ערך, כלומר void. לדוגמא משתנה המכיל הפניה לפונקצייה המקבלת שני ערכי int וסטרינג יהיה,
    Action<int, int, string> AnAction = //Insert Func Here

  • Func - דלגייט הבנוי להכיל פונקצייה המחזירה ערך, כך שהסוג האחרון המוכנס ל<> יהיה סוג המשתנה המוחזר. לדוגמא משתנה המכיל הפניה לפונקצייה המקבלת סטרינג ומחזירה bool (כמו בדוגמא הראשונית) יהיה,
    Func<string, bool> AnFunc = //Insert Func Here

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

Func<string> Saying1 = SayHi;
Func<int,int> Multiply1 = MultiplyByOne;
Action Print1 = PrintSomething;

סיכום ביניים

נחזור לקוד הראשוני, אז כמו שאמרנו הגדרנו פונקצייה שחתימתה הינה החזרת בוליאני, וקבלת מערך סטרינגים והפניה לפונקצייה (שחתימתה קבלת סטרינג והחזרת בוליאני). בפונקצייה נעבור על כל הסטרינגים במערך StrArr, ונזין אותם לפונקצייה שהופננו אליה, Validator. במידה וValidator החזיר true עבור ערך מסויים, נחזיר ישר גם true. ובמידה וערך זה לא הוחזר לאחר שעברנו על כל הסטרינגים, יוחזר false.
כעת נקרא לפונקצייה. נזין אליה מערך סטרינגים, ו... במקום שאמורה להיות ההפניה לפונקציה יש משהו מוזר אחר, x => x.Contains("Shlomi"). הדבר הזה נקרא ביטוי למבדה.

ביטויי למבדה ופונקציות אנונימיות

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

bool IsFirstLetterA (string Text) {return Text[0] == 'A';}
bool ContainsShlomi (string Text) {return Text.Contains("Shlomi");}
bool LongerThen3Chars (string Text) {return Text.Length > 3;}
____
Console.WriteLine(HasValidStringInside(new string[] {"Kanye East","Tomer Ashtamker","Adi Biti"}, IsFirstLetterA));

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

Func<string,bool> Validator = delegate(string Text) {return Text.Length > 3;};

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

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

Func<string,bool> Validator = Text => Text.Length > 3;

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

Func<string> SayHi = () => "Hi";
Func<int,int> SubtractThenMultiplyByOne = x => {x--; return x * 1;};
Action<string,string> PrintCombined = (s1,s2) => Console.WriteLine($"{s1}, {s2}");

אם נרצה,

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

אירועים

הרשמת פונקצייה

יכולת נוספת של משתנה המכיל הפניה לפונקצייה, היא היכולת שלו להכיל הפניות למספר פונקציות. לדוגמא,

void TurnOffTheLights() {Console.WriteLine("Turned off the lights");}
void CloseWindows() {Console.WriteLine("Closed Windows");}
____
Action GetOutOfTheHouse = TurnOffTheLights;
GetOutOfTheHouse += CloseWindows;
GetOutOfTheHouse += () => Console.WriteLine("Locked the door");
____
GetOutOfTheHouse()

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

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

הרשמה בין קלאסים

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

class TrainSignal
{
____public Action ATrainIsComing;
____public void ActivateSignal()
____{
________Console.WriteLine("Alert!");
________ATrainIsComing?.Invoke();
____}
}
class Car
{
____private string LicensePlate;
____public Car(TrainSignal TrainSignal1,string LicensePlate)
____{
________TrainSignal1.ATrainIsComing += StopCar;
________this.LicensePlate = LicensePlate;
____}
____void StopCar()
____{
________Console.WriteLine($"Car number {LicensePlate} stopped");
____}
}
____
TrainSignal TrainSignal1 = new TrainSignal();
Car Car1 = new Car(TrainSignal1, "BETTY32");
Car Car2 = new Car(TrainSignal1, "FFDDF45");
Car Car3 = new Car(TrainSignal1, "G4H6D41");
____
TrainSignal1.ActivateSignal();

ראשית, הגדרנו קלאס של רמזור רכבת, שמטרתו להודיע לכל המכוניות לעצור כשרכבת באה. הוא מכיל,

  1. משתנה מסוג Action בשם ATrainIsComing שלפי דפוס האירועים, מייצג את האירוע שרכבת באה. אליו נרשום את כל הפעולות שצריכות להתבצע ברגע שרכבת באה. במקרה זה, שכל המכוניות יעצרו.
  2. הפעולה ActivateSignal שתדפיס "אזהרה!" כשרכבת באה, ובמידה וATrainIsComing אינה נאל, כלומר שרשומה אליה לפחות פעולה אחת, תפעיל אותה בעזרת פעולת הInvoke() ששקולה להפעלה רגילה בעזרת () (אם אינכם מכירים את הפעולה ?. ממליץ לקרוא את מדריך בטיחות הנאל כאן).

לאחר מכן, הגדרנו את קלאס הרכב. המכיל,

  1. מאפיין מסוג סטרינג בשם LicensePlate המכיל את מזהה לוחית הרישוי.
  2. פעולה בונה, המקבלת עצם מסוג TrainSignal, וסטרינג לוחית הרישוי. הפעולה רושמת את פעולת עצירת המכונית, StopCar, שנסקור תכף, לאירוע הגעת הרכבת של עצם הTrainSignal. ובנוסף מגדירה את לוחית הרישוי שנשלחה, לזאת של הרכב.
  3. הפעולה StopCar לעצירת המכונית. מודיעה שהמכונית עצרה, ומדפיסה את מזהה לוחית הרישוי שלה.

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

Alert!
Car number BETTY32 stopped
Car number FFDDF45 stopped
Car number G4H6D41 stopped

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

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

הקונבנציה

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

המילה האירוע

נשים את המילה event בין סוג משתנה הדלגייט, לשם המשתנה. כך,

public event Action ATrainIsComing;

מה ששימוש במילה יתן לנו הוא,

  • לא ניתן להפעיל את האירוע מחוץ לקלאס שהוא שייך אליו.
  • לא ניתן להשתמש בפעולה = על מנת לאפס או לשנות את האירוע ישירות מחוץ לקלאס שהוא שייך אליו.

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

מנהל האירועים

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

void AFunction(object Sender, EventArgs e){}

כלומר,

  • קבלה של אובייקט בשם sender, אליו יוכנס האובייקט שהפעיל את האירוע.
  • משתנה בשם e מסוג EventArgs היכיל הפרמטרים נוספים הנרצה להעביר, בהמשך יוסבר בדיוק כיצד.

כעת נבנה תוכנה פשוטה, שתפעל כך - מסך עם הרכיבים,

  1. Label - שורת טקסט, בעלת המאפיינים צבע, וטקסט. כשניצור אותה שני הפרמטרים יאותחלו כ"None".
  2. Textbox - תיבת טקסט בה נזין צבע מסויים. שורת הטקסט תצבע בו במידה ונלחץ על כפתור כלשהו.
  3. Button - כפתור היכיל טקסט, כנלחץ עליו שורת הטקסט תקבל את טקסט הכפתור, ותצבע בצבע שהוזן בתיבת הטקסט.


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

class ButtonPressEventArgs:EventArgs
{
____public string Color;
____public ButtonPressEventArgs(string Color)
____{
________this.Color = Color;
____}
}
class Button
{
____public string Text;
____public Button(string Text)
____{
________this.Text = Text;
____}
____public event EventHandler<ButtonPressEventArgs> ButtonPressed;
____public void PressButton(Textbox Textbox1)
____{
________ButtonPressed(this,new ButtonPressEventArgs(Textbox1.Color));
____}
}

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

לאחר מכן, נגדיר את הקלאס Button שיכיל,

  1. מאפיין מסוג סטרינג היכיל את הטקסט על הכפתור בשם Text.
  2. פעולה בונה.
  3. אירוע בשם ButtonPressed מהסוג EventHandler<ButtonPressEventArgs>. במידה ונרצה להעביר לאירוע אובייקט מקלאס השונה מEventArgs, כפרמטרים הנוספים, נזין אותו בתוך ה<> כמו שעשיתי במצב זה. במידה ולא נדרש, לא נשתמש ב<> כלל ונרשום רק EventHandler.
  4. פעולה בשם PressButton להפעלת האירוע ButtonPressed. הפעולה תקבל אובייקט מסוג תיבת טקסט. בקריאה אליה האירוע יופעל וישלח אליו האובייקט הקורא בתור הsender (this) ואובייקט חדש מהסוג ButtonPressEventArgs, שיכלול את הצבע שהוזן לתיבת הטקסט. במידה ולא היינו רוצים להעביר פרמטרים נוספים, היינו מעבירים במקומו את הערך - EventArgs.Empty.

כעת נגדיר את קלאס הLabel,

class Label
{
____private string Text = "None";
____private string Color = "None";
____public Label() {}
____public void SetText(object Sender, ButtonPressEventArgs e)
____{
________this.Text = (Sender as Button).Text;
________this.Color = e.Color;
____}
____public override string ToString()
____{
________return $"The Color is: {this.Color}, And The Text Is :{this.Text}.";
____}
}

הקלאס יכיל,

  1. המאפיינים Text וColor, המייצגים את הטקסט בלייבל, וצבעו. מאותחלים כ"None".
  2. פעולה בונה.
  3. הפעולה SetText שתעדכן את טקסט הלייבל וצבעו. חתימתה מתאימה להרשמה לEventHandler שהגדרנו. הטקסט יילקח מאובייקט הsender, כלומר הכפתור שלחצנו עליו, והצבע מe.
  4. פעולת הדפסה של פרטי הלייבל, תשומש בבדיקות.

כעת, אחרי שהגדרנו את הקלאסים, נבצע את הפעולות,

//Init All Objects
Button Button1 = new Button("Boy");
Button Button2 = new Button("Houdy");
Textbox Textbox1 = new Textbox("Blue");
Label Label1 = new Label();
____
//Subscribe To Events
Button1.ButtonPressed += Label1.SetText;
Button2.ButtonPressed += Label1.SetText;
____
//First Print
Console.WriteLine(Label1);
____
//Press Button 1
Button1.PressButton(Textbox1);
Console.WriteLine(Label1);
____
//Press Button 2
Textbox1.Color = "Red";
Button2.PressButton(Textbox1);
Console.WriteLine(Label1);

כלומר,

  1. נגדיר אובייקטים של שני כפתורים, תיבת טקסט ולייבל.
  2. נרשום את פעולת שינוי הטקסט של הלייבל לאירוע הלחיצה של כל אחד מהכפתורים.
  3. נדפיס את הפרטים הראשוניים של הלייבל.
  4. נלחץ על כפתור מס' 1, ונשלח את תיבת הטקסט שהגדרנו. לאחר מכן נדפיס את פרטי הלייבל לאחר הלחיצה.
  5. נשנה את הצבע המוזן בתיבת הטקסט, נלחץ על כפתור מס' 2, נשלח את תיבת הטקסט שהגדרנו, ונדפיס את פרטי הלייבל.

והפלט שיתקבל יהיה,

The Color is: None, And The Text Is :None.
The Color is: Blue, And The Text Is :Boy.
The Color is: Red, And The Text Is :Houdy.

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




אין תגובות:

הוסף רשומת תגובה