91. System Linq

ראינו בפעמים הקודמות קצת מהן Extension Methods ומה אפשר לעשות איתן.

בנוסף, בפעם האחרונה, גם ראינו שאפשר לשלוח delegate לExtension method לכל מיני שימושים.

בFramework 3.5 קיבלנו namespace שלם שנקרא System.Linq עם כל מיני Extension Methods שעובדים על כל IEnumerable.


עוד בFramework 2.0 חשבו על הרעיון של לשלוח delegateים למתודות למטרות סינון, ולראיה ניתן לראות את הפונקציה הבאות בList:

1
2
3
4
5
6
7
8
9
10
11
12
13
public bool Exists(Predicate<T> match);
public T Find(Predicate<T> match);
public void ForEach(Action<T> action);
public List<T> FindAll(Predicate<T> match);
public int FindIndex(Predicate<T> match);
public int FindIndex(int startIndex, Predicate<T> match);
public int FindIndex(int startIndex, int count, Predicate<T> match);
public T FindLast(Predicate<T> match);
public int FindLastIndex(Predicate<T> match);
public int FindLastIndex(int startIndex, Predicate<T> match);
public int FindLastIndex(int startIndex, int count, Predicate<T> match);
public int RemoveAll(Predicate<T> match);
public bool TrueForAll(Predicate<T> match);

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

FindAll מקבלת פרדיקט ומחזירה את כל האיברים שמקיימים אותו

TrueForAll מקבלת פרדיקט ומחזירה אמת אם כל איברי הרשימה מקיימים אותו

ForEach רצה על כל איברי הרשימה ומבצעת על כל אחד מהם פעולה

לגבי השאר, אני מניח שתוכלו לנחש לבד…


הבעיה בסיפור הזה היא שהמתודות האלה יושבות בתוך מחלקה מאוד ספציפית שנקרא List<T>.

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

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

למשל תוכלו להסכים איתי שנוכל לממש לכל IEnumerable<T> את ForEach בצורה באה:

1
2
3
4
5
6
7
public void ForEach(Action<T> action)
{
foreach (T current inthis)
{
action(current);
}
}

ואת TrueForAll בצורה הבאה:

1
2
3
4
5
6
7
8
9
10
11
12
public bool TrueForAll(Predicate<T> match)
{
foreach (T current in this)
{
if (!match(current))
{
return false;
}
}
return true;
}

לכן בד"כ נאלץ לשכפל קוד, מה שתמיד רע.


הפתרון: Extension methods. במקום שכל אחד יצטרך לשכפל פונקציונאליות, הומצאו Extension Methods שעובדים על IEnumerable<T>שמבצעים את אותו הדבר בדיוק, למשל יכולנו לכתוב את המתודה TrueForAll כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static bool TrueForAll<T>(this IEnumerable<T> enumerable,
Predicate<T> match)
{
foreach (T current in enumerable)
{
if (!match(current))
{
return false;
}
}
return true;
}

ראו גם טיפ מספר 84.

למען האמת כתבו לנו את המתודה הזאת (עם חתימה מעט שונה) בשם All בnamespace ששמו System.Linq. בנוסף כתבו לנו עוד אוסף שלם של Extension Methods שמספק פונקציונאליות אדירה ונוחה בטירוף.

אחת המתודות שלא נמצא שם לא משנה כמה קשה נחפש, היא מתודה ששקולה למתודת הForEach שראינו. הסיבה היא שאחד הרעיונות שעומד מאחורי LINQ באופן כללי הוא תכנות פונקציונאלי. בין השאר, אחד העקרונות של תכנות פונקציונאליות הוא אובייקטים שהם immutable וstateless. מאחר ובד"כ בForEach נבצע מניפולציה על האוסף שלנו, הוחלט שלא יהיה בשבילו Extension Method.

בגדול הnamespace הזה הוא namespace מאוד שימושי, ואנחנו עוד נכיר אותו לעומק.

שבוע של שפה מובנית שאילתא טוב

שתף

90. Anonymous Types

