تمرّر C# افتراضيًا كل وسيط بالقيمة: تحصل الدالة على نسخة، والإسناد إلى المعامل لا يؤثر في المستدعي. أما المعدّلات ref وout وin فتمرّر متغيّر المستدعي نفسه بدلًا من ذلك.
المخرجات:
by value: 50
by ref: 60
تظهر الكلمة مرتين: في قائمة المعاملات (ref int score) وفي موضع الاستدعاء (ref score). كتابتها عند الاستدعاء إلزامية، وهذا مقصود: كل من يقرأ AddPoints(ref score) يعرف أن score قد تتغيّر. انسَها فيبلّغ المترجم بـ CS1620، Argument 1 must be passed with the 'ref' keyword.
ref: قراءة متغيّر المستدعي والكتابة فيه
معامل ref اسم بديل لمتغيّر المستدعي. تستطيع الدالة قراءة القيمة الحالية واستبدالها. المثال التقليدي هو التبديل، المستحيل بالمعاملات العادية:
المخرجات:
Linus Ada
volume 100
قواعد ref:
- يجب أن يكون الوسيط متغيّرًا (محليًا أو معاملًا أو حقلًا أو عنصر مصفوفة)، لا قيمة حرفية أو تعبيرًا. والخاصية لا تصلح أيضًا:
Clamp(ref player.Volume, 0, 100)هو الخطأ CS0206،A non ref-returning property or indexer may not be used as an out or ref value. انسخ الخاصية إلى متغيّر محلي، ومرّره، ثم أسنده إليها. - يجب أن يكون المتغيّر مُسندًا إليه بشكل مؤكّد قبل الاستدعاء.
int n; Clamp(ref n, 0, 10);هو CS0165،Use of unassigned local variable 'n'.
out: النتائج الإضافية
out للقيم التي تنتجها الدالة. لا يحتاج المستدعي إلى تهيئة المتغيّر، ويجب على الدالة الإسناد إليه في كل مسار قبل العودة:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
داخل الدالة يُعدّ معامل out غير مُسند حتى تكتب فيه، فلا تستطيع قراءة القيمة القديمة للمستدعي. هذا هو الفرق عن ref: ref باتجاهين، وout باتجاه واحد من الدالة إلى المستدعي.
نمط TryParse
أشهر استخدامات out هو نمط Try في المكتبة الأساسية: أعد bool للنجاح وضع النتيجة في معامل out، فلا ترمي المدخلات السيئة استثناءً أبدًا:
المخرجات:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
تصرّح out var quantity (C# 7.0) بالمتغيّر داخل الاستدعاء نفسه؛ ويأتي نوعه من المعامل. عند الفشل تضبطه TryParse على 0 وتعيد false. وحين لا يهمّك إلا نجاح القراءة أهمل القيمة بـ out _: bool isNumber = int.TryParse(input, out _);.
لاحظ كيف تُسند TryGetDiscount إلى rate في المسارين: مرة صراحة ومرة بتمريرها إلى Dictionary.TryGetValue، التي هي نفسها استدعاء out.
out مقابل إعادة صف
قبل C# 7 كانت out الطريقة الرئيسية لإعادة عدة قيم. أما الآن فالصفوف تفعل ذلك بشكل أكثر مباشرة:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
استخدم صفًا (أو صنفًا صغيرًا) للنتائج المتعددة العادية. واحتفظ بـ out لنمط Try، حيث تتيح إعادة bool وضع الاستدعاء في شرط if.
الأنواع المرجعية: تغيير الكائن مقابل استبداله
تمرير مثيل صنف بالقيمة يتيح للدالة أصلًا تغيير الكائن، لأن القيمة المنسوخة مرجع إلى الكائن نفسه. تضيف ref قدرة أخرى: استبدال الكائن الذي يشير إليه متغيّر المستدعي.
المخرجات:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
وجّهت ResetByValue نسختها الخاصة من المرجع إلى قائمة جديدة؛ ولم يرَ المستدعي ذلك أبدًا. أما ResetByRef فغيّرت متغيّر المستدعي نفسه. في الشيفرة اليومية نادرًا ما تحتاج إلى ref على معامل صنف: أعد الكائن الجديد بدلًا من ذلك.
أما في البنى فالفرق أكبر، لأن وسيط البنية يُنسخ حقلًا حقلًا. الدالة التي يجب أن تعدّل بنية المستدعي (Point، أو بنية إعدادات كبيرة) تحتاج إلى ref.
in: للقراءة فقط بالمرجع
أضافت C# 7.2 المعدّل in، الذي يمرّر بالمرجع لكنه يمنع الإسناد. غرضه الأداء: لا تُنسخ البنية الكبيرة في كل استدعاء، ويُضمن للمستدعي أنها لن تتغيّر.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
في موضع الاستدعاء تكون in اختيارية: تعمل Trace(matrix) وTrace(in matrix) كلتاهما. أما مع int وdouble وDateTime وسائر الأنواع الصغيرة فلا تكسب in شيئًا وقد تكون أبطأ قليلًا؛ استخدمها للبنى ذات الحقول المتعددة التي تمرّرها كثيرًا.
القيود
- لا يمكن أن تكون معاملات
refوoutاختيارية (CS1741). أما معاملinفيمكن:static void Connect(in int retries = 3)صحيحة. - لا يمكن للأشكال المحمّلة أن تختلف فقط بين
refوout(أوin)، لأنها متطابقة على مستوى وقت التشغيل. أماF(int x)وF(ref int x)فيمكن أن تتعايشا. - لا يمكن أن تملك دوال
asyncوالمكرِّرات (الدوال التي تستخدمyield return) معاملاتrefأوoutأوin. أعد منها صفًا أو كائن نتيجة بدلًا من ذلك. - لا يمكن لتعبير lambda أو دالة محلية التقاط معامل
refأوoutأوinللدالة المحيطة؛ انسخه إلى متغيّر محلي أولًا.
مقارنة ref وout وin
| يجب أن يُسند المستدعي أولًا | يجب أن تُسند الدالة | يمكن للدالة القراءة | الكلمة في موضع الاستدعاء | |
|---|---|---|---|---|
| (بلا معدّل) | نعم | لا | نعم (نسخة) | لا شيء |
ref | نعم | لا | نعم | إلزامية |
out | لا | نعم، في كل مسار | فقط بعد الإسناد | إلزامية |
in | نعم | غير مسموح | نعم | اختيارية |
الأسئلة الشائعة
ما الفرق بين ref وout في C#؟
كلاهما يمرّر متغيّر المستدعي نفسه لا نسخة منه. مع ref يجب أن يُسند إلى المتغيّر قبل الاستدعاء ويمكن للدالة قراءته وتغييره. ومع out لا يلزم الإسناد إليه أولًا، ولا تستطيع الدالة قراءته قبل الكتابة فيه، ويجب أن تُسند إليه قبل العودة (وإلا CS0177). تعني out أن "هذه نتيجة إضافية".
ماذا تعني out var في C#؟
منذ C# 7.0 يمكنك التصريح بمتغيّر out داخل الاستدعاء: if (int.TryParse(text, out var number)). يُستنتج نوع المتغيّر من المعامل، ويبقى في النطاق بعد العبارة أيضًا. وتُهمل out _ نتيجة لا تحتاجها.
هل تُمرَّر الكائنات بالمرجع في C#؟
لا، كل وسيط يُمرَّر بالقيمة ما لم تكتب ref أو out أو in. في الصنف تكون القيمة المنسوخة مرجعًا، فتستطيع الدالة تغيير حقول الكائن ويرى المستدعي ذلك. ما لا تستطيعه دون ref هو جعل متغيّر المستدعي يشير إلى كائن آخر.
ما هو معدّل المعامل in في C#؟
تمرّر in (C# 7.2 وما بعده) وسيطًا بالمرجع لكن للقراءة فقط: لا تستطيع الدالة الإسناد إليه. وُجدت لتجنّب نسخ البنى الكبيرة في كل استدعاء. أما للأنواع الصغيرة مثل int أو DateTime فلا تجلب فائدة.
لماذا أحصل على "Argument 1 must be passed with the 'ref' keyword"؟
تصرّح الدالة بمعامل ref وحُذفت الكلمة من الاستدعاء. تتطلب C# كتابة ref (وout) في موضع الاستدعاء أيضًا، كي يرى القرّاء أن المتغيّر قد يتغيّر: Increment(ref count); لا Increment(count);.