[DotNetBook] Span: نوع بيانات .NET جديد

مع هذه المقالة ، أواصل نشر سلسلة من المقالات ، ستكون نتائجه كتابًا عن عمل .NET CLR ، و .NET بشكل عام (حوالي 200 صفحة من الكتاب جاهزة بالفعل ، لذلك نرحب بنهاية المقالة للروابط).


كل من اللغة والمنصة موجودان منذ سنوات عديدة: وطوال هذا الوقت كان هناك العديد من الأدوات للعمل مع التعليمات البرمجية غير المُدارة. إذن ، لماذا تأتي واجهة برمجة التطبيقات التالية للعمل مع التعليمات البرمجية غير المُدارة إذا كانت موجودة بالفعل لسنوات عديدة؟ للإجابة على هذا السؤال ، يكفي أن نفهم ما كان مفقودًا من قبل.


حاول مطورو المنصة مساعدتنا في زيادة سطوع الحياة اليومية للتطوير باستخدام موارد غير مُدارة: هذه هي أغلفة تلقائية للطرق المستوردة. والتنظيم ، والذي يعمل في معظم الحالات تلقائيًا. هذه أيضًا تعليمات stackallloc ، والتي تمت مناقشتها في الفصل الموجود على رصة الخيط. ومع ذلك ، بالنسبة لي ، إذا جاء المطورون الأوائل الذين يستخدمون C # من عالم C ++ (كما فعلت) ، فإنهم يأتون الآن من لغات عالية المستوى (على سبيل المثال ، أعرف مطورًا جاء من JavaScript). ماذا يعني هذا؟ هذا يعني أن الناس يشكون بشكل متزايد من الموارد غير المُدارة والبنيات التي تشبه روح C / C ++ وأكثر من ذلك إلى Assembler.


ملاحظة


لم يتم تحديث الفصل المنشور على حبري ، وربما يكون قديمًا بالفعل. وبالتالي ، يرجى الرجوع إلى النص الأصلي للحصول على نص أحدث:



نتيجة لمثل هذا الموقف ، هناك محتوى أقل وأقل من التعليمات البرمجية غير الآمنة في المشاريع وثقة متزايدة في واجهة برمجة التطبيقات للنظام الأساسي نفسه. يمكن التحقق من ذلك بسهولة من خلال النظر في استخدام stackalloc عبر المستودعات المفتوحة: إنه لا يكاد يذكر. ولكن إذا أخذت أي كود يستخدمه:


