21. Enum.ToString(format)

בהמשך לטיפ היומי הקודם, נניח שיש לנו enum:

1
2
3
4
5
6
7
8
9
10
public enum Days
{
Sunday = 1,
Monday = 2,
Tuesday = 3,
Wednesday = 4,
Thursday = 5,
Friday = 6,
Saturday = 7
}

ואנחנו רוצים להדפיס את הערך של איבר בenum.

ראינו פעם שעברה שToString מחזיר את השם של האיבר ולא את הערך שלו.

נשאלת השאלה כיצד ניתן להדפיס את הערך של האיבר.

פתרון אחד הוא לעשות משהו כזה:

1
2
string dayValue =
Convert.ToInt32(Days.Sunday).ToString();

אלא שלא תמיד מאחורי enum מסתתר int.

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

1
2
3
string dayValue =
Convert.ChangeType(Days.Sunday,
Enum.GetUnderlyingType(typeof (Days))).ToString();

אפשרות שלישית היא להשתמש בToString

אבל רגע! אמרנו שToString מחזיר את השם של האיבר בenum. אבל יש overload שמקבל format!

1
2
3
4
5
string dayValue = Days.Sunday.ToString("D");
string dayHexValue = Days.Sunday.ToString("X");
Console.WriteLine(dayValue); // prints 1
Console.WriteLine(dayHexValue); // prints 00000001

הראשון מדפיס את ערך הenum והשני את ערך הenum בhex.

שבוע טוב

שתף

20. Enum.ToString

נניח שיש לנו איזשהו enum

1
2
3
4
5
6
7
8
9
10
public enum Days
{
Sunday = 1,
Monday = 2,
Tuesday = 3,
Wednesday = 4,
Thursday = 5,
Friday = 6,
Saturday = 7
}

ואנחנו רוצים להדפיס שם של ערך של הenum:

מסתבר שאפשר פשוט לעשות משהו כזה:

1
2
string day = Days.Sunday.ToString();
Console.WriteLine(day); // prints Sunday and not 1!

סופ"ש מעולה

שתף

19. HashSet

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

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

אם נשתמש בList, או בCollection סטנדרטי אחר חיפוש איבר יקח $ o(n) $, כיוון שפשוט אנחנו עוברים על כל האיברים ובודקים על כל אחד האם הוא שווה לאיבר שאנחנו מחפשים או לאו.

לכן עולה הרעיון של להשתמש במנגנון של GetHashCode בכדי למצוא איבר במהירות.

הדרך המרושלת לעשות זאת זה לאנוס Dictionary בשביל הצורך הזה:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Dictionary<string, bool> bandMembers =
new Dictionary<string, bool>();
bandMembers["George"] = true;
bandMembers["Paul"] = true;
bandMembers["John"] = true;
bandMembers["Ringo"] = true;
// ...
if (bandMembers.ContainsKey("George"))
{
// ...
}
if (bandMembers.ContainsKey("Gaga"))
{
// ...
}

ContainsKey של Dictionary אכן עובד עם GetHashCode ולכן יספק תשובה באופן מהיר יותר, אך זה שימוש לא נכון בDictionary מאחר ואנחנו בכלל לא מתייחסים לValue.

לפני Framework 3.5 לא היה פתרון כל כך טוב בBCL והאלטרנטיבה הכי טובה הייתה להשתמש בSet של PowerCollections, או לממש בעצמנו מנגנון כזה.

בFramework 3.5 הוסיפו מחלקה בשם HashSet<T>, בnamespace System.Collections.Generic בSystem.Core.dll.

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

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

הדוגמה הקודמת תכתב כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HashSet<string> bandMembers =
new HashSet<string>();
bandMembers.Add("George");
bandMembers.Add("Paul");
bandMembers.Add("John");
bandMembers.Add("Ringo");
if (bandMembers.Contains("George"))
{
// ...
}
if (bandMembers.Contains("Gaga"))
{
// ...
}

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

יום טוב

שתף

18. Collection Initializer

בC# 3.0 הוסיפו Feature נחמד של syntatic sugar בשפה בכדי לאתחל Collectionים

נניח שיש לנו class פשוט, למשל

