11. ICloneable and copy ctor

לעתים המחלקה שלנו מממשת ICloneable.בנוסף קיימת מחלקת בת שיורשת המחלקה שלנו.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Shape : ICloneable
{
#region ICloneable Members
public virtual object Clone()
{
...
}
#endregion
}
public class Circle : Shape
{
}

במקרה זה, היינו רוצים בד"כ לקרוא בפונקציה Clone של האב במחלקה של הבן, אלא שזו מחזירה טיפוס מהסוג של האב!

1
2
3
4
5
6
7
public class Circle : Shape
{
public override object Clone()
{
return base.Clone(); // Returns shape and not circle!
}
}

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

איך בכל זאת ניתן להתגבר על הבעיה?

Copy Constructor to the rescue:

ניצור Copy Ctor במחלקת אב:

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
public class Shape : ICloneable
{
public Shape(Shape source)
{
// Write clone logic for shape here.
}
#region ICloneable Members
public virtual object Clone()
{
return new Shape(this);
}
#endregion
}
public class Circle : Shape
{
public Circle(Circle source)
: base(source)
{
// Write specific clone logic for circle here.
}
public override object Clone()
{
return new Circle(this);
}
}

יום טוב

שתף

10. Array Initialization

רובנו מכירים את הsyntax הבא לאתחול מערך:

1
int[] array = new int[] {1, 1, 2, 3, 5, 8};

יש כמה syntaxים דומים שפחות מוכרים בקרב לאוכלוסיה.

הראשון הוא הכתיב המלא, הרי האתחול שרשמנו למעלה הוא קיצור של הכתיבה הבאה

1
int[] array = new int[6] {1, 1, 2, 3, 5, 8};

השני הוא הכתיב הבא:

1
int[] array = new[] {1, 1, 2, 3, 5, 8};

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

השלישי הוא הכתיב היותר קצר:

1
int[] array = {1, 1, 2, 3, 5, 8};

הערות:

הכתיבות הבאות נתמכות החל מnet 1.0, ואין צורך בLINQ וכו’ בשביל זה.

חג שמח

שתף

9. String.Join

מדי פעם אנחנו רואים קוד כזה

1
2
3
4
5
6
7
8
string numbers = string.Empty;
for (int i = 0; i < 1000; i++)
{
numbers += string.Format("(number = {0}) OR ", i);
}
numbers = numbers.Remove(numbers.Length - 4);

בו משרשרים הרבה מחרוזות עם איזשהו מפריד ביניהן.

בדרך כלל האיטרציה היא על איזשהו IEnumerable ולאו דווקא כפי שעשיתי.

הבעיה בסיפור הזה היא שהסיבוכיות כאן היא $ o(n^2) $, כי כל איטרציה אנחנו יוצרים אובייקט חדש ובעצם מעתיקים את כל האובייקט הקודם כפי שהוא! (הרי ידוע שstring הוא Immutable)

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

יבואו הצדיקים ויאמרו, השתמש בStringBuilder. כך נראה הקוד עם StringBuilder:

1
2
3
4
5
6
7
8
StringBuilder numbers = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
numbers.AppendFormat("(number = {0}) OR ", i);
}
numbers = numbers.Remove(numbers.Length - 4, 4);

כעת הסיבוכיות היא $ o(n) $, עם זאת עדיין משהו פה לא כל כך יפה.

הפתרון: השתמשו בstring.Join:

1
2
3
4
5
6
7
8
List<string> numbersList = new List<string>(1000);
for (int i = 0; i < 1000; i++)
{
numbersList.Add(string.Format("(number = {0})", i));
}
string numbers = string.Join(" OR ", numbersList.ToArray());

הערה:

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

אפשר לפתור את הבעיה אם אתם משתמשים בFramework 4, אז יש overloadים חדשים לstring.Join. ביניהם יש גם overload שמקבלIEnumerable<T>,המבצע פשוט Join של הToString() של כל אובייקט בIEnumerable.

כך יראה הקוד בnet 4:

1
2
3
4
5
6
7
8
List<string> numbersList = new List<string>(1000);
for (int i = 0; i < 1000; i++)
{
numbersList.Add(string.Format("(number = {0})", i));
}
string numbers = string.Join(" OR ", numbersList);

או כך, אם אתם מעדיפים לכתוב בLINQ

1
2
3
4
5
IEnumerable<string> numbersEnumerable =
from number in Enumerable.Range(0, 1000)
select string.Format("(number = {0})", number);
string numbers = string.Join(" OR ", numbersEnumerable);

(ניתן לכתוב את הגרסה של LINQ בnet 3.5, אבל תצטרכו לקרוא לJoin עם הToArray של IEnumerable<T>)

שתף

8. String.Format

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

