31. More generic constraints

בוקר טוב,

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

1
where T : ValueType

לפעמים אנחנו רוצים שType שנקבל יהיה דווקא value type ולא reference type

נוכל לכתוב את זה כך:

1
where T : struct

שימוש אפשרי:

1
2
3
4
5
public static T Clone<T>(T source)
where T : struct
{
return source;
}

הפונקציה הזאת מקבלת כמעט כל value type ומשכפלת את הערך שלו.

למשל

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public struct Person
{
public string Name;
public string LastName;
public Person(string name, string lastName)
{
Name = name;
LastName = lastName;
}
}
Person ziggy = new Person("Bob", "Marley");
Person bob = Clone(ziggy);
ziggy.Name = "Ziggy";
Console.WriteLine(bob.Name); // Prints Bob!

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

באופן דומה קיים Constraint מקביל:

1
where T : class

המשמש כדי לדרוש שמחלקה תהיה reference type. (שימו לב, גם interfaceים, delegateים, Arrayים וכו’ נחשבים reference type).

למה זה טוב? כפי שציינו, כאשר מעבירים value type לפונקציה, מועבר עותק שלו. פעולה זו עשויה להיות כבדה, לכן לפעמים נרצה להכריח פונקציה לקבל רק reference types שאינם דורשים העתקה של כל האובייקט.

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

שבוע גנרי טוב

שתף

30. Advanced generic constraints

בהמשך לשבוע הגנרי הטוב,

נניח שיש לנו ממשק של משהו שמזכיר מספר

1
2
3
4
public interface IAddable
{
IAddable Add(IAddable other);
}

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

איזה דברים יכולים לממש את זה?

הרבה דברים מתמטיים:

בעצם כל דבר שסגור לחיבור:

מחלקה של מספרים שלמים שתכתבו, מחלקה של מספרים ממשיים/רציונליים/מרוכבים שתממשו, מחלקה של מטריצות מסדר latex 5 \times 6 $ שתממשו, מחלקה של וקטורים ממימד 3 שתממשו, ועוד ועוד.

הכל נראה טוב ויפה, עד שנגיע לבעיה הבאה:

יש לנו שתי מחלקות

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Matrix4x4 : IAddable
{
public IAddable Add(IAddable other)
{
// ...
}
}
public class RationalNumber : IAddable
{
public IAddable Add(IAddable other)
{
// ...
}
}

כעת נוכל לעשות משהו כזה:

1
2
3
4
RationalNumber rationalNumber;
Matrix4x4 matrix;
IAddable result = rationalNumber.Add(matrix); // Compiles!

אני אסביר את הבעיה:

באמת אפשר לחבר מטריצות (אחת עם השנייה), ואפשר גם לחבר מספרים רציונליים (אחד עם השני), אבל אי אפשר לחבר מספר רציונלי עם מטריצה בגודל $ 4 \times 4 $!

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

בפועל לרוב נקבל שגיאה בזמן ריצה מאחר ובפונקציה Add המממושת בRationalNumber בוודאי תהיה הסבה של other (המשתנה שאנחנו מקבלים בפונקציה) לRationalNumber.

פתרון:

שלב 1: נהפוך את הממשק לגנרי:

1
2
3
4
public interface IAddable<T>
{
T Add(T other);
}

כעת המימושים יראו כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Matrix4x4 : IAddable<Matrix4x4>
{
public Matrix4x4 Add(Matrix4x4 other)
{
// ...
}
}
public class RationalNumber : IAddable<RationalNumber>
{
public RationalNumber Add(RationalNumber other)
{
// ...
}
}

והשורה השטנית

1
object result = rationalNumber.Add(matrix);

כבר לא מתקמפלת!

The best overloaded method match for ‘RationalNumber.Add(RationalNumber)’ has some invalid arguments
Argument ‘1’: cannot convert from ‘Matrix4x4’ to ‘RationalNumber’

הכל טוב ויפה, לא?

לא.

למה לא?

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

1
2
3
4
5
6
7
public class DumbAddable : IAddable<int>
{
public int Add(int other)
{
// ...
}
}

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

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