فصل دراسي Interop.ReadDir
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs


 unsafe { // s_readBufferSize is zero when the native implementation does not support reading into a buffer. byte* buffer = stackalloc byte[s_readBufferSize]; InternalDirectoryEntry temp; int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp); // We copy data into DirectoryEntry to ensure there are no dangling references. outputEntry = ret == 0 ? new DirectoryEntry() { InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType } : default(DirectoryEntry); return ret; } 

يصبح من الواضح سبب عدم الشعبية. انظر دون قراءة الكود وأجب على سؤال واحد: هل تثق به؟ أستطيع أن أفترض أن الجواب لا. ثم أجب عن الآخر: لماذا؟ سيكون الجواب واضحًا: بالإضافة إلى رؤية كلمة Dangerous ، التي تشير إلى حد ما إلى حدوث خطأ ما ، فإن العامل الثاني الذي يؤثر على موقفنا هو byte* buffer = stackalloc byte[s_readBufferSize]; line byte* buffer = stackalloc byte[s_readBufferSize]; ، وبشكل أكثر تحديدًا ، byte* . هذا السجل محفز لأي شخص حتى تظهر الفكرة في رأسي: "ماذا ، لا يمكن القيام به بشكل مختلف أم ماذا؟". ثم دعونا نلقي نظرة أكثر قليلاً على التحليل النفسي: لماذا قد تنشأ مثل هذه الفكرة؟ من ناحية ، نستخدم تراكيب اللغة والبناء المقترح هنا بعيد عن ، على سبيل المثال ، C ++ / CLI ، والذي يسمح لك بفعل أي شيء على الإطلاق (بما في ذلك الإدراجات في Assembler النقي) ، ومن ناحية أخرى ، يبدو غير عادي.


إذن ما هو السؤال؟ كيفية إعادة المطورين إلى حضن التعليمات البرمجية غير المُدارة؟ من الضروري منحهم إحساسًا بالهدوء لأنهم لا يستطيعون ارتكاب خطأ عن طريق الصدفة ، بسبب الجهل. فلماذا يتم تقديم Span<T> و Memory<T> ؟


Span [T] ، ReadOnlySpan [T]


يمثل نوع Span جزءًا من صفيف بيانات معين ، وهو نطاق فرعي من قيمه. في نفس الوقت ، السماح ، كما في حالة المصفوفة ، بالعمل مع عناصر من هذا النطاق للكتابة والقراءة. ومع ذلك ، من أجل زيادة سرعة التشغيل والفهم العام ، دعنا نقارن أنواع البيانات التي يتم من خلالها تنفيذ نوع Span ونلقي نظرة على الأغراض المحتملة لتقديمه.


النوع الأول من البيانات التي تريد التحدث عنها هو مجموعة منتظمة. بالنسبة للمصفوفات ، سيبدو العمل مع Span كما يلي:


  var array = new [] {1,2,3,4,5,6}; var span = new Span<int>(array, 1, 3); var position = span.BinarySearch(3); Console.WriteLine(span[position]); // -> 3 

كما نرى في هذا المثال ، كبداية ننشئ مصفوفة بيانات معينة. بعد ذلك ، نقوم بإنشاء Span (أو مجموعة فرعية) ، والتي ، تشير إلى الصفيف نفسه ، تسمح لكودها باستخدام نطاق القيم الذي تم تحديده أثناء التهيئة فقط.


نرى هنا الخاصية الأولى لنوع البيانات هذا: فهي تنشئ بعض السياق. دعنا نطور فكرتنا بالسياقات:


 void Main() { var array = new [] {'1','2','3','4','5','6'}; var span = new Span<char>(array, 1, 3); if(TryParseInt32(span, out var res)) { Console.WriteLine(res); } else { Console.WriteLine("Failed to parse"); } } public bool TryParseInt32(Span<char> input, out int result) { result = 0; for (int i = 0; i < input.Length; i++) { if(input[i] < '0' || input[i] > '9') return false; result = result * 10 + ((int)input[i] - '0'); } return true; } ----- 234 

كما نرى ، يقدم Span<T> تجريدًا للوصول إلى جزء معين من الذاكرة ، للقراءة والكتابة. ماذا يعطينا هذا؟ إذا تذكرنا ما يمكن القيام به على أساس Span ، فإننا نتذكر كلاً من الموارد والخطوط غير المُدارة:


 // Managed array var array = new[] { '1', '2', '3', '4', '5', '6' }; var arrSpan = new Span<char>(array, 1, 3); if (TryParseInt32(arrSpan, out var res1)) { Console.WriteLine(res1); } // String var srcString = "123456"; var strSpan = srcString.AsSpan().Slice(1, 3); if (TryParseInt32(strSpan, out var res2)) { Console.WriteLine(res2); } // void * Span<char> buf = stackalloc char[6]; buf[0] = '1'; buf[1] = '2'; buf[2] = '3'; buf[3] = '4'; buf[4] = '5'; buf[5] = '6'; if (TryParseInt32(buf.Slice(1, 3), out var res3)) { Console.WriteLine(res3); } ----- 234 234 234 

أي ، اتضح أن Span<T> هو وسيلة للتوحيد في العمل مع الذاكرة: مُدارة وغير مُدارة ، مما يضمن الأمان في العمل مع هذا النوع من البيانات أثناء Garbage Collection: إذا بدأت مناطق الذاكرة ذات المصفوفات المُدارة بالتحرك ، ستكون آمنة لنا.


ومع ذلك ، هل يستحق أن نفرح كثيرا؟ هل يمكن تحقيق كل هذا من قبل؟ على سبيل المثال ، إذا تحدثنا عن المصفوفات المدارة ، فلا يوجد أي شك على الإطلاق: ما عليك سوى لف المصفوفة في فصل آخر ، وتوفير واجهة مماثلة ، وبذلك تكون قد انتهيت. علاوة على ذلك ، يمكن إجراء عملية مماثلة باستخدام الأوتار: لديهم الطرق اللازمة. مرة أخرى ، ما عليك سوى لف السلسلة بنفس النوع تمامًا وتوفير طرق للعمل معها. شيء آخر هو أنه من أجل تخزين سلسلة أو مخزن مؤقت أو صفيف في نوع واحد ، سيتعين عليك العبث كثيرًا عن طريق تخزين الارتباطات لكل خيار من الخيارات الممكنة في نسخة واحدة (بالطبع ، سيكون واحد فقط نشطًا):


 public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... } 

