4 min. read

הפיצ’ר הזה עובר בין הכסאות אצל הרבה אנשים ואני חושב שיש לו שימושים מעולים אך גם שימוש לרעה שצריך להיזהר ממנו.

Tuple

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

1
2
3
4
5
6
7
public void SendData(Tuple<int,byte[]> information)
{
var headerInfo = information.Item1;
var bytes = information.Item2;

// ....
}

השימוש ב-Item1 ו-Item2 מונע מאיתנו להעניק שמות אינדיקטיביים ולהבין יותר טוב את הקוד שלנו.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Tuple<int,byte[]> GenerateMessage(string header, string info)
{
var headerMsg = GetHeader(header);
var bytes = ToBytes(info);
return new Tuple<int,byte[]>(headerMsg, bytes);
}

public void OtherFunc()
{
var info = GenerateMessage("Message", "Welcome to Simply Code");

// Somewhere in the code
var header = info.Item1;
}

השימוש ב-Item1 ו-Item2 לא מספיק מובנים ובדרך כלל יכולים לגרום ליותר צרות אם נעשה שימוש נרחב.

לכאן מגיע הפיצ’ר החדש - Deconstruction.

Deconstructors

זה לא - Destructors.
הפעולה מבצעת את ההפך מבנאי - יוצר משתנים ממשתנה אחד.

1
2
3
4
5
6
7
8
9
10
11
12
13
public (int age, string name) GetPersonInfo()
{
var age = 48;
var name = "Bob";
return (age, name);
}

public void SomeFunc()
{
(int age,string name) = GetPersonInfo();

// Instead of Item1 and Item2 we get 'age' and 'name'.
}

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

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

Class Deconstructors

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

public void Deconstructor(out int age, out string name)
{
age = Age;
name = Name;
}
}

// Usage:

Person p = new Person() { Age = 48, Name = "Bob" };

(int age, string name) = p;

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

1
2
3
4
5
6
7
8
public static class PersonExtensions
{
public static void Deconstructor(this Person person, out int age, out string name)
{
age = person.Age;
name = person.Name;
}
}

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

הקוד צריך להיות פתוח לשדרוג וסגור לשינויים.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Dog
{
// Properties

public void Deconstruct(out string name, out int age, out int weight, out string says)
{
name = Name;
age = Age;
weight = Weight;
says = Says;
}
}

Dog d = new Dog();
// תחביר מקוצר
var (name, age, _, _) = d;
Console.WriteLine(name);

מה כדאי לעשות עם הפיצ’ר הזה

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

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

1
2
3
4
5
public (bool success, int value) ParseMessage(string message)
{
// Some code...
return (success, value);
}
  • לבצע Refactor לקוד שמשתמש בטאפלים.
1
2
3
4
5
6
7
8
9
public Tuple<int,string> GetInfo(Dog d)
{
return new Tuple<int,string>(d.Age, d.Name);
}

public (int age, string name) GetInfo(Dog d)
{
return (d.Age, d.Name);
}

מה לא כדאי לעשות

  • לממש את הפונקציונליות בכל מחלקה
    לא כל מחלקה צריכה את זה!

    1
    2
    3
    4
    5
    public class Person
    {
    public int Age {get; set;}
    public string Name {get; set;}
    }

    במחלקה הזו אנחנו בדרך כלל לא נצטרך Deconstructor.

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

אני לא ארחיב על הנקודה הזו יותר, בC# 9.0 התווסף פיצ’ר חדש - Record Class שכדאי להשתמש בו יותר.
אבל אם אתם מסיבה מוזרה משתמשים ב-C# 8 ומטה, אל תמהרו להשתמש בטאפל בכל מקום.

למה זה התווסף רק ב-C# 7.0?

אחת הבעיות המרכזיות של טאפלים היו בעיות הביצועים שלהם.
מכיוון שטאפל היה מחלקה יצירה של טאפל היה יוצר את האובייקט על ה-Heap.
שימוש נרחב בטאפלים היה גורם להיווצרות הרבה אובייקטים קטנים שיצרו עומס רב על ה-Garbage Collector.

החל מ-.Net 4.7 יש לנו את ה-ValueTuple.
הטאפל הזה מוגדר כמבנה ולא כמחלקה והעתקה של הטאפל היא ע”י העתקת ערכים והיצירה שלו הוא על גבי המחסנית.

Benchmark - בדיקת ביצועים פשוטה

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
var time = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
Tuple<int, string> tup = new Tuple<int, string>(0, "");
}
time.Stop();
System.Console.WriteLine($"Tuple Time: {time.ElapsedMilliseconds}");
}

{
var time = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
ValueTuple<int, string> tup = new ValueTuple<int, string>(0, "");
}
time.Stop();
System.Console.WriteLine($"ValueTuple Time: {time.ElapsedMilliseconds}");
}

הרצה של זה ב-Debug ו-Release.
המעבד עליו רץ הקוד: Intel i7-7700HQ 2.8 Ghz.

1
2
3
4
5
6
7
dotnet run -c "Release":
Tuple Time: 77
ValueTuple Time: 3

dotnet run -c "Debug":
Tuple Time: 135
ValueTuple Time: 47

מה שונה בקוד הזה? הקריאה ליצירת האובייקט בקוד ה-IL.
יצירת Tuple:

1
2
3
4
5
// [14 21 - 14 76]
IL_000a: ldc.i4.0
IL_000b: ldstr ""
IL_0010: newobj instance void class [System.Runtime]System.Tuple`2<int32, string>::.ctor(!0/*int32*/, !1/*string*/)
IL_0015: pop

יצירת ValueTuple:

1
2
3
4
IL_004c: ldc.i4.0
IL_004d: ldstr ""
IL_0052: newobj instance void valuetype [System.Runtime]System.ValueTuple`2<int32, string>::.ctor(!0/*int32*/, !1/*string*/)
IL_0057: pop

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

תודה על הקריאה!


אהבתם? מוזמנים להביע תמיכה כאן: כוס קפה