זה לא נכון, משתי הסיבות הבאות:

  1. תשתית אמורה להגן כמה שניתן מהמתכנת מלעשות דברים לא נכונים.
  2. זה פוגע גם בתשתית. נניח שנכתוב פונקציה כזאת:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static IAddable<T> AddMany<T>(IAddable<T>[] parameters)
{
if (parameters.Length == 0)
{
throw new ArgumentException
("Excepted to recieve at least one parameter",
"parameters");
}
else
{
IAddable<T> currentSum = parameters[0];
for (int i = 1; i < parameters.Length; i++)
{
currentSum = currentSum.Add(parameters[i]);
}
return currentSum;
}
}

פונקציה לגיטימית שמחברת הרבה IAddable<T>.

זה לא יתקמפל לנו:

The best overloaded method match for ‘IAddable.Add(T)’ has some invalid arguments
Argument ‘1’: cannot convert from ‘IAddable’ to ‘T’

שלב ב’ של הפתרון:

נשנה את הממשק כך:

1
2
3
4
public interface IAddable<T> where T : IAddable<T>
{
T Add(T other);
}

עכשיו אתם אמורים לחשוב לעצמכם, רגע, זה לא רקורסיבי????

התשובה הפשוטה היא שזה לא.

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

כאשר מממשים רשימה כותבים בד"כ משהו כזה

1
2
3
4
5
struct listNode
{
int value;
listNode* next;
}

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

נחזור לשלב ב’ של הפתרון. כעת לא תתקמפל המחלקה DumbAddable:

The type ‘int’ cannot be used as type parameter ‘T’ in the generic type or method ‘IAddable’. There is no boxing conversion from ‘int’ to ‘IAddable’

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static T AddMany<T>(T[] parameters)
where T : IAddable<T>
{
if (parameters.Length == 0)
{
throw new ArgumentException
("Excepted to recieve at least one parameter",
"parameters");
}
else
{
T currentSum = parameters[0];
for (int i = 1; i < parameters.Length; i++)
{
currentSum = currentSum.Add(parameters[i]);
}
return currentSum;
}
}

שימו לב שעכשיו המתודה גם נראית הרבה יותר טוב, אין בה בשום מקום IAddable<T>, חוץ מבConstraint.

סופ"ש גנרי מצוין

שתף

29. Generic constraints

בהמשך לשבוע הגנרי הטוב,

נניח שיש לנו פונקציה פשוטה שמוצאת מקסימום של מערך נתון:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static int Max(int[] array)
{
if (array.Length == 0)
{
throw new ArgumentException("Array was empty",
"array");
}
else
{
int currentMax = array[0];
foreach (int element in array)
{
if (currentMax < element)
{
currentMax = element;
}
}
return currentMax;
}
}

כרגיל לשבוע, אין שום דבר מיוחד בint, והיינו רוצים שהפונקציה תעבוד גם לchar, ulong, long וכו’.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static T Max<T>(T[] array)
{
if (array.Length == 0)
{
throw new ArgumentException("Array was empty",
"array");
}
else
{
T currentMax = array[0];
foreach (T element in array)
{
if (currentMax < element)
{
currentMax = element;
}
}
return currentMax;
}
}

Operator ‘ ‘T’ does not contain a definition for ‘CompareTo’ and no extension method ‘CompareTo’ accepting a first argument of type ‘T’ could be found (are you missing a using directive or an assembly reference?)

כדי לפתור בעיות מסוג וכאלה, המציאו עוד כלי מאוד חזק לעבודה עם Generics – Generic constraints.

הפונקציה CompareTo אכן קיימת, אבל בממשק IComparable (או IComparable<T>).

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

נוכל לעשות זאת כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static T Max<T>(T[] array)
where T : IComparable
{
if (array.Length == 0)
{
throw new ArgumentException("Array was empty",
"array");
}
else
{
T currentMax = array[0];
foreach (T element in array)
{
if (element.CompareTo(currentMax) > 0)
{
currentMax = element;
}
}
return currentMax;
}
}

הwhere מציין שהפונקציה יכולה לקבל רק T שמקיים את התנאים הכתובים אחריו.

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

על המחלקה שאנחנו רוצים שהוא ירש ממנה יש מספר הגבלות: היא לא יכולה להיות System.Array (תכתבו פשוט T[] בארגומנטים), System.Delegate, System.Enum, System.ValueType או object.

