يجمع الصف (tuple) عددًا صغيرًا ثابتًا من القيم في قيمة واحدة دون التصريح بصنف لها. ("Ana", 31) صف من string وint. الصفوف هي الطريقة المألوفة لإعادة شيئين أو ثلاثة من دالة ولحمل أزواج قصيرة العمر داخل دالة.
إنشاء صف
يُكتب النوع بين أقواس أيضًا: (string, int) نوع صف عنصره الأول string والثاني int.
المخرجات:
Ana is 31
(3, 4)
179.70
(Ana, 32)
دون أسماء تكون العناصر Item1 وItem2 وItem3 وهكذا. هذه الصفوف بنى System.ValueTuple، فإسناد واحد إلى متغيّر آخر ينسخ القيم، وتغيير النسخة يترك الأصل كما هو.
العناصر المسمّاة
Item1 لا تقول شيئًا عن ماهية القيمة. أعطِ العناصر أسماء، إما في النوع أو في القيمة الحرفية:
المخرجات:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
الأسماء تسهيل وقت الترجمة. وقت التشغيل يكون الصفان أعلاه قيمتي ValueTuple<string, int> عاديتين، ولهذا يُترجم إسناد person إلى صف بأسماء مختلفة: لا يجب أن يتطابق إلا أنواع العناصر وترتيبها.
تستنتج C# 7.1 وما بعدها أيضًا الأسماء من المتغيّرات في القيمة الحرفية، فتعطيك var t = (name, age); العنصرين t.name وt.age دون كتابتهما مرتين. وفي C# 7.0 تكون تلك العناصر Item1 وItem2 فقط.
إعادة عدة قيم من دالة
هذا هو السبب الرئيسي لإضافة الصفوف إلى اللغة. قبلها كانت الخيارات معاملات out، أو صنفًا لمرة واحدة، أو System.Tuple بعناصره المجهولة Item1.
المخرجات:
Lowest 60, highest 95
Average 78.75
تصبح أسماء العناصر في نوع الإرجاع الأسماء التي يراها المستدعي. قارن الدالة نفسها بمعاملات out:
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
تبقى معاملات out العُرف لشكل TryParse، حيث تقول bool هل نجحت العملية وتعود القيمة معها. أما لـ "احسب عدة أشياء وأعدها" فالصف أسهل قراءة. راجع ref وout.
التفكيك
يفكّ التفكيك (deconstruction) الصف إلى متغيّرات منفصلة في عبارة واحدة:
المخرجات:
Lima 24.5
Lima
24.5
2 1
أسماء المتغيّرات في var (city, temp) من اختيارك؛ ولا يلزم أن تطابق أسماء عناصر الصف، لأن التفكيك يسير بالموضع. ويبني التبديل (a, b) = (b, a) صفًا من القيم القديمة أولًا ثم يُسند، فلا تضيع أي قيمة.
يمكن لأصنافك أن تدعم التفكيك بالتصريح بدالة Deconstruct بمعاملات out. ومنذ C# 7 مع .NET Core 2.0 أو أحدث يملك KeyValuePair واحدة أيضًا، فيمكن لحلقة قاموس أن تفكّ كل مدخل في ترويستها:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
var (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
ويعمل الشكل نفسه foreach (var (a, b) in listOfTuples) مع قائمة من الصفوف. وأخذ الصف كله كمتغيّر حلقة واحد وقراءة عناصره المسمّاة، كما يفعل القسم التالي، يعمل بالقدر نفسه.
الصفوف في القوائم وLINQ والقواميس
قائمة من الصفوف المسمّاة طريقة خفيفة لحمل صفوف بيانات داخل دالة:
المخرجات:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ينفّذ ValueTuple الدالتين Equals وGetHashCode عنصرًا عنصرًا، فيجد (2, 7) المبني في مكانين مختلفين مدخل القاموس نفسه. هذا يجعل الصف أبسط مفتاح مركّب: لا صنف، ولا دالة تجزئة مكتوبة يدويًا، ولا ضمّ نصوص مثل row + ":" + col.
مساواة الصفوف
تقارن Equals عنصرًا عنصرًا وتعمل منذ ظهور الصفوف:
المخرجات:
True
False
أضافت C# 7.3 المعاملين == و!= للصفوف، اللذين يُترجمان إلى المقارنة نفسها عنصرًا عنصرًا ويتجاهلان الأسماء أيضًا:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple مقابل ValueTuple
قدّمت .NET 4 النوع System.Tuple، وهو صنف يُنشأ بـ Tuple.Create. ما زال في الإطار وما زال يظهر في الشيفرات وواجهات API الأقدم، ولهذا تنتهي عمليات البحث عن "C# tuple" غالبًا عنده.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| الصياغة | (1, "a")، (int, string) | Tuple.Create(1, "a")، Tuple<int, string> |
| النوع | بنية (نوع قيمة) | صنف (نوع مرجعي) |
| أسماء العناصر | نعم | لا، فقط Item1 وItem2 |
| قابل للتغيير | نعم، العناصر حقول | لا، خصائص للقراءة فقط |
| التفكيك | نعم | نعم، عبر دوال التوسيع |
المخرجات:
Ben 25
(Ben, 25)
تحوّل ToValueTuple() وToTuple() بين الاثنين. فضّل value tuples في الشيفرة الجديدة: لا تحجز شيئًا في الكومة ويمكنها حمل الأسماء.
متى تستخدم صنفًا أو سجلًا بدلًا منها
الصفوف أفضل حين يكون التجميع محليًا وبديهيًا: دالة تعيد رقمين، أو استعلام LINQ يحمل زوجًا إلى الخطوة التالية، أو قاموس يحتاج إلى مفتاح من جزأين. انتقل إلى نوع مسمّى حين:
- يظهر الشكل نفسه في عدة تواقيع دوال عامة.
(string, string, int)في خمسة أماكن صنف لم يُكتب بعد. - تحتاج القيم إلى سلوك أو تحقق أو أكثر من ثلاثة أو أربعة عناصر.
- تعبر البيانات حدًّا مثل التسلسل. الأسماء لا توجد وقت التشغيل، فيكتب Newtonsoft.Json الحقلين
Item1وItem2، ويكتبSystem.Text.Jsonالقيمة{}لأن عناصر الصف حقول، وهو يتخطاها افتراضيًا.
تمنحك سجلات C# 9 معظم راحة الصف باسم حقيقي. record Score(string Name, int Points); لها مساواة بالقيمة وToString مقروءة وتفكيك مدمج. راجع السجلات.
أخطاء شائعة
- توقّع بقاء الأسماء وقت التشغيل. يرى الانعكاس وأدوات التسلسل
Item1وItem2(أو لا شيء إطلاقًا). - تعديل نسخة. الصفوف بنى:
var t2 = t1; t2.Item1 = 5;يتركt1دون تغيير. - صفوف بعناصر كثيرة. بعد ثلاثة أو أربعة يكون النوع المسمّى أسهل قراءة.
- الخلط في ترتيب التفكيك. التفكيك موضعي: مع صف
(Name, Age)تضعvar (age, name) = person;الاسم فيage، ولأنvarتستنتج الأنواع فلا شيء يحذّرك.
الأسئلة الشائعة
كيف أعيد عدة قيم من دالة في C#؟
أعد صفًا: صرّح بنوع الإرجاع كـ (int Min, int Max) واكتب return (lowest, highest);. يقرأ المستدعي result.Min وresult.Max، أو يفكّك بـ var (min, max) = MinMax(data);. البدائل هي معاملات out، التي تناسب نمط TryParse، أو صنف صغير أو سجل حين يكون للنتيجة معنى خاص بها.
ما هو الصف المسمّى في C#؟
صف لعناصره أسماء بدل Item1 وItem2: يتيح لك (string Name, int Age) person = ("Ana", 31); كتابة person.Name. الأسماء موجودة وقت الترجمة فقط؛ في الأساس ما زال ValueTuple<string, int>، وما زالت Item1 تعمل.
ما الفرق بين Tuple وValueTuple في C#؟
System.Tuple (من .NET 4) صنف: يُحجز في الكومة، وغير قابل للتغيير، وعناصره دائمًا Item1 وItem2 فقط. أما System.ValueTuple (C# 7) فبنية بحقول قابلة للتغيير، تدعم أسماء العناصر وصياغة (a, b)، وهي ما ينشئه كل صف حرفي. استخدم value tuples في الشيفرة الجديدة.
كيف يعمل تفكيك الصفوف في C#؟
تصرّح var (name, age) = person; بمتغيّرين وتُسند إليهما عناصر الصف بالترتيب. ويمكنك أيضًا الإسناد إلى متغيّرات موجودة بـ (a, b) = (b, a)، الذي يبدّلهما، وتخطي عناصر بالمُهمَل _: var (_, age) = person;.
هل يمكنني مقارنة الصفوف بـ == في C#؟
منذ C# 7.3 تقارن == و!= الصفوف عنصرًا عنصرًا: (1, "a") == (1, "a") تساوي true. تُتجاهل الأسماء؛ لا يهمّ إلا المواضع والقيم. وقبل C# 7.3 استخدم Equals، التي تنفّذها value tuples بالطريقة نفسها.