בעבר נתקלנו בכמה Featureים תכנותיים שיוצרים לנו מחלקות אנונימיות שלמות מאחורי הקלעים.

למשל, Anonymous Method שניגשת למשתנים לוקאליים של פונקציה, יוצרת מאחורי הקלעים מחלקה.

גם שימוש בyield return יוצר מאחורי הקלעים מחלקה.

בC# 3.0 נוספה אפשרות ליצור טיפוסים אנונימיים מאחורי הקלעים באופן ייזום. הדבר נעשה בעיקר בשביל לעשות מניפולציות על מידע.

השימוש הוא בצורה הבאה:

נוכל להגדיר משתנה לוקאלי של פונקציה כך:

1
2
3
4
5
6
var person =
new
{
FirstName = "Yali",
LastName = "Sobol",
};

השימוש בvarהוא הכרחי, משום שהטיפוס הוא אנונימי, ולכן אין לו שם.

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

נוכל גם לגשת לProperties שלו:

1
string firstName = person.FirstName;

קיים Feature נחמד – אם ניצור את הטיפוס עפ"י Properties או משתנים לוקאליים, הוא ידע לקבל את השמות שלהם לבד:

1
2
3
4
5
6
7
8
9
10
int Width = 3;
int Height = 5;
var box = new {Width, Height};
// Same as
var box = new
{
Width = Width,
Height = Height
};

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

הקוד עצמו לא כל כך מעניין, אבל יש מספר דברים מעניינים בו:

  1. שימו לב שהשדות הם readonly, והאובייקט Immutable
  2. שימו לב שמחלקה גנרית
  3. שימו לב שלEquals יש מימוש לא טריוויאלי והוא ממומש באמצעות EqualityComparer (ראו גם טיפ 79)
  4. גם לGetHashCode יש מימוש לא טריוויאלי
  5. גם לToString יש מימוש טוב, והוא אפילו משתמש בStringBuilder!
  6. יש Attribute של DebuggerDisplay, שמאפשר לנו לראות בWatch את המחלקה בצורה נוחה לעין

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

נספח קוד (יש המשך אחריו):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[DebuggerDisplay(@"\{ FirstName = {FirstName}, LastName = {LastName} }", Type="<Anonymous Type>"), CompilerGenerated]
internal sealed class AnonymousType<TFirstName, TLastName>
{
// Fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly TFirstName FirstNameField;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly TLastName LastNameField;
// Methods
[DebuggerHidden]
public AnonymousType(TFirstName FirstName, TLastName LastName)
{
this.FirstNameField = FirstName;
this.LastNameField = LastName;
}
[DebuggerHidden]
public override bool Equals(object value)
{
var type = value as AnonymousType<TFirstName, TLastName>;
return (((type != null) && EqualityComparer<TFirstName>.Default.Equals(this.FirstNameField, type.FirstNameField)) && EqualityComparer<TLastName>.Default.Equals(this.LastNameField, type.LastNameField));
}
[DebuggerHidden]
public override int GetHashCode()
{
int num = 0x31750700;
num = (-1521134295 * num) + EqualityComparer<TFirstName>.Default.GetHashCode(this.FirstNameField);
return ((-1521134295 * num) + EqualityComparer<TLastName>.Default.GetHashCode(this.LastNameField));
}
[DebuggerHidden]
public override string ToString()
{
StringBuilder builder = new StringBuilder();
builder.Append("{ FirstName = ");
builder.Append(this.FirstNameField);
builder.Append(", LastName = ");
builder.Append(this.LastNameField);
builder.Append(" }");
return builder.ToString();
}
// Properties
public TFirstName FirstName
{
get
{
return this.FirstNameField;
}
}
public TLastName LastName
{
get
{
return this.LastNameField;
}
}
}

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

השימוש בAnonymous Types מוגבל רק לscope של המתודה בה אנחנו משתמשים בו. לא נוכל להשתמש מחוץ לScope של המתודה, לכן לא נרצה להחזיר אותו כערך פונקציה וכו’.

כמובן, גם לא נרצה לאתחל אותו כערך של Field של מחלקה שלנו.

השימוש העיקרי בAnonymous Types הוא בשימוש בLINQ, כפי שעוד נראה…

סופ"ש השלמות טוב

שתף

89. Automatic properties

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

למשל, לא אחת כתבנו Properties כאלה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person
{
private string m_Name;
public string Name
{
get
{
return m_Name;
}
set
{
m_Name = value;
}
}
}

אחד הFeatureים שנוספו בC# 3.0 הוא Automatic Properties, החוסך לנו ליצור את הField של המחלקה. נוכל, למשל, בדוגמה זו לעשות אותו הדבר בצורה הבאה:

1
2
3
4
5
6
7
8
public class Person
{
public string Name
{
get;
set;
}
}

ראינו בעבר שגם אפשר לתת רמת חשיפה שונה לAccessorים ע"י ציון הmodifier לפני הAccessor. (טיפ מספר 25)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person
{
private string m_Name;
public string Name
{
get
{
return m_Name;
}
private set
{
m_Name = value;
}
}
}

נוכל להשתמש בזה גם בAutomatic Properties:

1
2
3
4
5
public string Name
{
get;
private set;
}

או למשל

1
2
3
4
5
public string Name
{
get;
protected set;
}

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


הגבלות:

לא נוכל לשים breakpoint בAutomatic Property, מה שיכול לפגוע בדיבוג שלנו. למשל, נוח לשים breakpoint בsetter ולגלות מי משנה לו את הערך לnull למשל.

לא נוכל לשים לוגיקה משלנו באחד מהAccessorים. למשל, אם אנחנו מממשים את INotifyPropertyChanged, לא נוכל להקפיץ את האירוע בשינוי של Property, אלא אם נשתמש בProperty רגיל.

לא נוכל להפוך Automatic Property לreadonly, כלומר לMember שאפשר לאתחל אותו רק בConstructor.

המשך שבוע השלמות טוב

שתף

88. Object initializer and calling base

נניח שיש לנו מחלקת אב ומחלקת בת שיורשת ממנה.

נניח בנוסף שהמחלקת אב צריכה לקבל מאיזושהי סיבה אובייקט בConstructor שלה, למשל:

1
2
3
4
5
6
public abstract class Animal
{
protected Animal(IFoodProvider provider)
{
}
}

ונניח עוד הנחות נוקשות, שאין Constructor מתאים שמאתחל את כל הProperties החשובים לנו באובייקט, למשל

1
2
3
4
5
6
public class Dog : Animal
{
public Dog() : base(new BoneProvider())
{
}
}

ולBoneProvider יש רק Constructor דיפולטי, או שאין לו Constructor שממלא את כל השדות שחשובים לנו.

כדי למלא את הProperties האלה בC# 2.0 היינו צריכים ליצור מתודה סטטית:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Dog : Animal
{
public Dog() : base(GetBoneProvider())
{
}
private static IFoodProvider GetBoneProvider()
{
BoneProvider provider = new BoneProvider();
provider.AllowedBonesPerDay = 3;
provider.AvailableBones = 100;
return provider;
}
}

בC# 3.0 נוכל לחסוך ולהשתמש בObject Initializer כדי להעביר את הפרמטרים מעלה בצורה הבאה:

1
2
3
4
5
6
7
8
9
10
11
public class Dog : Animal
{
public Dog() :
base(new BoneProvider
{
AllowedBonesPerDay = 3,
AvailableBones = 100
})
{
}
}

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

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

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

המשך שבוע השלמות טוב

שתף

87. Object initializer

ראינו בעבר (טיפ מספר 18) שאפשר לאתחל Collection בצורה נחמדה.

קיימת בC# 3.0 אפשרות גם לאתחל Properties של אובייקט בצורה כזאת.

במקום לכתוב קוד כזה:

1
2
3
Person person = new Person();
person.FistName = "Jonathan";
person.LastName = "Geffen";

נוכל לכתוב את זה בצורה הזאת:

1
2
3
4
5
6
Person person =
new Person
{
FistName = "Jonathan",
LastName = "Geffen"
};

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

ככה נוכל גם לאתחל אובייקטים היררכיים בצורה כזאת:

1
2
3
4
5
6
7
8
9
10
11
Person person =
new Person
{
FistName = "Jonathan",
LastName = "Geffen",
Child = new Person
{
FistName = "Aviv",
LastName = "Geffen"
}
};

במקום ככה:

1
2
3
4
5
6
Person person = new Person();
person.FistName = "Jonathan";
person.LastName = "Geffen";
person.Child = new Person();
person.Child.FistName = "Aviv";
person.Child.LastName = "Geffen";

אפשר גם לשלב את זה עם Collection Initializer ולקבל משהו כזה

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Band blackField =
new Band
{
new Person
{
FistName = "Aviv",
LastName = "Geffen"
},
new Person
{
FistName = "Steven",
LastName = "Wilson"
}
};

במקום ככה:

1
2
3
4
5
6
7
8
9
10
11
12
13
Band blackField = new Band();
blackField.Add(new Person
{
FistName = "Steven",
LastName = "Wilson"
});
blackField.Add(new Person
{
FistName = "Aviv",
LastName = "Geffen"
});

או יותר גרוע:

1
2
3
4
5
6
7
8
9
10
11
12
13
Band blackField = new Band();
Person steeve = new Person();
steeve.FistName = "Steven";
steeve.LastName = "Wilson";
blackField.Add(steeve);
Person aviv = new Person();
aviv.FistName = "Aviv";
aviv.LastName = "Geffen";
blackField.Add(aviv);

יש לציין ששימוש בobject initializer יוצר מאחורי הקלעים במידת הצורך משתני עזר כנ"ל, למשל בדוגמה

1
2
3
4
5
6
7
8
9
10
11
Person person =
new Person
{
FistName = "Jonathan",
LastName = "Geffen",
Child = new Person
{
FistName = "Aviv",
LastName = "Geffen"
}
};

הקוד יתקמפל למשהו כזה:

1
2
3
4
5
6
7
8
Person g__initLocal1a = new Person();
g__initLocal1a.FistName = "Jonathan";
g__initLocal1a.LastName = "Geffen";
Person g__initLocal1b = new Person();
g__initLocal1b.FistName = "Aviv";
g__initLocal1b.LastName = "Geffen";
g__initLocal1a.Child = g__initLocal1b;
Person person = g__initLocal1a;

הגבלות: נוכל להשתמש בObject Initializer רק מיד אחרי Constructor.

אם מישהו אחר יוצר לנו את האובייקט (למשל Factory), ואנחנו רוצים לאתחל אותו עם Properties, לא נוכל להשתמש בObject initializer (בVB יש keyword אחר שנקרא With שמאפשר לעשות משהו דומה, גם במידה והאובייקט לא בהכרח נוצר באמצעות Constructor)


לסיכום, שימוש בObject Initializer מגדיל את הקריאות של הקוד שלנו וכדאי להשתמש בו 😃

המשך יום (לא) גשום טוב

שתף

86. var keyword

אחד הדברים המצחיקים שאנחנו עושים בכתיבת קוד הוא חזרה על עצמנו.

למשל, נניח שיש לנו מתודה כזאת:

1
2
3
4
5
6
7
8
public IEnumerable<Person> GetFamilyInfo(string familyName)
{
List<Person> result = new List<Person>();
// ...
return result;
}

אז אנחנו קוראים לה בצורה כזאת:

1
IEnumerable<Person> bananiFamily = GetFamilyInfo("Banani");

כבר בדוגמה פשוטה זו חזרנו על עצמנו מספר פעמים:

פעם ראשונה היא בשורה

1
List<Person> result = new List<Person>();

שימו לב שמצוין הטיפוס List<Person> פעמיים – פעם אחת בצד ימין ופעם אחת בצד שמאל,

למרות שיכולנו לזהות את הטיפוס של result לפי ערך הביטוי של צד ימין.

באופן דומה, בשורה

1
IEnumerable<Person> bananiFamily = GetFamilyInfo("Banani");

מופיעה כפילות, אם כי יותר סמויה. הטיפוס IEnumerable<Person> מופיע הפעם רק בצד שמאל, אבל עצם העובדה שקראנו לפונקציה GetFamilyInfo, עשויה להסגיר שהטיפוס בצד שמאל אמור להיות IEnumerable<Person>.

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

לדוגמה, בשתי הדוגמאות שכתבנו, נוכל לכתוב במקום את הקוד הבא:

1
var result = new List<Person>();

הקומפיילר מזהה שהטיפוס של צד ימין הוא List<Person>, ולכן קובע את הטיפוס של result להיות List<Person>

באופן דומה,

1
var bananiFamily = GetFamilyInfo("Banani");

מאחר והפונקציה GetFamilyInfo מחזירה IEnumerable<Person>, הקומפיילר נותן לbananiFamily את הטיפוס IEnumerable<Person>


קיים בלבול נפוץ בין המילה השמורה var לבין הכנסת המשתנה לטיפוס object.

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

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

1
2
var result = new List<Person>();
result.Add(new Person()); // Compiles and works

אם, למשל, נחליף את המילה var בobject, השורה השנייה לא תתקמפל

1
2
object result = new List<Person>();
result.Add(new Person()); // Doesn't compile

בנוסף, ההשמה היא strongly typed, כלומר לא נוכל להחליף את הטיפוס באמצע:

1
2
var result = new List<Person>();
result = 3; // Won't compile, since result is of type List<Person>

מצד שני, object הוא אובייקט שכל האובייקטים יורשים ממנו, ולכן הצבה זו תהיה חוקית

1
2
object result = new List<Person>();
result = 3; // Compiles

הגבלות:

נוכל להשתמש בKeyword עבור משתנים מקומיים בלבד (לא נוכל לFields, ערכי החזר של פונקציה, ארגומנטים של פונקציה וכו’)

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


מתי להשתמש?

שימו לב שראינו שתי דוגמאות לשימוש בvar – אחד באתחול באמצעות Constructor, והשני בקריאה לפונקציה.

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

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

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

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

1
2
3
4
5
6
Dictionary<string, ICollection<ulong>> tableNamesToPks =
new Dictionary<string, ICollection<ulong>>();
// Use var instead
var tableNamesToPks =
new Dictionary<string, ICollection<ulong>>();

נימה אישית - אני לא מאוהדי var, ולא ממליץ להשתמש בזה, אלא אם זה לא פוגע בקריאות (למשל באתחול באמצעות Constructor באותה שורה)

שבוע גשום טוב

שתף

85. Decorator is your friend

לפעמים אנחנו יוצרים ממשק עם פונקציונאליות בסיסית

למשל

1
2
3
4
public interface ILog
{
void Log(string content);
}

כעת מגיע אלינו תוכניתן חדש ונמרץ ומבקש מאיתנו שנוסיף אפשרות להוסיף תאריך.

“רק תוסיף לי Property שאומר האם לרשום תאריך או לא"

קיבלנו ממשק חדש

1
2
3
4
5
public interface ILog
{
void Log(string content);
bool WriteDate { get; set; }
}

כעת מגיע אותו תוכניתן, ומבקש שגם נכתוב לConsole את הLOG בצבע שהוא מבקש

“רק תוסיף לי עוד שני Properties שאומרים אם לכתוב לConsole ובאיזה צבע":

1
2
3
4
5
6
7
public interface ILog
{
void Log(string content);
bool WriteDate { get; set; }
bool WriteToConsole { get; set; }
ConsoleColor Color { get; set; }
}

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

פתרון אחר הוא להשתמש בDesign Pattern ידוע שנקרא Decorator.

במקום להוסיף את כל ההרחבה של הפונקציונאליות לInterface, נעשה משהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DateLogger : ILog
{
private readonly ILog m_Source;
public DateLogger(ILog source)
{
m_Source = source;
}
public void Log(string content)
{
m_Source.Log
(string.Format("Content: {0}, Date: {1}",
content,
DateTime.Now));
}
}

ומשהו כזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConsoleLogger : ILog
{
private readonly ILog m_Source;
private readonly ConsoleColor m_Color;
public ConsoleLogger(ILog source, ConsoleColor color)
{
m_Source = source;
m_Color = color;
}
public void Log(string content)
{
Console.ForegroundColor = m_Color;
Console.WriteLine(content);
m_Source.Log(content);
}
}

כעת כדי להשתמש בפונקציונאליות נעשה משהו כזה:

1
2
3
4
ILog baseLogger;
ILog logger = new ConsoleLogger(new DateLogger(baseLogger),
ConsoleColor.Yellow);

הרבה יותר אלגנטי מאלפי Properties.


למרחיקי לכת:

נוכל לעשות מחלקת בסיס:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class DecoratedLogger : ILog
{
private readonly ILog m_Source;
protected DecoratedLogger(ILog source)
{
m_Source = source;
}
public void Log(string content)
{
InnerLog(content);
m_Source.Log(content);
}
protected abstract void InnerLog(string content);
}

ואז רק לרשת ולממש את InnerLog.

סופ"ש ממומשק טוב

שתף

84. Extension methods and interfaces

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

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

למשל, ראו את הדוגמה הבאה:

קיים ממשק המייצג גוף המסוגל להסתובב בכל כיוון:

1
2
3
4
5
6
public interface ITurnable
{
void TurnLeft();
void TurnOpposite();
void TurnRight();
}

הפונקציה TurnLeft – גורמת לגוף להסתובב 90 מעלות שמאלה

הפונקציה TurnOpposite – גורמת לגוף להסתובב ב180 מעלות

הפונקציה TurnRight – גורמת לגוף להסתובב ב90 מעלות ימינה

בסה"כ ממשק די פשוט, אלא שכדי לממש אותו, המממש צריך לממש 3 מתודות, מה שמדכא אותו לממש את הממשק הזה.

עוד דוגמה היא כזאת: נניח שיש לנו ממשק עם מתודה:

1
2
3
4
5
6
public interface IMailSender
{
void SendMessage(string address, string subject);
void SendMessage(string address, string subject, string content);
void SendMessage(string address, string subject, string content, object[] attachments);
}

(ודמיינו עוד כמה overloadים)

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

בעיות אלה אפשר לפתור בצורה אלגנטית באמצעות extension methods:

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

1
2
3
4
public interface ILeftTurnable
{
void TurnLeft();
}

ונוסיף שני extension methods כנ"ל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class TurnableExtensions
{
private static void TurnLeft(this ILeftTurnable turnable, int times)
{
for (int i = 0; i < times; i++)
{
turnable.TurnLeft();
}
}
public static void TurnOpposite(this ILeftTurnable turnable)
{
turnable.TurnLeft(2);
}
public static void TurnRight(this ILeftTurnable turnable)
{
turnable.TurnLeft(3);
}
}

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

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

1
2
3
4
public interface IMailSender
{
void SendMessage(string address, string subject, string content, object[] attachments);
}

ואת הגישה לשאר הפונקציות נעשה עם extension methods:

1
2
3
4
5
6
7
8
9
10
11
12
public static class MailSenderExtensions
{
public static void SendMessage(this IMailSender sender, string address, string subject)
{
sender.SendMessage(address, subject, null);
}
public static void SendMessage(this IMailSender sender, string address, string subject, string content)
{
sender.SendMessage(address, subject, content, null);
}
}

כך הרווחנו שני דברים:

למממש של המחלקה יש פחות עבודה לעשות, ואילו הממשק עדיין נוח למשתמשים בו.

כדאי לשים את הExtension Methods הנ"ל בNamespace של הממשק שלכם כדי שמי שמשתמש בממשק, ימצא את זה אוטומטית עם שימוש בממשק.


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

כדי לתמוך בזה, ניתן לעשות משהו כזה:

ניצור ממשק נוסף:

1
2
3
4
public interface IRightTurnable
{
void TurnRight();
}