1
2
3
4
5
6
7
8
9
10
11
12
public class Person
{
public string Name
{
get; set;
}
public int Age
{
get; set;
}
}

יהי IEnumerable שיש לו פונקציית Add, למשל

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PersonCollection : IEnumerable<Person>
{
private ICollection<Person> m_PersonCollection = new List<Person>();
public void Add(string name, int age)
{
Person person = new Person();
person.Name = name;
person.Age = age;
m_PersonCollection.Add(person);
}
#region IEnumerable<Person> Members
// ...
#endregion
}

אז במקום לאתחל אותו כך:

1
2
3
4
5
6
7
8
PersonCollection people = new PersonCollection();
people.Add("John", 48);
people.Add("Hurley", 32);
people.Add("James", 39);
people.Add("Sayid", 40);
people.Add("Jack", 37);
people.Add("Jin", 33);

נוכל לאתחל אותו עם הsyntax הנחמד הבא:

1
2
3
4
5
6
7
8
9
10
PersonCollection people =
new PersonCollection
{
{"John", 48},
{"Hurley", 32},
{"James", 39},
{"Sayid", 40},
{"Jack", 37},
{"Jin", 33}
};

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

אם רוצים להוסיף פרמטר, פשוט צריך לשנות את פונקציית הAdd.

בנוסף ניתן "להחביא" את הType של המחלקה ולהשתמש בממשק במקום בצורה הזאת, ע"י

1
2
3
4
5
6
7
8
9
10
IEnumerable<Person> people =
new PersonCollection
{
{"John", 48},
{"Hurley", 32},
{"James", 39},
{"Sayid", 40},
{"Jack", 37},
{"Jin", 33}
};

שימושים:

IDictionary<TKey, TValue> מממש את הממשק IEnumerable<KeyValuePair<TKey, TValue>> כפי שראינו אתמול.

בנוסף יש לו פונקציית public void Add(TKey key, TValue value) ולכן נוכל לאתחל אותו כך:

1
2
3
4
5
6
7
IDictionary<int, string> idToName =
new Dictionary<int, string>
{
{123456789, "Israel Israeli"},
{314159265, "Apple Pi"},
{011235813, "Fibbo Nacci"},
};

במקום כך:

1
2
3
4
5
6
IDictionary<int, string> idToName =
new Dictionary<int, string>();
idToName.Add(123456789, "Israel Israeli");
idToName.Add(314159265, "Apple Pi");
idToName.Add(011235813, "Fibbo Nacci");

הערה: החל מC# 6.0, הסינטקס הזה עובד גם על Extension Method בשם Add.

יום טוב

שתף

17. KeyValuePair

מדי פעם אנחנו צריכים לרוץ על כל הזוגות שיש בIDictionary.

הדרך הפחות הטובה לעשות זאת היא כך

1
2
3
4
5
6
7
IDictionary<string, ulong> dictionary;
foreach (string currentKey in dictionary.Keys)
{
ulong currentValue = dictionary[currentKey];
// ...
}

למרבה המזל IDictionary<TKey,TValue> מממשICollection<KeyValuePair<TKey, TValue>> ולכן גם

1
IEnumerable<KeyValuePair<TKey, TValue>>:

הדרך היותר טובה היא כך:

1
2
3
4
5
6
7
8
IDictionary<string, ulong> dictionary;
foreach (KeyValuePair<string, ulong> currentPair in dictionary)
{
string currentKey = currentPair.Key;
ulong currentValue = currentPair.Value;
// ...
}

ההבדל הוא שבשיטה הראשונה אנחנו מחפשים עבור כל מפתח את הערך שלו (אמנם בכמעט $ o(1) $), ובשיטה השנייה אנחנו מראש עוברים על כל הזוגות.

יום טוב

שתף

16. Dictionary.TryGetValue

הרבה פעמים אנחנו כותבים את הקוד הבא:

1
2
3
4
5
6
7
8
9
10
IDictionary<int, string> myDictionary;
if (myDictionary.ContainsKey(givenKey))
{
return myDictionary[givenKey];
}
else
{
return null;
}