נראה הרבה מאוד דברים מגניבים שאפשר לעשות עם זה בטיפים הבאים.

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

שתף

28. Calling generic methods implicitly

בהמשך לשבועי הגנרי הטוב,

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

1
2
3
4
5
6
7
public static T[] Reverse<T>(T[] array)
{
// ...
}
string[] fruits = { "Apple", "Banana", "Orange", "Mango" };
string[] reversed = Reverse<string>(fruits);

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

1
2
string[] fruits = { "Apple", "Banana", "Orange", "Mango" };
string[] reversed = Reverse(fruits);

הקומפיילר החכם יודע לבד מה הType שאנחנו רוצים לקרוא איתו לGeneric Method לפי הארגומנט שאנחנו שולחים לפונקציה.

גם את הדוגמה השנייה של אתמול:

נוכל במקום לכתוב כך:

1
2
3
4
5
6
public static int Count<T>(IEnumerable<T> enumerable)
{
// ...
}
int count = Count<string>(fruits);

לכתוב כך:

1
int count = Count(fruits);

ואפילו אפשר!

1
int length = Count("This string's length is 26"); // length = 26

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

אמרתי שבהרבה מאוד מקרים אפשר לעשות את זה. מתי אי אפשר?

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

1
public class DumbEnumerable : IEnumerable<int>, IEnumerable<Person>

אם ננסה לקרוא עכשיו לCount נקבל עכשיו את השגיאה הבאה בקימפול:

1
int count = Count(new DumbEnumerable());

The type arguments for method ‘Count(System.Collections.Generic.IEnumerable)’ cannot be inferred from the usage. Try specifying the type arguments explicitly.

דוגמה נוספת היא כשאנחנו מנסים לשלוח אובייקט שהוא לא מType מתאים, למשל:

1
int count = Count(123); // doesn't compile

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

למשל, בדוגמה הראשונה, ייתכן ולפעמים נרצה לקבל object[] לפעמים מהפונקציה (למשל אם עובדים בתשתית).

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

1
object[] reversed = Reverse<object>(fruits);

תהנו

שתף

27. Generic Methods

בהמשך לשבוע הגנרי הטוב,

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Person[] Reverse(Person[] array)
{
Person[] result =
new Person[array.Length];
int currentIndex = array.Length - 1;
foreach (Person current in array)
{
result[currentIndex--] = current;
}
return result;
}

כמו בדוגמאות של אתמול, אין פה שום דבר מיוחד במחלקה Person, ויכולנו לכתוב את אותו קוד גם עבור int, object וכו’.

לדוגמה עבור string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static string[] Reverse(string[] array)
{
string[] result =
new string[array.Length];
int currentIndex = array.Length - 1;
foreach (string current in array)
{
result[currentIndex--] = current;
}
return result;
}

בדומה לGeneric Types יש Feature של Generic Methods.

נוכל לכתוב כך:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static T[] Reverse<T>(T[] array)
{
T[] result =
new T[array.Length];
int currentIndex = array.Length - 1;
foreach (T current in array)
{
result[currentIndex--] = current;
}
return result;
}

שימוש:

1
2
3
4
5
int[] numbers = {4, 8, 15, 16, 23, 42};
int[] reversed = Reverse<int>(numbers);
string[] fruits = {"Apple", "Banana", "Orange", "Mango"};
string[] reversed = Reverse<string>(fruits);

אבל לדבר הזה יש שימושים נוספים.

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

בשביל זה אנחנו צריכים לכאורה ליצור פונקציה שיכול לקבל IEnumerable<T> עבור כל T.

בזכות Generic Methods אפשר!

1
2
3
4
5
6
7
8
9
10
11
public static int Count<T>(IEnumerable<T> enumerable)
{
int count = 0;
foreach (T current in enumerable)
{
count++;
}
return count;
}

קריאה למתודה:

1
2
string[] fruits = { "Apple", "Banana", "Orange", "Mango" };
int count = Count<string>(fruits);

המשך יום גנרי טוב

שתף

26. Generic Types

הטיפים הקרובים הם הכנה בשביל הטיפים שאחריהם.

(זהירות, הרבה קוד משוכפל)

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

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
public class IntCollection
{
private int[] m_InternalArray = new int[4];
private int m_NumOfValues;
public int this[int index]
{
get
{
return m_InternalArray[index];
}
set
{
m_InternalArray[index] = value;
}
}
public void Add(int item)
{
if (m_NumOfValues == m_InternalArray.Length)
{
int[] copyArray = new int[m_InternalArray.Length * 2];
m_InternalArray.CopyTo(copyArray, 0);
m_InternalArray = copyArray;
}
m_InternalArray[m_NumOfValues] = item;
m_NumOfValues++;
}
}

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

עכשיו נניח שאנחנו רוצים אותו מימוש גם עבור מחלקות אחרות, לאו דווקא int, אז מה שהיינו צריכים לעשות בימי framework 1 העליזים זה לשכפל את הקוד, ובכל מקום להחליף את int במחלקה שאנחנו מעוניינים לבנות עבורה את הCollection:

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
public class PersonCollection
{
private Person[] m_InternalArray = new Person[4];
private int m_NumOfValues;
public Person this[int index]
{
get
{
return m_InternalArray[index];
}
set
{
m_InternalArray[index] = value;
}
}
public void Add(Person item)
{
if (m_NumOfValues == m_InternalArray.Length)
{
Person[] copyArray = new Person[m_InternalArray.Length * 2];
m_InternalArray.CopyTo(copyArray, 0);
m_InternalArray = copyArray;
}
m_InternalArray[m_NumOfValues] = item;
m_NumOfValues++;
}
}

וכך באמת היה קורה באותם הימים. כך קיבלנו למשל DataRowCollection, DataTableCollection,DataColumnCollection ושלל מחלקות אחרות מFramework 1.0 שכולן פחות או יותר עם אותה פונקציונאליות, רק על Type שונה.

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

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
public class Collection
{
private object[] m_InternalArray = new object[4];
private int m_NumOfValues;
public object this[int index]
{
get
{
return m_InternalArray[index];
}
set
{
m_InternalArray[index] = value;
}
}
public void Add(object item)
{
if (m_NumOfValues == m_InternalArray.Length)
{
object[] copyArray = new object[m_InternalArray.Length * 2];
m_InternalArray.CopyTo(copyArray, 0);
m_InternalArray = copyArray;
}
m_InternalArray[m_NumOfValues] = item;
m_NumOfValues++;
}
}

אבל אז בכל גישה למחלקה היינו צריכים לעשות הסבה לint / Person / הסוג המבוקש של האוסף.

בנוסף, עבור primitive types היינו מקבלים boxing וunboxing בכל גישה (הכנסה והוצאה מobject), דבר שהיה גורע מהביצועים.

בFramework 2.0 אחד הדברים הכי חשובים שהכניסו זה Generics. הדבר מאפשר לנו לכתוב קוד גנרי ולתת למשתמש במחלקה שלנו להחליט מבחוץ מה יהיה הType.

הקוד שכתבנו קודם לכן יראה כך:

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
public class Collection<T>
{
private T[] m_InternalList = new T[4];
private int m_NumOfValues;
public T this[int index]
{
get
{
return m_InternalList[index];
}
set
{
m_InternalList[index] = value;
}
}
public void Add(T item)
{
if (m_NumOfValues == m_InternalList.Length)
{
T[] copyArray = new T[m_InternalList.Length*2];
m_InternalList.CopyTo(copyArray, 0);
m_InternalList = copyArray;
}
m_InternalList[m_NumOfValues] = item;
m_NumOfValues++;
}
}

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

1
2
3
4
5
6
7
8
Collection<Person> personCollection = new Collection<Person>();
// a collection of Person
Collection<int> intCollection = new Collection<int>();
// a collection of int
Collection<object> objectCollection = new Collection<object>();
// a collection of object

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

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

קצת על הsyntax: בתוך המחלקה אנחנו מציינים את הGeneric Types שלנו בסוגריים כאלה <> המופרדים בפסיקים ביניהם. אנחנו יכולים לתת כל שם שעולה על רוחנו, אבל מומלץ להשתמש בT אם יש רק Type אחד כזה בתור פרמטר, ואחרת בשם עם תחילית T.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Pair<TFirst, TSecond>
{
private TFirst m_First;
private TSecond m_Second;
public TFirst First
{
get { return m_First; }
set { m_First = value; }
}
public TSecond Second
{
get { return m_Second; }
set { m_Second = value; }
}
}

הדבר תקף גם לגבי ממשקים. נוכל להגדיר באופן זהה Generic Types ולקבל ממשק שבו מגדירים טיפוס מבחוץ. למשל:

1
2
3
4
5
6
7
8
9
10
11
12
private interface IPair<TFirst, TSecond>
{
TFirst First
{
get;
}
TSecond Second
{
get;
}
}

בנוסף לFeature המהפכני, קיבלנו את הnamespace ששמו System.Collections.Generic בו ממשקים ומימושים עם Generics, ביניהם List, Dictionary ושאר הממשקים והטיפוסים שכבר למדנו לאהוב.

שבוע גנרי טוב

שתף

25. Property accessors modifiers

לפעמים יש לנו Property במחלקה שיש לו getter וsetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int m_MyField;
public int MyProperty
{
get
{
// Write get code here
return m_MyField;
}
set
{
// Write set code here
m_MyField = value;
}
}

מדי פעם משיקולים כאלה ואחרים, אנחנו לא רוצים שהModifier של הgetter והsetter יהיה זהה, למשל שאחד יהיה public והשני יהיה private.

ניתן לפתור בעיה זו ע"י הוספת הModifier לפני הget/set:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int MyProperty
{
get
{
// Write getter code here
return m_MyField;
}
private set
{
// Write setter code here
m_MyField = value;
}
}

אנחנו מקבלים Property בשם MyProperty שהgetter שלו הוא public והsetter שלו הוא private.

כמובן, נוכל להחליף את הModifierים של public וprivate בכל שני Modifierים שונים זה מזה אחרים.

יש שתי הגבלות שצריך לדעת:

  • לא נוכל לתת לשני הAccessorים modifierים
  • לא נוכל לתת Modifier שמאפשר חשיפה יותר גבוהה מהmodifier של הProperty. למשל, לא נוכל שהProperty יהיה private ושהgetter יהיה public. לעומת זאת, נוכל שהProperty יהיה protected והsetter יהיה private.

סופ"ש מצוין

שתף

24. @variable names

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

1
int int = 3;

כלומר להגדיר משתנה בשם של Keyword.

האמת שזה לא מדויק, כי בC# 2.0/3.0 הוסיפו Keywordים חדשים (למשל yield, select, from, var ועוד), ולמען תאימות לאחור, חלק מהkeywordים, ביניהם החדשים גורמים לשגיאה רק אם משתמשים בהם בcontext הנכון.

למשל אין בעיה להגדיר משתנה בשם select כל עוד זה לא בscope של שאילתת LINQ:

1
2
3
4
5
string select = "SELECT * FROM TABLE"; // Compiles
from select in Collection
select select;
// Doesn't compile

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

לפעמים גם רוצים לתת שם שהוא CLSCompliant, כלומר יעבוד גם בשפות netיות אחרות.

יש תמיכה בזה ברמת השפה:

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

1
int @int = 3;

תוכלו לומר "יופי, הוספת תחילית @. יכולת להוסיף גם תחילית _".

אז זהו שיש לזה משמעות מעבר: אפשר לרשום למשל את הקוד הזה:

1
2
3
int @number = 3;
Console.WriteLine(number);
// Compiles

ואת הקוד הבא:

1
2
3
int number = 3;
Console.WriteLine(@number);
// Compiles

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

הקומפיילר מזהה את שני המשתנים כאותו משתנה.

הוכחה נוספת לכך היא שהקוד הבא לא מתקמפל:

1
2
3
int @number = 3;
int number = 3;
// Doesn't compile

גם בשגיאות ובwarning שאנחנו מקבלים הקומפיילר מסיר את ה@

1
int @int = 3;

The variable ‘int’ is assigned but its value is never used

באופן כללי אני ממליץ שלא לתת למשתנים שמות של מילים שמורות, אבל יש אנשים שאוהבים לעשות את זה. למשל, לקרוא לערך החזר של פונקציה @return. 😕

או לחלופין לתת לשם של משתנה/פרמטר את השם @event.

שתף

23. @Verbatim string