כעת נתחשב בממשק זה בExtension Method שכתבנו:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void TurnRight(this ILeftTurnable turnable)
{
IRightTurnable rightTurnable =
turnable as IRightTurnable;
if (rightTurnable != null)
{
rightTurnable.TurnRight();
}
else
{
turnable.TurnLeft(3);
}
}

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

המשך ערב ממומשק טוב

שתף

83. Interface polymorphism pitfalls

ראינו פעם שעברה מהו explicit implementation (מימוש פרטי) של ממשק.

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

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

1
2
3
4
5
public interface IMoveable
{
void MoveNext();
void MovePrevious();
}

כעת ניצור שני ממשקים שיורשים ממנו:

1
2
3
4
5
6
7
public interface IVerticalMoveable : IMoveable
{
}
public interface IHorizontalMoveable : IMoveable
{
}

וכעת ניצור מחלקה כזאת:

1
2
3
public class Creature : IVerticalMoveable, IHorizontalMoveable
{
}

ונרצה לממש מימושים פרטיים של כל אחד מהממשקים:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Creature : IVerticalMoveable, IHorizontalMoveable
{
void IVerticalMoveable.MoveNext()
{
}
void IVerticalMoveable.MovePrevious()
{
}
void IHorizontalMoveable.MoveNext()
{
}
void IHorizontalMoveable.MovePrevious()
{
}
}

אלא שנקבל למרבה הצער את השגיאות הבאות בזמן קימפול:

‘Creature’ does not implement interface member ‘IMoveable.MovePrevious()’
‘Creature’ does not implement interface member ‘IMoveable.MoveNext()’
‘IVerticalMoveable.MoveNext’ in explicit interface declaration is not a member of interface
‘IVerticalMoveable.MovePrevious’ in explicit interface declaration is not a member of interface
‘IHorizontalMoveable.MoveNext’ in explicit interface declaration is not a member of interface
‘IHorizontalMoveable.MovePrevious’ in explicit interface declaration is not a member of interface

למה זה קורה?

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

בקיצור, מבאס.


עוד דוגמה:

הפעם דווקא לא מתחום הExplicit implementation.

נניח שיש לנו מחלקת אב שמממשת ממשק:

1
2
3
4
5
6
7
8
9
10
11
12
public interface IDrawable
{
void Draw();
}
public class Shape : IDrawable
{
public void Draw()
{
Console.WriteLine("A shape was drawn");
}
}

שימו לב שבמחלקת האב, הפונקציה Draw אינה וירטואלית.

עכשיו ברצוננו לרשת ממחלקת האב וליצור פונקציית Draw אחרת:

