לעיתים נרצה לבנות מחלקות אשר מאד דומות בתבניתם, ושונות רק בסוג הנתונים שהם מחזיקות.
דוגמאות:
נרצה מחלקה Point המייצגת נקודה במישור בעלות יכולות, כמו למשל להזיז את הנקודה, כאשר לעיתים נרצה שהאורדינאטות (x,y) של הנקודה יהיו מסוג int ולעיתים נרצה שיהיו מסוג double (או מכל סוג אחר – long, byte, decimal...)
מחלקה לניהול רשימה של נתונים (Collection) עם יכולת להוסיף ולמחוק איברים כאשר בכל פעם נרצה לשים שם נתונים אחרים
בעזרת הידע שיש לנו עד עתה נצטרך לבנות את המחלקות מספר פעמים, פעם אחת עבור כל טיפוס. אפשרות אחרת היא להשתמש בטיפוס object אך אפשרות זו לא מספיק טובה כי היא מחייבת אותנו לבצע casting בכל פעם שנרצה לעבוד עם המשתנה ובנוסף אנו עלולים לטעות ולהכניס למשתנה משתנה מטיפוס שלא התכוונו אליו והקומפיילר לא יתריע על כך, אלא נקבל את השגיאה רק בזמן ריצה ב- casting הלא חוקי.
Generics נותן פתרון לבעיה זו בכך שהוא מאפשר להגדיר גם את סוג המשתנה כפרמטר, כך שנוכל ביצירת האובייקט לקבוע מה יהיה הטיפוס עבור האובייקט המסוים ובאובייקט אחר נקבע טיפוס אחר. בשיטה זו לא נצטרך לבצע casting מפני שאנו מגדירים את הטיפוס ואם ננסה להכניס ערך מטיפוס לא נכון בטעות נקבל שגיאת קומפילציה.
מיד לאחר שם המחלקה לדוגמא:
class Point <T> { ... }
לאחר מכן נוכל להשתמש בפרמטר זה בתוך המחלקה:
class Point<T> { private T x; private T y;
public T X { get { return x; } set { x = value; } }
public T Y { get { return y; } set { y = value; } }
public Point(T x, T y) { X = x; Y = y; }
public override string ToString() { return string.Format("X = {0}, Y = {1}", X, Y); } }
כאשר ניצור את האובייקט נספק לו את הפרמטר של הטיפוס גם כן בסוגריים משולשים. לדוגמא Point כאשר ה- T הוא מסוג int:
Point<int> pInt = new Point<int>(10, 5);
שימו לב שבדוגמא הבאה שתי השורות האחרונות יגררו שגיאת קומפילציה מפני שטיפוס הנתונים מוגדר מראש ולא ניתן להכניס ערך מסוג אחר:
ניתן להגביל את הטיפוס שנהיה מוכנים לקבל כפרמטר גנרי באמצעות המילה השמורה where. בדרך-כלל נגביל את הטיפוס ל- class, struct או שנחייב אותו לממש ממשק (interface) מסוים.
הפונקציה בדוגמא הבאה, מקבלת 2 פרמטרים גנריים. הראשון חייב להיות struct היורש משני הממשקים IComparable, IFormattable. הפרמטר השני חייב להיות class היורש מהממשק ICloneable:
public void MethodWithConstrain<T, K>(T a, K b) where T : struct, IComparable, IFormattable where K : class, ICloneable { Console.WriteLine(a); Console.WriteLine(b); }
דוגמא נוספת – שימו לב ל- 11 ההעמסות של פונקצית Max של מחלקת Math:
בדרך-כלל, ניתן לחסוך מימושים שונים של אותה הפונקציה (בהעמסות שונות של סוגי טיפוסים) על-ידי שימוש במנגנון הג'נרי. כלומר, באמצעות Generics ,לדוגמא, נוכל ליצור פונקציה אחת שמקבלת טיפוס כלשהו שהוא בר-השוואה (שניתן להשוות בין ערכים שונים מאותו הטיפוס) ולחסוך את המימושים הנוספים המיותרים:
public static T Max<T>(T a, T b) where T : IComparable { if (a.CompareTo(b) > 0) return a;
return b; }
פונקציה זו יכולה להשוות בין שני פריטים כלשהם, מכל class/struct קיים/עתידי כל עוד הוא עונה לקריטריון הבא: יורש מהממשק IComparable.
ב- Net. קיימים מספר אוספים הנמצאים ב- namespace הנקרא System.Collections. להלן דוגמא למספר אוספים נפוצים:
ArrayList – מייצג אוסף דינמי של פריטים.
Hashtable – מייצג אוסף של זוגות ערכים (key/value) הממויין לפי ה- hash code של ה- key.
SortedList– מייצג אוסף של זוגות ערכים (key/value) הממויין לפי הערך של ה- key.
Queue – מייצג אוסף של פריטים בתור (הראשון שנכנס לאוסף, הוא הראשון שייצא – FIFO).
Stack – מייצג אוסף של פריטים במחסנית (האחרון שנכנס לאוסף, הוא הראשון שייצא – LIFO).
לשימוש באוספים אלו שני חסרונות עיקריים:
ביצועים – שימוש באוספים הנ"ל יכול לגרום לביצועים נמוכים כאשר אנו מכניסים לאוסף טיפוסים מסוג struct בשל הפעולות בזיכרון המתבצעות בזמן ריצה ע"י המנגנונים של Net. (בתהליך הנקרא boxing שבו מתייחסים ל- value-type כגון struct כ- reference type). כלומר, פגיעה במהירות זמן הריצה של התוכנית.
המרות – האוספים הרגילים אינם סוגים בטוחים (Typed-Safe) מיכוון שהם עובדים עם מחלקת Object. זה אומנם מקנה להם את היכולת להכיל כל טיפוס אך מנגד יוצר בעיה שהאוסף יכול לקבל טיפוסים שאנחנו לא רוצים שיהיו בו. וכמובן שכל שליפה של פריטים מהאוסף תאלץ אותנו להשתמש בהמרות מיותרות. הפיתרון הנהוג היה ליצור מחלקות אוסף משלנו ולממש אותם לטיפוס מסויים - פיתרון שמצריך המון עבודה.
וכאן נכנס לתמונה השימוש ב- Generics (הקיים החל מגירסה 2 של .Net): שימוש באוספים גנריים.
לכל האוספים הקלאסיים המוזכרים לעייל יש גירסה גנרית שניתן להשתמש בה. האוספים נמצאים ב- namespace הנקרא System.Collections.Generic (מופיע כברירת מחדל ב- using בראש של כל קובץ C# החל מגירסה 2).
אוספים אלו משתמשים בטיפוס גנרי מסויים אותו אנו מגדירים בקוד, כך ששני החסרונות שציינו לא רלוונטיים באוספים אלו.
לדוגמא, על-מנת ליצור אוסף גנרי דינמי (המסוגל לגדול/לקטון בהתאם לצורך) המתנהג כמו האוסף הקלאסי ArrayList אך ללא החסרונות שציינו, ניתן להשתמש באוסף הגנרי <>List :
List<int> numbers = newList<int>();
בדוגמא הנ"ל בחרנו לאסוף מספרים שלמים (int). כמובן שניתן לאסוף כל טיפוס נתונים שנרצה.
האוסף מכיל את כל הפונקציונליות הרלוונטית (הוספה/הסרת פריטים, פונקציות שירות כמו מיון וכו'):
נציגים(delegates) ואירועים (events) הינם נושאים מאד מרכזיים בעבודה ב- NET. . הם מאפשרים למפתח לבנות רכיב בלי להגביל את מי שמשתמש בו לפעולות מסוימות.
delegate הוא טיפוס המצביע על פונקציה.
תפקידו של ה- delegate הוא ליצור אובייקט המצביע על פונקציה. במקום להפעיל את הפונקציה בצורה הרגילה, נפעיל אותה דרך ה- delegate. היתרון של עבודה עם delegate הוא בהצבעה על פונקציה שאנו לא מכירים בזמן כתיבה של רכיב (מחלקה / פונקציה וכד') מסוים, ומי שיקבע איזו פונקציה תופעל הוא מי שמשתמש ברכיב.
להלן דוגמא שננתח אותה מיד:
namespace Delegates { delegate int Calc(int x, int y);
class Program { static int Add(int a, int b) { return a + b; }
static void Main(string[] args) { Calc c= new Calc(Add); int result = c(4, 6);
Console.WriteLine(result); } } }
הגדרת delegate מתבצעת באמצעות המילה השמורה delegate ולאחריה חתימה של פונקציה, כאשר שם "הפונקציה" הוא למעשה שם ה- delegate.
להלן הגדרת delegate בשם Calc:
delegate int Calc(int x, int y);
delegate יכול להצביע רק על פונקציות שחתימתן היא בדיוק כמו חתימת ה- delegate. ה- delegate (נציג) נקרא כך מיכוון שתפקידו "לייצג" פונקציות. בדוגמא שלנו ה- delegate יכול להצביע רק על פונקציות שמקבלות בדיוק שני פרמטרים מסוג int ומחזירות int. כלומר, הוא יוכל לייצג כל פונקציה שעונה לתנאים אלו בלבד – מקבלת שני מספרים שלמים (int) ומחזירה מספר שלם.
להלן דוגמא להצבעה על פונקציה בשם Add:
Calc c = new Calc(Add);
בכדי להפעיל את הפונקציה דרך ה- delegate יש להתייחס למשתנה שיצרנו כאילו הוא שם הפונקציה. בדוגמא שלהלן יודפס 10 (הסכום של 4,6):
int result = c(4, 6); Console.WriteLine(result);
Multicast delegate
delegate יכול להצביע גם על מספר פונקציות, ניתן לעשות זאת באמצעות האופרטור =+ לדוגמא:
namespace Delegates { delegate int Calc(int x, int y);
class Program { static int Add(int a, int b) { Console.WriteLine("Add"); return a + b; }
static int Sub(int a, int b) { Console.WriteLine("Sub"); return a - b; }
static void Main(string[] args) { Calc c; c = new Calc(Add); c += new Calc(Sub); int result = c(4, 6);
Console.WriteLine(result); } } }
בפלט התוכנית ניתן לראות שהופעלו 2 הפונקציות אך התשובה שהוחזרה היא התשובה של הפונקציה האחרונה שהופעלה:
עד כאן הבנו איך יוצרים ומשתמשים ב- delegate להלן דוגמא הממחישה למה צריך delegate (מיד ננתח אותה):
namespace SortWithDelegate { public delegate int CompareDeleg(int a, int b);
class Program { static int[] arr;
static int CompareAsc(int x, int y) { return x - y; }
static int CompareDesc(int x, int y) { return y - x; }
static void Sort(CompareDeleg compareMethod) { for (int i = 0; i < arr.Length - 1; i++) for (int j = i + 1; j < arr.Length; j++) if (compareMethod(arr[i], arr[j]) > 0) Replace(i, j); }
private static void Replace(int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
static void PrintArr() { foreach (int num in arr) Console.Write(num + ",");
בתוכנית זו ישנה פונקציה בשם Sort שתפקידה למיין מערך מסוג []int. חישבו שהיינו רוצים למיין את המערך בצורות שונות פעם בסדר עולה ופעם בסדר יורד, עם הידע שיש לנו עד היום היינו צריכים לבנות 2 פונקציות, אחת למיון עולה ואחת למיון יורד. ואם בעתיד היינו רוצים שיטת מיון אחרת (חישבו למשל על תאריכים שאפשר למיין בשיטות שונות: שנים, רבעונים, חודשים ועוד).
הרעיון הוא שהפונקציה Sort תקבל את המערך ו- delegate בשם CompareDeleg שיטפל בצורת המיון, כך שהפונקציה לא תגביל לסוג מיון מסוים אלא תיתן למי שמפעיל אותה לקבוע את סוג המיון.
ה- delegate בנוי כך שהוא מקבל שני מספרים (int) ומחזיר מספר המייצג מי יותר גדול בצורה הבאה:
מספר חיובי – המספר הראשון יותר גדול
מספר שלילי – מספר השני יותר גדול
אפס – המספרים שווים
כעת כאשר נרצה להפעיל את הפונקציה נשלח אליה את המערך וכתובת של פונקציה אשר מגדירה את צורת המיון ובכל פעם נוכל לשלוח צורת מיון אחרת. ניתן לראות בדוגמא שישנן 2 פונקציות שכל אחת מהן מייצגת מיון בצורה אחרת:
CompareAsc – למיון עולה
CompareDesc – למיון יורד
בכל הפעלה של הפונקציה Sort אנו שולחים כתובת של פונקציה אחרת ולאחר מכן מדפיסים את המערך. בפלט התוכנית ניתן לראות שבכל הדפסה סדר המיון הוא שונה (המצב הראשוני, עולה, יורד):
נשלח ב-27/7/2011 11:12
מדריך #C מתקדם – אירועים - Events
event – הודעה שאובייקט שולח לאובייקטים אחרים.
event הינו למעשה מקרה פרטי של delegate. הרעיון של event הינו להצביע על פונקציה והוא מספק מספר הגבלות אשר לא קיימות ב- delegate רגיל:
ניתן להגדיר event רק כחבר מחלקה (member) ולא ניתן בתוך פונקציה או כפרמטר לפונקציה
ניתן להפעיל event רק באותה מחלקה בה הוא נכתב
ניתן להוסיף ולהוריד פונקציות מ- event באמצעות =+, =- (כמו multicast delegate), אך לא ניתן לדרוס את מה שיש שם באמצעות =
הגבלות אלו מספקות ביטחון גבוה יותר, אשר מונע טעויות כמו דרישה של הפונקציות אליהם מצביע ה- event, ולכן אם לא נצטרך משהו שסותר הגדרות אלו (כגון פרמטר לפונקציה), נעדיף להשתמש ב- event.
בכדי להגדיר event צריך תחילה להגדיר delegate, לדוגמא:
public delegate void SpeedDelegate(int newSpeed);
לאחר מכן יש להגדיר משתנה מסוג ה- delegate בצרוף המילה השמורה event:
public event SpeedDelegate SpeedChanged;
רישום פונקציה ל- event (הצבעה) והפעלת ה- event הינם בדיוק כמו ב- delegate (למעט ההגבלה של =).
להלן דוגמא שמיד ננתח:
public delegate void SpeedDelegate(int newSpeed);
class Car { public event SpeedDelegate SpeedChanged;
private int speed;
public int Speed { get { return speed; } }
public void AddSpeed(int delta) { speed += delta; if (SpeedChanged != null) SpeedChanged(speed); } }
class Program { static void Main(string[] args) { Car c = new Car();
//רישום הפונקציה לאירוע c.SpeedChanged += new SpeedDelegate(PrintSpeed);
בדוגמא זו ישנה מחלקה בשם Car המאפשרת שינוי מהירות הרכב. למחלקה event בשם SpeedChanged אשר מודיע כאשר השתנתה מהירות הרכב. בתוכנית רשמנו את הפונקציה PrintSpeed ל- event ובכל פעם שנוסיף מהירות (5 פעמים במקרה שלנו) ה- event יפעיל את הפונקציה הזו שמדפיסה את המהירות החדשה.
פלט התוכנית:
השימוש באירועים נפוץ ב- .NET בעיקר בסביבות עשירות ב- GUI, למשל, WinForms, WPF, ASP.NET וכו'. השימוש באירועים בסביבות אלו מאפשר, למשל, להפעיל פונקציות המתבצעות כתוצאה מפעולה של המשתמש. לדוגמא, אירוע של לחיצה על כפתור יגרור תגובה בצורה של פונקציה המסוגלת לבצע אוסף של פעולות.
public partial class MyForm : Form { Button myButton;
public MyForm() { InitializeComponent(); //הרישום לאירוע myButton.Click += new EventHandler(myButton_Click); }
//הפונקציה המתבצעת כתוצאה מהפעלת האירוע private void myButton_Click(object sender, EventArgs e) { MessageBox.Show("Test"); } }
אנחנו לא יכולים לדעת מראש (וגם לא גם צריכים לדעת) האם המשתמש יחלץ על הכפתור, ואם כן, מתי זה יקרה וכמה פעמים. אבל בטוח שבכל פעם שהמשתמש יחלץ על הכפתור האירוע Click יתעורר ויפעיל את הפונקציה או הפונקציות שנרשמו אליו מראש.
נשלח ב-27/7/2011 11:12
מדריך #C מתקדם - Attributes - תכונות
Attributes הן תכונות שניתן לתת לאלמנט תכונתי כלשהו כגון: מחלקה, פונקציה, assembly ועוד. התכונה הינה למעשה מידע נוסף שאנו מספקים לאלמנט אשר בד"כ גורר אחריו התנהגות כלשהי. בשלב זה נכיר את התחביר ונלמד על מספר attributes פשוטים כאשר בהמשך נלמד על סוגים רבים של attributes בנושאים הרלוונטיים.
כל attribute שניצור וכל attribute שכבר קיים ב-NET., הוא למעשה מחלקה (class) היורשת בצורה ישירה או עקיפה ממחלקה הנקראת Attribute.
את ה- attribute נכתוב באמצעות סוגריים מרובעים מעל האלמנט אליו נרצה לשייך אותו:
[AttributeName] Some element
לדוגמא, attribute בשם Obsolete מעל הפונקציה Print:
העמסת אופרטורים (Operator Overloading) מאפשרת לתת משמעות רצויה לאופרטור כאשר הוא יופעל על המחלקה שלנו. המטרה היא לאפשר הפעלת אופרטורים על המחלקה שלנו או מתן מימוש שונה לאופרטורים שכבר ניתן להפעיל (כגון ==).
התחביר להעמסת אופרטורים מתחלק ל- 2 סוגים:
אופרטור אונרי (Unary) – הפועל רק על אופרנד אחד, כגון ++a
אופרטור בינארי (Binary) – הפועל על 2 אופרנדים, כגון a+b
העמסת אופרטור אונרי:
public static Circle operator ++(Circle c) { //מימוש }
מבנה כותרת ההעמסה של אופרטוראונרי יהיה בדיוק בסדר הבא:
publicstatic
הערך המוחזר (Circle בדוגמא הנ"ל)
מילת המפתח operator
האופרטור שרוצים להעמיס (++ בדוגמא)
פרמטר מטיפוס המחלקה ( Circle c בדוגמא)
העמסת אופרטור בינארי:
public static Circle operator +(Circle c, int x) { //מימוש }
מבנה כותרת ההעמסה של אופרטורבינארי יהיה בדיוק בסדר הבא: