منذ البداية ، دعم C # تمرير الوسائط حسب القيمة أو المرجع. ولكن قبل الإصدار 7 ، كان المحول البرمجي C # يدعم طريقة واحدة فقط لإرجاع قيمة من طريقة (أو خاصية) - الإرجاع حسب القيمة. في C # 7 ، تغير الوضع مع إدخال ميزتين جديدتين: مرجع العودة ومرجع السكان المحليين. المزيد عنهم وأدائهم - تحت الخفض.

الأسباب
هناك العديد من الاختلافات بين المصفوفات والمجموعات الأخرى من حيث وقت تشغيل اللغة العامة. منذ البداية ، كانت مصفوفات CLR مدعومة ، ويمكن اعتبارها وظيفة مضمنة. يمكن أن تعمل بيئة CLR ومترجم JIT مع المصفوفات ، ولديهم أيضًا ميزة أخرى: مفهرس الصفيف يقوم بإرجاع العناصر حسب المرجع وليس حسب القيمة.
لإثبات ذلك ، سيتعين علينا اللجوء إلى الطريقة المحظورة - استخدم نوع القيمة القابلة للتغيير:
public struct Mutable { private int _x; public Mutable(int x) => _x = x; public int X => _x; public void IncrementX() { _x++; } } [Test] public void CheckMutability() { var ma = new[] {new Mutable(1)}; ma[0].IncrementX();
سينجح الاختبار لأن مفهرس الصفيف يختلف اختلافًا كبيرًا عن مفهرس القائمة.
يعطي المترجم C # تعليمات خاصة لمفهرس المصفوفة - ldelema ، والتي تُرجع ارتباطًا مُدارًا لعنصر من هذه المصفوفة. بشكل أساسي ، يقوم مفهرس الصفيف بإرجاع عنصر حسب المرجع. ومع ذلك ، لا يمكن أن تتصرف List بنفس الطريقة ، لأنه في C # لم يكن من الممكن * إرجاع اسم مستعار للحالة الداخلية. لذلك ، يقوم مفهرس القائمة بإرجاع عنصر حسب القيمة ، أي إرجاع نسخة من هذا العنصر.
* كما سنرى قريبًا ، لا يزال مفهرس القائمة لا يمكنه إرجاع عنصر حسب المرجع.
هذا يعني أن ma [0] .IncrementX () يستدعي الطريقة التي تعدل العنصر الأول للصفيف ، بينما ml [0] .IncrementX () يستدعي الطريقة التي تعدل نسخة العنصر دون التأثير على القائمة الأصلية.
قيم الإرجاع والمتغيرات المحلية المرجعية: الأساسيات
معنى هذه الوظائف بسيط للغاية: يسمح لك الإعلان عن القيمة المرجعية التي تم إرجاعها بإرجاع الاسم المستعار لمتغير موجود ، ويمكن للمتغير المحلي المرجعي تخزين هذا الاسم المستعار.
1. مثال بسيط:
[Test] public void RefLocalsAndRefReturnsBasics() { int[] array = { 1, 2 };
2. إرجاع القيم المرجعية ومعدل القراءة فقط
يمكن للقيمة المرجعية التي تم إرجاعها إرجاع الاسم المستعار لحقل المثيل ، وبدءًا من C # الإصدار 7.2 ، يمكنك إرجاع الاسم المستعار دون أن تتمكن من الكتابة إلى الكائن المقابل باستخدام معدِّل المرجع للقراءة فقط:
class EncapsulationWentWrong { private readonly Guid _guid; private int _x; public EncapsulationWentWrong(int x) => _x = x;
- يمكن أن تُرجع الأساليب والخصائص "الاسم المستعار" للحالة الداخلية. في هذه الحالة ، يجب ألا يتم تعريف طريقة المهمة للخاصية.
- يؤدي الرجوع عن طريق المرجع إلى كسر التغليف ، حيث يكتسب العميل السيطرة الكاملة على الحالة الداخلية للكائن.
- يؤدي الرجوع عبر رابط للقراءة فقط إلى تجنب نسخ أنواع القيم دون داع ، مع عدم السماح للعميل بتغيير الحالة الداخلية.
- يمكن استخدام ارتباطات القراءة فقط لأنواع المراجع ، على الرغم من أن هذا لا معنى له في الحالات غير القياسية.
3. القيود القائمة. يمكن أن يكون إرجاع اسم مستعار خطرًا: سيؤدي استخدام اسم مستعار لمتغير يتم وضعه على المكدس بعد اكتمال الطريقة إلى تعطل التطبيق. لجعل هذه الوظيفة آمنة ، يطبق المترجم C # قيود مختلفة:
- غير قادر على إعادة الارتباط إلى المتغير المحلي.
- غير قادر على إرجاع مرجع لهذا في الهياكل.
- يمكنك إرجاع ارتباط إلى متغير موجود في كومة الذاكرة المؤقتة (على سبيل المثال ، إلى عضو في الفصل).
- يمكنك إرجاع رابط إلى معلمات المرجع / الخروج.
لمزيد من المعلومات ، نوصيك بالاطلاع على المنشور الممتاز
لقواعد الإرجاع الآمنة لإرجاع المرجع . مؤلف المقالة ، فلاديمير سادوف ، هو منشئ دالة مرجع الرجوع لمترجم C #.
الآن بعد أن أصبح لدينا فكرة عامة عن القيم المرجعية التي تم إرجاعها والمتغيرات المحلية المرجعية ، دعنا نلقي نظرة على كيفية استخدامها.
استخدام القيم المرجعية التي تم إرجاعها في الفهارس
لاختبار تأثير هذه الوظائف على الأداء ، سنقوم بإنشاء مجموعة فريدة غير قابلة للتغيير تسمى NaiveImmutableList <T> ومقارنتها مع T [] وقائمة للهياكل ذات الأحجام المختلفة (4 و 16 و 32 و 48).
public class NaiveImmutableList<T> { private readonly int _length; private readonly T[] _data; public NaiveImmutableList(params T[] data) => (_data, _length) = (data, data.Length); public ref readonly T this[int idx]
يتم إجراء اختبار الأداء لجميع المجموعات ويضيف كل قيم الخاصية N لكل عنصر:
private const int elementsCount = 100_000; private static LargeStruct_48[] CreateArray_48() => Enumerable.Range(1, elementsCount).Select(v => new LargeStruct_48(v)).ToArray(); private readonly LargeStruct_48[] _array48 = CreateArray_48(); [BenchmarkCategory("BigStruct_48")] [Benchmark(Baseline = true)] public int TestArray_48() { int result = 0;
النتائج هي كما يلي:

على ما يبدو ، هناك خطأ ما! أداء مجموعة NaiveImmutableList <T> الخاصة بنا هو نفسه القائمة. ماذا حدث؟
إرجاع القيم مع معدّل للقراءة فقط: كيف يعمل
كما ترى ، تقوم مفهرس NaiveImmutableList <T> بإرجاع ارتباط للقراءة فقط باستخدام معدّل المرجع للقراءة فقط. هذا مبرر تمامًا ، لأننا نريد الحد من قدرة العملاء على تغيير الحالة الأساسية لمجموعة ثابتة. ومع ذلك ، فإن الهياكل التي نستخدمها في اختبار الأداء ليست قابلة للقراءة فقط.
سيساعدنا هذا الاختبار على فهم السلوك الأساسي:
[Test] public void CheckMutabilityForNaiveImmutableList() { var ml = new NaiveImmutableList<Mutable>(new Mutable(1)); ml[0].IncrementX();
فشل الاختبار! لكن لماذا؟ لأن بنية "روابط للقراءة فقط" تشبه بنية في المعدلات والمجالات للقراءة فقط فيما يتعلق بالبنى: يولد المترجم نسخة واقية في كل مرة يتم فيها استخدام عنصر هيكل. هذا يعني أن مل [0]. لا يزال يُنشئ نسخة من العنصر الأول ، لكن المفهرس لا يقوم بذلك: يتم إنشاء النسخة عند نقطة الاستدعاء.
هذا السلوك منطقي بالفعل. يدعم المترجم C # تمرير الوسيطات بالقيمة والمرجع وبواسطة "رابط للقراءة فقط" باستخدام المعدل في (لمزيد من التفاصيل ، انظر
في المعدّل والبنيات للقراءة فقط في C # ("في المعدِّل والبنى للقراءة فقط في C # ")). يدعم المترجم الآن ثلاث طرق مختلفة لإرجاع قيمة من طريقة: حسب القيمة ، حسب المرجع ، ورابط للقراءة فقط.
تشبه روابط القراءة فقط الروابط العادية إلى حد أن المترجم يستخدم نفس InAttribute للتمييز بين قيم الإرجاع الخاصة بهم:
private int _n; public ref readonly int ByReadonlyRef() => ref _n;
في هذه الحالة ، يتم تجميع طريقة ByReadonlyRef بكفاءة في:
[InAttribute] [return: IsReadOnly] public int* ByReadonlyRef() { return ref this._n; }
التشابه بين المعدل في الرابط والقراءة فقط يعني أن هذه الوظائف ليست مناسبة جدًا للهياكل العادية ويمكن أن تسبب مشاكل في الأداء. فكر في مثال:
public struct BigStruct {
إلى جانب بناء الجملة غير المعتاد عند الإعلان عن متغير لـ bigStruct ، يبدو الرمز جيدًا. الهدف واضح: ترجع BigStruct حسب المرجع لأسباب تتعلق بالأداء. لسوء الحظ ، نظرًا لأن بنية BigStruct قابلة للكتابة ، يتم إنشاء نسخة واقية في كل مرة يتم فيها الوصول إلى العنصر.
استخدام القيم المرجعية التي تم إرجاعها في الفهارس. المحاولة رقم 2
لنجرب نفس مجموعة الاختبارات للهياكل للقراءة فقط بأحجام مختلفة:

الآن النتائج أكثر منطقية. لا يزال وقت المعالجة يتزايد للهياكل الكبيرة ، ولكن هذا متوقع ، لأن معالجة أكثر من 100 ألف من الهياكل الأكبر تستغرق وقتًا أطول. ولكن الآن وقت التشغيل لـ NaiveimmutableList <T> قريب جدًا من الوقت T [] وأفضل بكثير من حالة List.
الخلاصة
- يجب التعامل مع القيم المرجعية التي تم إرجاعها بعناية لأنها يمكن أن تكسر التغليف.
- تكون القيم المرجعية التي تم إرجاعها مع مُعدِّل للقراءة فقط فعالة للهياكل للقراءة فقط. في حالة الهياكل التقليدية ، قد تحدث مشاكل في الأداء.
- عند العمل باستخدام الهياكل القابلة للكتابة ، تقوم القيم المرجعية التي تم إرجاعها باستخدام معدّل القراءة فقط بإنشاء نسخة واقية في كل مرة يتم فيها استخدام المتغير ، مما قد يتسبب في مشاكل في الأداء.
تعد القيم المرجعية التي تم إرجاعها والمتغيرات المحلية المرجعية وظائف مفيدة لمنشئي المكتبات ومطوري كود البنية التحتية. ومع ذلك ، من الخطورة جدًا استخدامها في رمز المكتبة: من أجل استخدام مجموعة تقوم بإرجاع العناصر بشكل فعال باستخدام ارتباط للقراءة فقط ، يجب على كل مستخدم مكتبة أن يتذكر: رابط للقراءة فقط إلى بنية قابلة للكتابة ينشئ نسخة واقية "عند نقطة الاتصال ". في أفضل الأحوال ، سيؤدي هذا إلى إبطال زيادة محتملة في الإنتاجية ، وفي أسوأ الأحوال سيؤدي إلى تدهور خطير إذا تم في نفس الوقت تقديم عدد كبير من الطلبات لمتغير محلي مرجعي واحد ، للقراءة فقط.
ستظهر روابط PS للقراءة فقط في BCL. تم عرض طرق المرجع للقراءة فقط للوصول إلى العناصر في مجموعات ثابتة في الطلب التالي لدمج التغييرات في corefx repo (
تنفيذ ItemRef API Proposal ("اقتراح لتضمين ItemRef API")). لذلك ، من المهم جدًا أن يفهم الجميع ميزات استخدام هذه الوظائف وكيف ومتى يجب تطبيقها.