אלא שקוד זה בזבזני, כיוון שאנחנו מחפשים את המפתח פעמיים בDictionary (פעם ב ContainsKey ופעם שנייה בגישה לIndexer).

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IDictionary<int, string> myDictionary;
string requestedValue;
bool wasKeyFound =
myDictionary.TryGetValue(givenKey, out requestedValue);
if (wasKeyFound)
{
return requestedValue;
}
else
{
return null;
}

בקוד זה יש חיפוש יחיד של המפתח בDictionary, ע"י הפונקציה TryGetValue.

שיהיה שבוע טוב

שתף

15. Enumerable.Any

לעתים קרובות יש לנו IEnumerable שמתקבל למשל משאילתת LINQ:

1
2
3
4
5
6
7
IEnumerable<string> fruits =
new []{"Apple", "Banana", "Peach", "Grapes", "Watermelon"};
IEnumerable<string> fruitQuery =
from fruit in fruits
where fruit.Length == 5
select fruit;

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

1
2
3
4
if (fruitQuery.Count() > 0)
{
// ...
}

אלא שדרך זו אינה יעילה, כיוון שהמימוש של הExtension Method של Count הוא כהלן:

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
public static int Count<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw new ArgumentException("source");
}
ICollection<TSource> is2 = source as ICollection<TSource>;
if (is2 != null)
{
return is2.Count;
}
else
{
int num = 0;
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
num++;
}
}
return num;
}
}

שימו לב:

אם ה IEnumerable<T>שהעברנו הוא ICollection<T> מתבצעת אופטימיזציה וזה ניגש לProperty בשם Count שלו.

אבל אחרת אנחנו עוברים על כל ה IEnumerable<T>בשביל לספור את כמות האיברים שבו, כשבסה"כ רצינו לבדוק שהוא אינו ריק.

בד"כ יקרה המקרה הגרוע, כיוון שמדובר בדרך כלל בתסריט של בדיקה האם מתקבלת תשובה משאילתת LINQ.

הפתרון הוא להשתמש בExtension Method שנקרא Any, כעת הif יראה כך:

1
2
3
4
if (fruitQuery.Any())
{
// ...
}

לו יש מימוש יותר פשוט ויעיל:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static bool Any<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw new ArgumentException("source");
}
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
{
return true;
}
}
return false;
}

הערה אחרונה:

לAny יש גם overload שמקבל predicate ומחזיר תשובה האם קיים איבר שמקיים את הpredicate.

ראו גם טיפ מספר 156.5 לפתרון דומה לשאלה "האם IEnumerable מכיל לפחות x איברים?"

סופ"ש מצוין

שתף

14. readonly and const

[נכתב ע”י עידו גוטרמן]

קיימים שני סוגים של קבועים שבהם נרצה להשתמש

Compile-time constants

1
public const int myCompileTimeConstValue = 6;

בזמן קומפילציה מומר הcompile time const לערך. הIL אינה מכירה את הקבוע הנ"ל.

כמו כן ניתן להגדיר קבועים רק לPrimitive Types. למשל קטע הקוד הבא לא יתקמפל :

1
public const DateTime myDate = new DateTime(1987 , 1 , 22);

נציין כי חסכון בזמן ריצת התוכנית (זניח בהשוואה לRuntime consts). בנוסף לרב, שימוש בconsts יצור assembly גדול יותר ( כיוון שהIL מחליף ref לפרמטר בערך עצמו)

נשתמש בconst כאשר נרצה לבצע

  1. שימוש בEnums
  2. אתחול ctors של Attributes באמצעות consts
  3. Primitive constant (שאינו משתנה בין גרסאות)

Run-time constants

הקבוע מוגדר במקום בזיכרון הניתן להשמה רק בזמן הctor. ז"א שיהיה מקום בזיכרון שיאחסן את הקבוע ובזמן הJIT הוא יומר לקוד.

1
public static readonly int myRuntimeConstValue = 66;

נשתמש בRuntime constants כאשר

  1. נרצה להגדיר קבועים מטיפוס reference types
  2. הקבועים שלנו יכולים להשתנות בין גרסאות ( למשל מיקום קובץ דיפולטי) – ראה דוגמא בסוף
  3. נוכל להשתמש בRuntime constant שאינו סטטי על מנת להגדיר קבועים שונים למחלקות שונות