מדי פעם אנחנו צריכים לכתוב תווים מיוחדים, למשל \ (קרי Backslash – באק סלאש) “, אנטר בקוד.

למשל

1
string workspaceFolder = "C:\workspace\directory";

מימי C העליזים אנחנו יודעים שאי אפשר בדיוק לכתוב ככה, מאחר ו\ זה escape character.

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

1
string workspaceFolder = "C:\\workspace\\directory ";

כדי לכתוב מחרוזת המכילה " היינו צריכים לכתוב ככה:

1
string quote = "\"The table doesn't lie.\" Itay N";

כאשר במקום " אנחנו כותבים \"

ואנטר היינו צריכים לכתוב \n במקום אנטר.

בסה"כ כדי לכתוב משפט שמשלב את כל הtokenים היינו צריכים לכתוב בצורה הבאה:

1
string quote = "\"The root of evil is K:\\.\" Some anonymous guy.\nMarch 2007";

בC# המציאו שיטה נחמדה כדי לפתור את הבעיה הזו.

אפשר להוסיף @ לפני המחרוזת

נוכל לכתוב את הדוגמה הראשונה כך:

1
string workspaceFolder = @"C:\workspace\directory";

את השנייה כך:

1
string quote = @"""The table doesn't lie."" Itay N";

שימו לב שבמקום " אנחנו כותבים "" (כלומר " פעמיים).

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

1
2
string quote = @"""The root of evil is K:\."" Some anonymous guy.
March 2007";

שימו לב שזה שובר את ההזחה (אינדנטציה) ולכן כדאי לשקול שימוש בזה.

שתף

22. FlagsAttribute

הטיפ האחרון לזמן הקרוב על enumים:

מדי פעם אנחנו יוצרים enum שאנחנו מעוניינים שיהיו אפשרויות של צירוף מספר ערכים מתוכו.

למשל, enum המציין כיוון:

1
2
3
4
5
6
7
8
public enum Direction
{
None,
North,
South,
East,
West
}

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

1
2
PrintDirectionData(Direction.West |
Direction.North);

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

כל ערך רצוי לקבל בenum הוא חזקה של 2.

ערך של אף אפשרות מקבל את הערך 0.

לדוגמה:

1
2
3
4
5
6
7
8
public enum Direction
{
None = 0,
North = 1,
South = 2,
East = 4,
West = 8
}

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

1
2
3
4
if ((direction & Direction.South) == Direction.South)
{
// ...
}

הקוד לעיל בודק האם המשתמש שלח אפשרות שמכילה Direction.South.

הסיבה שהדבר הזה עובד היא מכיוון שבבינארית האפשרויות נראות כך:

1
2
3
4
North = 0b0001,
South = 0b0010,
East = 0b0100,
West = 0b1000

(הכתיב הוא בסינטקס של C# 7.0)

וכשמשלבים שני דגלים מקבלים ערך שמופיעה בו "נורה" בדיוק בדגלים ששילבנו. דרך נוחה לבדוק האם "נורה" דולקת היא לעשות AND עם ה"נורה" ולבדוק שקיבלנו את ערך הנורה (או לחלופין לבדוק שקיבלנו ערך שאינו 0).

הכל נראה טוב ויפה, אך ישנה בעיה. שימו לב לקוד הבא:

1
2
3
4
5
6
7
Direction west = Direction.West;
Direction southEast = Direction.South |
Direction.East;
Console.WriteLine(west); // prints West
Console.WriteLine(southEast); //prints 6

שימו לב שהשורה השנייה מדפיסה 6, בניגוד לשורה הראשונה שמדפיסה את שם הenum.

מהסיבה הזו ועוד סיבות דומות, המציאו הAttribute ששמו FlagsAttribute.

אם נשים כעת את הAttribute הזה מעל הenum ונריץ את הקוד, נקבל:

1
2
3
4
5
6
7
Direction west = Direction.West;
Direction southEast = Direction.South |
Direction.East;
Console.WriteLine(west); // prints West
Console.WriteLine(southEast); //prints South, East

הAttribute הזה אומר לFramework שהenum הוא בעצם enum של דגלים ("נורות") ונותן לנו תמיכה מתאימה בכמה פונקציות של enumים.

המשך יום טוב

שתף