1
2
3
4
5
6
7
8
9
string string1;
string string2;
string string3;
string string4;
string string5;
string myVeryLongString =
string1 + " AND " + string2 + " OR " + string3 +
" AND (" + string4 + "=" + string5 + ")";

הבעיה היא שקשה לעקוב אחר השרשור.

הדרך המומלצת היא כנ"ל:

1
2
3
4
5
6
7
8
9
10
11
12
13
string string1;
string string2;
string string3;
string string4;
string string5;
string myVeryLongString =
string.Format("{0} AND {1} OR {2} AND ({3} = {4})",
string1,
string2,
string3,
string4,
string5);

פעולה זו מתבצעת בעזרת StringBuilder בצורה יעילה…

יום טוב

שתף

7. String.IsNullOrEmpty

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

1
2
3
4
5
6
7
string str;
if ((str == null) ||
(str == string.Empty))
{
// ...
}

או כך:

1
2
3
4
5
6
7
string str;
if ((str != null) &&
(str != string.Empty))
{
// ...
}

השתמשו בפונקציה string.IsNullOrEmpty(str):

בדוגמא הראשונה:

1
2
3
4
5
6
string str;
if (string.IsNullOrEmpty(str))
{
// ...
}

בדוגמא השנייה:

1
2
3
4
5
6
string str;
if (!string.IsNullOrEmpty(str))
{
// ...
}

קצת על הפונקציה string.IsNullOrEmpty(str):

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

1
2
3
4
5
6
7
8
9
public static bool IsNullOrEmpty(string value)
{
if (value != null)
{
return (value.Length == 0);
}
return true;
}

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

שתף

6. String.Empty

השתמשו ב string.Empty

לדוגמה, במקום לכתוב

1
string str = "";

השתמשו ב

1
string str = string.Empty;

חג שמח

שתף

5. String.Equals

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

1
2
string str1, str2;
str1.ToUpper() == str2.ToUpper();

אפשר לכתוב:

1
2
string.Equals(str1, str2, StringComparison.InvariantCultureIgnoreCase);
str1.Equals(str2, StringComparison.InvariantCultureIgnoreCase);

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

שתף

4. ReadOnlyCollection

לפעמים אנחנו רוצים לחשוף <ICollection<T כי זה נוח (יש פונקציות Contains, Count וכו’).

הבעיה היא שיש גם פונקציות שאנחנו לא תמיד רוצים לחשוף כגון Add,Remove וכו’, כי זה מאפשר למשתמש מבחוץ לערוך לנו את הCollection.

הפתרון:

במקום

1
2
3
4
5
6
7
8
9
private List<string> m_Collection = new List<string>();
public ICollection<string> Collection
{
get
{
return m_Collection;
}
}

נכתוב

1
2
3
4
5
6
7
8
9
private List<string> m_Collection;
public ICollection<string> Collection
{
get
{
return new ReadOnlyCollection<string>(m_Collection);
}
}

חסרון:

ReadOnlyCollection מקבל בCtor שלו רק IList<T>.

שתף

3. Null Operator

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

1
2
3
4
5
6
Class1 x = GetClass1();
if (x == null)
{
x = new Class1();
}

אתם יכולים לכתוב קוד כזה:

1
x = GetClass1() ?? newClass1();

לא ברור כמה זה קריא.

שימושים:

במקום

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Class1 m_MyMember;
public Class1 MyProperty
{
get
{
if (m_MyMember == null)
{
m_MyMember = new Class1();
}
return m_MyMember;
}
}

תוכלו לכתוב את הקוד הבא:

1
2
3
4
5
6
7
8
public Class1 MyProperty
{
get
{
m_MyMember = m_MyMember ?? new Class1();
return m_MyMember;
}
}

כלומר הקוד

1
x = a ?? b;

שקול לכתיבה

1
2
3
4
5
6
x = a;
if (x == null)
{
x = b;
}
שתף

2. True Operator

רוצים לכתוב קוד כזה?

1
2
3
4
5
6
Class1 class1 = new Class1();
if (class1)
{
// ...
}

אתם יכולים לעשות דבר כזה (ואל תעשו!)

1
2
3
4
5
6
7
8
9
public static bool operator true(Class1 class1)
{
// ...
}
public static bool operator false(Class1 class1)
{
// ...
}

למשל בדיקת null כמו בימי ה-C העליזים לבדיקה האם פוינטר מצביע ל-NULL או לא:

1
2
3
4
5
6
7
8
9
public static bool operator true(Class1 class1)
{
return class1 != null;
}
public static bool operator false(Class1 class1)
{
return class1 == null;
}

ואז

1
2
3
4
5
6
Class1 class1;
if (class1)
{
class1.Print();
}
שתף