أو ، إذا بدأت من الهندسة ، فقم بثلاثة أنواع ترث واجهة واحدة. اتضح أنه من أجل جعل الأداة واجهة موحدة بين أنواع البيانات managed ، مع الحفاظ على الأداء الأقصى ، لا توجد طريقة أخرى غير Span<T> .


علاوة على ذلك ، لمواصلة المناقشة ، ما هو هيكل ref struct من حيث Span ؟ هذه هي بالضبط "الهياكل ، إنها فقط على المكدس" ، والتي كثيرا ما نسمع عنها في المقابلات. وهذا يعني أن هذا النوع من البيانات يمكن أن يمر فقط من خلال المكدس وليس لديه الحق في الوصول إلى كومة الذاكرة المؤقتة. وبالتالي ، فإن Span ، كونه هيكلًا مرجعيًا ، هو نوع بيانات سياق يوفر طرقًا ، وليس كائنات في الذاكرة. من هذا ، لفهمه ، يجب علينا المضي قدما.


من هنا يمكننا صياغة تعريف لنوع Span والنوع للقراءة فقط ReadOnlySpan المرتبط به:


Span هو نوع بيانات يوفر واجهة واحدة للعمل مع أنواع غير متجانسة من صفائف البيانات بالإضافة إلى القدرة على نقل مجموعة فرعية من هذا الصفيف إلى طريقة أخرى بحيث ، بغض النظر عن عمق السياق ، تكون سرعة الوصول إلى الصفيف الأصلي ثابتة وعالية قدر الإمكان.

وحقاً: إذا كان لدينا شيء مثل هذا الرمز:


 public void Method1(Span<byte> buffer) { buffer[0] = 0; Method2(buffer.Slice(1,2)); } Method2(Span<byte> buffer) { buffer[0] = 0; Method3(buffer.Slice(1,1)); } Method3(Span<byte> buffer) { buffer[0] = 0; } 

ثم ستكون سرعة الوصول إلى المخزن المؤقت المصدر عالية قدر الإمكان: أنت لا تعمل مع كائن مُدار ، ولكن مع مؤشر مُدار. على سبيل المثال ليس مع نوع .NET مُدار ، ولكن بنوع غير آمن ملفوف في غلاف مُدار.


سبان [T] بالأمثلة


يتم ترتيب الشخص لدرجة أنه غالبًا ما يحصل حتى على تجربة معينة ، ثم لا يأتي الفهم النهائي لسبب الحاجة إلى أداة. وبالتالي ، بما أننا بحاجة إلى بعض الخبرة ، فلننتقل إلى الأمثلة.


ValueStringBuilder


أحد الأمثلة الأكثر إثارة للخوارزميات هو نوع ValueStringBuilder ، والذي تم دفنه في مكان ما في أحشاء mscorlib ولسبب ما ، مثل العديد من أنواع البيانات الأخرى المثيرة للاهتمام ، تم وضع علامة على المعدل internal ، مما يعني أنه إذا لم يكن لدراسة كود مصدر mscorlib ، فسوف نتحدث عن طريقة التحسين الرائعة هذه. لن تعرف.


ما هو الطرح الرئيسي لنوع نظام StringBuilder؟ هذا ، بالطبع ، هو جوهره: هو نفسه وما يقوم عليه (وهذه مجموعة من الأحرف char[] ) هي أنواع مرجعية. وهذا يعني شيئين على الأقل: ما زلنا (وإن كنا قليلاً) نحمّل مجموعة والثانية - نزيد من فرصة تفويت ذاكرة التخزين المؤقت للمعالج.


سؤال آخر كان لدي ل StringBuilder هو تكوين سلاسل صغيرة. على سبيل المثال عندما يكون سطر النتيجة "إعطاء سن" قصيرًا: على سبيل المثال ، أقل من 100 حرف. عندما يكون لدينا تنسيق قصير إلى حد ما ، تنشأ مشاكل في الأداء:


  $"{x} is in range [{min};{max}]" 

ما مدى سوء هذا السجل من الجيل اليدوي عبر StringBuilder؟ الجواب بعيد عن الوضوح دائمًا: كل هذا يتوقف على مكان التكوين: كم مرة سيتم استدعاء هذه الطريقة. بعد كل شيء ، string.Format الأولى ، يخصص string.Format ذاكرة StringBuilder الداخلية ، والتي ستنشئ مجموعة من الأحرف (SourceString.Length + args.Length * 8) وإذا تبين أثناء تشكيل الصفيف أن الطول لم يتم تخمينه ، فسيتم إنشاء StringBuilder آخر لتشكيل الاستمرار ، وبالتالي تشكيل قائمة متصلة ببساطة. ونتيجة لذلك ، سيكون من الضروري إرجاع السطر الذي تم إنشاؤه: وهذه نسخة أخرى. التبذير والتبذير. الآن ، إذا تمكنا من التخلص من وضع الصفيف الأول من السلسلة التي يتم تشكيلها على كومة الذاكرة المؤقتة ، فسيكون رائعًا: سوف نتخلص بالتأكيد من مشكلة واحدة.


نلقي نظرة على النوع من أحشاء mscorlib :