סיכום

באופן כללי נעדיף תמיד להשתמש בRuntime constants על פני Comiple Time constants. הפגיעה בביצועים היא זניחה לעומת הגמישות שהדבר נותן. ( ראה טבלה )

לדוגמא

הקבועים myCompileTimeConstValue וmyRuntimeConstValue מוגדר בassembly – myCommonConstants.dll , והאפליקציה MyApp.exe משתמשת בהם.

כאשר נחליף את ערך הקבועmyCompileTimeConstValue . נצטרך לקמפל נצטרך לקמפל מחדש את הmyCommonConstants.dll

ואת כל הassemblies שמשתמשים בו(!).

כאשר נשנה את הערך myRuntimeConstValue נצטרך לקמפל מחדש רק את הmyCommonConstants.dll

מצ"ב טבלה

מקור

Comments and Recommendations const readonly Usage
Use const. Primitive types that will never change should be const. Yes Yes Primitive constant
Use static readonly. Any constant value that might change should be readonly, not const. Yes Yes Release Dependent const, primitive type
Use static readonly. It’s the only one that works. No Yes Other constants
Use (instance) readonly. It’s the only option, and immutability is enforced by the compiler. No Yes Immutable members
Enumerated values must be const. Yes No Enumerated values
These must be constants. Yes No Values used to construct attributes
שתף

13. using Directive

מדי פעם אנחנו משתמשים במבנה נתונים מסובך בשביל הנוחות, למשל:

1
2
IDictionary<string, ICollection<ulong>> tableToRelevantPks =
new Dictionary<string, ICollection<ulong>>();

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

אפשר לעשות את הטריק הבא:

נכתוב ביחד עם הusing של הקובץ את השורות הבאות:

1
2
using ITableToPksCollection = System.Collections.Generic.IDictionary<string, System.Collections.Generic.ICollection<ulong>>;
using TableToPksCollection = System.Collections.Generic.Dictionary<string, System.Collections.Generic.ICollection<ulong>>;

כעת נוכל לכתוב בכיף את הקוד הבא:

1
ITableToPksCollection tableToRelevantPks = new TableToPksCollection();
שתף

12. as Operator

[נכתב ע”י שני אלחרר]

והטיפ היומי הוא בנושא המרות (Casts).

לפעמים אנחנו לא מודעים לסוג של אובייקט, אז אנחנו עושים Castים כך:

1
2
3
4
5
6
7
8
9
10
11
object obj = GimmeObject();
try
{
int initifiedObj = (int) obj;
// Object is int, continue...
}
catch (InvalidCastException)
{
// Object is not int
}

השיטה הזו לא נכונה, כיוון שזה לא נכון להשתמש בExceptionים לFlow הרציף של התוכנה.

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

is יחזיר ערך חיובי אם האובייקט יורש/הוא מהType שניתן, as יחזיר את האובייקט מומר לType שניתן אם הוא יורש ממנו / הוא מהType שניתן – ואם לא, הוא יחזיר את הערך null.

דוגמה לשימוש בis, בדיקה שאובייקט יורש מIDisposable :

1
2
3
4
5
6
object obj = GimmeObject();
if (obj is IDisposable)
{
Console.WriteLine("Object is disposable")
}

דוגמה לשימוש בas, קריאה לDispose של אוסף אובייקטים שחלקם יורשים וחלקם לא מIDisposable :

1
2
3
4
5
6
7
8
9
10
11
IEnumerable<object> objects = GimmeObjects();
foreach (object obj in objects)
{
IDisposable disposable = obj as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}

שימו לב כי כתיבת השורה

1
MyClass myClassObj = obj as MyClass;

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

1
2
3
4
5
6
7
8
9
10
MyClass myClassObj;
if (obj is MyClass)
{
myClassObj = (MyClass)obj;
}
else
{
myClassObj = null;
}

ולכן לא ניתן להשתמש באופרטור as בשביל ValueTypes (כגון int, DateTime וכו’)

ראו גם טיפים 326-328 להבדלים בין is וas.

שתף