1
2
3
4
5
6
7
public class Triangle : Shape
{
public void Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

אלא שכעת כשנקרא לDraw עם IDrawable על Triangle, נקבל את התוצאה הלא רצויה הבאה:

1
2
IDrawable shape = new Triangle();
shape.Draw(); // A shape was drawn. :(

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

מה שקורה זה בזמן שמקומפלת המחלקה Shape, מאחר והיא מממשת את IDrawable, מתבצע מיפוי של הפונקציה Draw של הממשק, למתודה שנמצאת אצל Shape.

ננסה אולי לעשות משהו כזה:

1
2
3
4
5
6
7
public class Triangle : Shape
{
void IDrawable.Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

אבל נקבל לצערנו את שגיאת הקימפול הבאה:

‘Triangle.IDrawable.Draw()’: containing type does not implement interface ‘IDrawable’

כלומר, מימוש explicit של ממשק, לא עובר בירושה.

אז ננסה עוד משהו:

1
2
3
4
5
6
7
public class Triangle : Shape, IDrawable
{
void IDrawable.Draw()
{
Console.WriteLine("A triangle was drawn");
}
}

וכעת הקוד הבא יעבוד:

1
2
IDrawable shape = new Triangle();
shape.Draw(); // A triangle was drawn. :)

נראה שהבעיה שלנו נפתרה, אבל זה לא כך. אם נעשה משהו כזה:

1
2
Shape shape = new Triangle();
shape.Draw(); // A shape was drawn. :(

שהרי קריאה על instance של Shape, קוראת למתודה Draw של Shape (המתודה לא וירטואלית, ולכן הקריאה מפוענחת בזמן קימפול), ולכן לא פותר לנו את הבעיה, אלא רק מחליף לנו אותה בבעיה אחרת.

המסקנה היא:

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

המשך יום ממומשק טוב

שתף

82. Interface explicit implementation

פעמים רבות אנחנו מממשים ממשקים.

ניתן לממש ממשק בשתי דרכים: explicitly וimplicitly.

הדרך שאנחנו משתמשים בה בד”כ היא מימוש implicitly:

מה שזה אומר זה שפשוט במחלקה שלנו יש פונקציה עם חתימה שיש גם בממשק, למשל:

הממשק

1
2
3
4
public interface IFruit
{
string Name { get; }
}

עם המימוש

1
2
3
4
5
6
7
8
9
10
public class Banana : IFruit
{
public string Name
{
get
{
return "Banana";
}
}
}

Explicit implementation:

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

1
2
3
4
public interface IIsraeliProduct
{
string Name { get; }
}

עכשיו יש לנו טיפוס שמממש את שני הממשקים:

1
2
3
4
5
6
7
8
9
10
public class Banana : IFruit, IIsraeliProduct
{
public string Name
{
get
{
return "Banana";
}
}
}

עדיין עובד, אבל אולי אנחנו רוצים משהו אחר, למשל לעשות שאם אנחנו משתמשים בIIsraeliProduct, יחזור לנו שם המוצר בעברית.

נוכל לעשות זאת באמצעות Explicit implementation: הרעיון הוא שאנחנו משייכים את המימוש לממשק, למשל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Banana : IFruit, IIsraeliProduct
{
string IFruit.Name
{
get
{
return "Banana";
}
}
string IIsraeliProduct.Name
{
get
{
return "בננה";
}
}
}

כעת נוכל להשתמש במימוש שמעניין אותנו ע"י הכנסה לinstance מType מתאים:

1
2
3
4
5
Banana banana = new Banana();
IFruit fruit = banana;
IIsraeliProduct product = banana;
Console.WriteLine(fruit.Name); // Banana
Console.WriteLine(product.Name); // בננה

נשים לב לכמה דברים מעניינים:

  1. אנחנו לא מציינים modifier (private/public וכו’) על המימוש. הסיבה היא שהמימוש הוא "מימוש פרטי", כלומר לא ניתן לגשת למימוש זה בלי להסב את הטיפוס לממשק שהמתודה שייכת לו
  2. לא נוכל לקרוא למימוש בלי הסבה לממשק הנתון

שימושים:

  1. יצירת שני מימושים שונים לשתי פונקציות עם אותה חתימה, בהתאם לinterface. בד"כ עדיף שכל המימושים הפרטיים יחזירו אותו ערך, אבל לפעמים זה יכול לעזור.
  2. הסתרת מתודה ממחלקה. לפעמים מכריחים אותנו לממש ממשק, אבל אנחנו לא מעוניינים שהמשתמש יוכל להשתמש בפונקציה של הממשק. כך אנחנו מסתירים לו אותה, ורק אם הוא יסב את המשתנה לטיפוס של הממשק, הוא יוכל לגשת אליה. נפוץ מאוד כאשר מממשים גם גרסה גנרית וגם גרסה לא גנרית של ממשק. בד"כ נסתיר את הגרסה הלא גנרית.
  3. מימוש שני ממשקים עם מתודה בעלת אותו שם וחתימה, עד כדי ערך החזר. לדוגמה, נניח שיש לנו את שני הממשקים הבאים:
1
2
3
4
5
6
7
8
9
public interface INamesContainer
{
IEnumerable<string> Value { get; }
}
public interface IPksContainer
{
IEnumerable<ulong> Value { get; }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
public class MyContainer : INamesContainer, IPksContainer
{
IEnumerable<ulong> IPksContainer.Value
{
get { return null; }
}
IEnumerable<string> INamesContainer.Value
{
get { return null; }
}
}

ראו גם טיפ 285 על איך Java מסתדרת בלי Explicit Implementation.

יום ממומשק טוב

שתף