فئة ClassStringBuilder
/ src / mscorlib / shared / System / Text / ValueStringBuilder


  internal ref struct ValueStringBuilder { //           private char[] _arrayToReturnToPool; //     private Span<char> _chars; private int _pos; //    ,       public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public int Length { get => _pos; set { int delta = value - _pos; if (delta > 0) { Append('\0', delta); } else { _pos = value; } } } //   -       public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } //       //     :   public void Insert(int index, char value, int count) { if (_pos > _chars.Length - count) { Grow(count); } int remaining = _pos - index; _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); _chars.Slice(index, count).Fill(value); _pos += count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _pos; if (pos < _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } //   ,     //         //            //           [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int requiredAdditionalCapacity) { Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos); char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); _chars.CopyTo(poolArray); char[] toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Clear() { char[] toReturn = _arrayToReturnToPool; this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again if (toReturn != null) { ArrayPool<char>.Shared.Return(toReturn); } } //  :       private void AppendSlow(string s); public bool TryCopyTo(Span<char> destination, out int charsWritten); public void Append(string s); public void Append(char c, int count); public unsafe void Append(char* value, int length); public Span<char> AppendSpan(int length); } 

تشبه هذه الفئة في الوظائف شقيقها الأكبر StringBuilder ، ولكن لديها ميزة واحدة مثيرة للاهتمام ومهمة للغاية: إنها نوع مهم. على سبيل المثال يتم تخزينها وإرسالها بالكامل حسب القيمة. ويخبرنا أحدث مُعدل نوع ref ، الذي تم تعيينه لتوقيع إعلان النوع ، أن هذا النوع له قيود إضافية: له الحق في التواجد فقط في المكدس. على سبيل المثال سيؤدي إخراج مثيلاته إلى حقول الفئة إلى حدوث خطأ. لماذا كل هؤلاء القرفصاء؟ للإجابة على هذا السؤال ، ما StringBuilder سوى إلقاء نظرة على فئة StringBuilder ، التي وصفنا جوهرها للتو:


فئة StringBuilder /src/mscorlib/src/System/Text/StringBuilder.cs


 public sealed class StringBuilder : ISerializable { // A StringBuilder is internally represented as a linked list of blocks each of which holds // a chunk of the string. It turns out string as a whole can also be represented as just a chunk, // so that is what we do. internal char[] m_ChunkChars; // The characters in this block internal StringBuilder m_ChunkPrevious; // Link to the block logically before this block internal int m_ChunkLength; // The index in m_ChunkChars that represent the end of the block internal int m_ChunkOffset; // The logical offset (sum of all characters in previous blocks) internal int m_MaxCapacity = 0; // ... internal const int DefaultCapacity = 16; 

StringBuilder هي فئة يوجد داخلها ارتباط لمجموعة من الشخصيات. على سبيل المثال عندما تقوم بإنشائه ، في الواقع ، يتم إنشاء كائنين على الأقل: StringBuilder نفسه ومجموعة من الأحرف لا يقل عن 16 حرفًا (بالمناسبة ، هذا هو السبب في أنه من المهم جدًا تحديد الطول المقدر للسلسلة: سيتم بناءه من خلال إنشاء قائمة متصلة من صفائف مكونة من 16 حرفًا. ) ماذا يعني هذا في سياق حديثنا حول نوع ValueStringBuilder: السعة غير موجودة افتراضيًا ، لأن يأخذ ذاكرة من الخارج ، بالإضافة إلى أنه نوع مهم ويجبر المستخدم على تخصيص مخزن مؤقت للأحرف على المكدس. ونتيجة لذلك ، يتم دفع مثيل النوع بالكامل إلى المكدس مع محتوياته ، ويتم حل مشكلة التحسين هنا. لا تخصيص الذاكرة على كومة الذاكرة المؤقتة؟ لا توجد مشكلة في ترهل الأداء في كومة الذاكرة المؤقتة. ولكنك تخبرني: لماذا لا تستخدم ValueStringBuilder (أو نسخته المكتوبة ذاتيًا: هل هي داخلية وغير متاحة لنا) دائمًا؟ الجواب: تحتاج إلى النظر في المشكلة التي تقوم بحلها. هل ستكون السلسلة الناتجة ذات حجم معروف؟ هل سيكون لها حد أقصى معروف معروف؟ إذا كانت الإجابة بنعم وإذا كان حجم السلسلة لا يتجاوز بعض الحدود المعقولة ، فيمكنك استخدام إصدار هادف من StringBuilder. خلاف ذلك ، إذا كنا نتوقع طوابير طويلة ، فإننا ننتقل إلى استخدام الإصدار العادي.


ValueListBuilder


نوع البيانات الثاني الذي أريد ملاحظته بشكل خاص هو نوع ValueListBuilder . تم إنشاؤه للحالات عندما يكون من الضروري إنشاء مجموعة من العناصر لفترة قصيرة وإعطائها على الفور لبعض الخوارزميات للمعالجة.


موافق: المهمة مشابهة جدًا لمهمة ValueStringBuilder . نعم ، وقد تم حلها بطريقة مشابهة جدًا:


ملف ValueListBuilder.cs


بصراحة ، مثل هذه الحالات شائعة جدًا. ومع ذلك ، قبل أن نحل هذا السؤال بطريقة أخرى: أنشأنا List ، وملأناها بالبيانات وفقدنا الرابط. إذا تم استدعاء الطريقة في كثير من الأحيان بما فيه الكفاية ، ينشأ موقف حزين: العديد من مثيلات فئة List معلقة في كومة الذاكرة المؤقتة ، ومعها المصفوفات المرتبطة بها معلقة في كومة الذاكرة المؤقتة. الآن تم حل هذه المشكلة: لن يتم إنشاء كائنات إضافية. ومع ذلك ، كما في حالة ValueStringBuilder ، تم حلها فقط لمبرمجي Microsoft: تحتوي الفئة على معدل internal .


شروط وأحكام الاستخدام


لفهم جوهر نوع البيانات الجديد أخيرًا ، تحتاج إلى "التعامل معه" من خلال كتابة أمرين ، أو أفضل ، المزيد من الأساليب باستخدامه. ومع ذلك ، يمكن تعلم القواعد الأساسية الآن:


  • إذا كانت طريقتك ستعالج بعض مجموعة البيانات الواردة دون تغيير حجمها ، يمكنك محاولة التوقف عند نوع Span . إذا لم يكن هناك تعديل لهذا المخزن المؤقت ، ثم على نوع ReadOnlySpan ؛
  • إذا كانت طريقتك ستعمل مع السلاسل أو حساب بعض الإحصائيات أو تحليل سلسلة ، فيجب أن تقبل طريقتك ReadOnlySpan<char> . إنه ملزم: هذه قاعدة جديدة. بعد كل شيء ، إذا قبلت سلسلة ، فإنك بذلك تجبر شخصًا على عمل سلسلة فرعية لك
  • إذا كنت بحاجة إلى إنشاء مصفوفة قصيرة إلى حد ما مع البيانات (على سبيل المثال ، 10 كيلو بايت كحد أقصى) كجزء من عمل الطريقة ، فيمكنك بسهولة تنظيم مثل هذا الصفيف باستخدام Span<TType> buf = stackalloc TType[size] . ومع ذلك ، بالطبع ، يجب أن يكون TType نوعًا ذا معنى فقط ، مثل stackalloc يعمل فقط مع أنواع ذات مغزى.

في حالات أخرى ، يجدر إلقاء نظرة فاحصة على Memory أو استخدام أنواع البيانات الكلاسيكية.


كيف يعمل سبان


بالإضافة إلى ذلك ، أود أن أتحدث عن كيفية عمل Span وما هو الشيء الرائع في ذلك. وهناك شيء يمكن الحديث عنه: ينقسم نوع البيانات نفسه إلى نسختين: لـ .NET Core 2.0+ وللجميع.


Span.Fast.cs ، ملف .NET Core 2.0


 public readonly ref partial struct Span<T> { ///    .NET    internal readonly ByReference<T> _pointer; ///      private readonly int _length; // ... } 

ملف ؟؟؟ [فك]


 public ref readonly struct Span<T> { private readonly System.Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; // ... } 

الشيء هو أن .NET Framework الكبير و .NET Core 1. * ليس لديهم جامع بيانات معدلة معدلاً خصيصًا (بخلاف إصدار .NET Core 2.0+) وبالتالي يضطرون للسحب على طول مؤشر إضافي: إلى بداية المخزن المؤقت الذي العمل. أي ، اتضح أن Span يعمل داخليًا مع الكائنات المُدارة في منصة .NET على أنها غير مُدارة. نلقي نظرة على الدواخل من الإصدار الثاني من الهيكل: هناك ثلاثة حقول. الحقل الأول هو مرجع إلى الكائن المُدار. والثاني هو الإزاحة من بداية هذا الكائن بالبايت للحصول على بداية المخزن المؤقت للبيانات (في الأسطر هو مخزن مؤقت بأحرف char ، في الصفائف هو مخزن مؤقت يحتوي على بيانات الصفيف). وأخيرًا ، الحقل الثالث هو عدد عناصر هذا المخزن المؤقت المكدس واحدًا تلو الآخر.


على سبيل المثال ، خذ مهمة Span للسلاسل النصية:


ملف coreclr :: src / System.Private.CoreLib / Shared / System / MemoryExtensions.Fast.cs


 public static ReadOnlySpan<char> AsSpan(this string text) { if (text == null) return default; return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length); } 

حيث أن string.GetRawStringData() كما يلي:


ملف تعريف الحقل coreclr :: src / System.Private.CoreLib / src / System / String.CoreCLR.cs


ملف تعريف GetRawStringData coreclr :: src / System.Private.CoreLib / shared / System / String.cs


 public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string>, IEquatable<string>, ICloneable { // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // [NonSerialized] private int _stringLength; // For empty strings, this will be '\0' since // strings are both null-terminated and length prefixed [NonSerialized] private char _firstChar; internal ref char GetRawStringData() => ref _firstChar; } 

على سبيل المثال اتضح أن الطريقة تذهب مباشرة داخل الخط ، وتسمح لك مواصفات ref char بتتبع ارتباط GC غير المُدار داخل الخط ، وتحريكه جنبًا إلى جنب مع الخط أثناء تشغيل GC.


تحدث نفس القصة مع المصفوفات: عند إنشاء Span ، تقوم بعض التعليمات البرمجية داخل JIT بحساب إزاحة بداية بيانات المصفوفة وتهيئة Span هذا الإزاحة. , .


Span[T]


, Span , , . :


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = new byte[100]; return reff; } 

. , :


 unsafe void Main() { var x = GetSpan(); } public Span<byte> GetSpan() { Span<byte> reff = stackalloc byte[100]; return reff; } 

. , , , .


, , , , . , . , , , x[0.99] .


, , , , : CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope : , , , .



Span<T> , . , use cases .




Source: https://habr.com/ru/post/ar418911/


All Articles