
تمتد <T> أمثلة الاستخدام
لا يمكن للإنسان بطبيعته أن يفهم تمامًا الغرض من أداة معينة حتى يحصل على بعض الخبرة. لذلك ، دعنا ننتقل إلى بعض الأمثلة.
ValueStringBuilder
أحد أكثر الأمثلة إثارة للاهتمام فيما يتعلق بالخوارزميات هو نوع ValueStringBuilder
. ومع ذلك ، يتم دفنه بعمق داخل mscorlib ويشار إليه بالمعدل internal
مثله مثل العديد من أنواع البيانات الأخرى المهمة جدًا. هذا يعني أننا لن نجد هذه الأداة الرائعة للتحسين إذا لم نقم بالبحث في شفرة مصدر mscorlib.
ما هو العيب الرئيسي لنوع نظام StringBuilder
؟ العيب الرئيسي هو النوع وأساسه - إنه نوع مرجعي ويستند إلى char[]
، أي مجموعة أحرف. على الأقل ، هذا يعني شيئين: نحن نستخدم الكومة (وإن لم يكن الكثير) على أي حال وزيادة فرص تفويت أموال وحدة المعالجة المركزية.
مشكلة أخرى مع StringBuilder
التي واجهتها هي بناء سلاسل صغيرة ، وهي عندما يجب أن تكون السلسلة الناتجة قصيرة على سبيل المثال أقل من 100 حرف. التنسيق القصير يثير مشاكل في الأداء.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .
$"{x} is in range [{min};{max}]"
إلى أي مدى هذا البديل أسوأ من البناء اليدوي من خلال StringBuilder
؟ الجواب ليس واضحًا دائمًا. ذلك يعتمد على مكان البناء وتكرار استدعاء هذه الطريقة. في البداية ، يخصص string.Format
الذاكرة لـ StringBuilder
الداخلية التي ستنشئ مجموعة من الأحرف (SourceString.Length + args.Length * 8). إذا تبين أثناء إنشاء الصفيف أنه تم تحديد الطول بطريقة غير صحيحة ، فسيتم إنشاء StringBuilder
آخر لإنشاء الباقي. سيؤدي ذلك إلى إنشاء قائمة واحدة مرتبطة. نتيجة لذلك ، يجب أن ترجع السلسلة التي تم إنشاؤها والتي تعني نسخًا آخر. هذا هو مضيعة. سيكون أمرا رائعا إذا استطعنا التخلص من تخصيص مجموعة من السلسلة المشكلة على الكومة: هذا من شأنه أن يحل إحدى مشاكلنا.
لنلقِ نظرة على هذا النوع من عمق mscorlib
:
فئة ValueStringBuilder
/ src / mscorlib / Shared / System / Text / ValueStringBuilder
internal ref struct ValueStringBuilder { // this field will be active if we have too many characters private char[] _arrayToReturnToPool; // this field will be the main private Span<char> _chars; private int _pos; // the type accepts the buffer from the outside, delegating the choice of its size to a calling party 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; } } } // Here we get the string by copying characters from the array into another array public override string ToString() { var s = new string(_chars.Slice(0, _pos)); Clear(); return s; } // To insert a required character into the middle of the string //you should add space into the characters of that string and then copy that character 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); } // If the original array passed by the constructor wasn't enough // we allocate an array of a necessary size from the pool of free arrays // It would be ideal if the algorithm considered // discreteness of array size to avoid pool fragmentation. [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); } } // Missing methods: the situation is crystal clear 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 / 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
، فهذا يعني عدم وجود capacity
افتراضية ، لأنه يستعير ذاكرة خارجية. أيضًا ، إنه نوع قيمة ، ويجعل المستخدم يقوم بتخصيص مخزن مؤقت للأحرف على المكدس. وبالتالي ، يتم وضع المثيل الكامل للنوع على المكدس مع محتوياته ويتم حل مشكلة التحسين. نظرًا لعدم وجود حاجة لتخصيص ذاكرة على الكومة ، لا توجد مشكلات في انخفاض الأداء عند التعامل مع الكومة. لذلك ، قد يكون لديك سؤال: لماذا لا نستخدم دائمًا ValueStringBuilder
(أو التناظرية المخصصة الخاصة به لأنه لا يمكننا استخدام الأصل لأنه داخلي)؟ الجواب هو: يعتمد على المهمة. هل سيكون للخيط الناتج حجم محدد؟ هل سيكون لها الحد الأقصى للطول المعروف؟ إذا أجبت بـ "نعم" وإذا كانت السلسلة لا تتجاوز الحدود المعقولة ، فيمكنك استخدام إصدار قيمة StringBuilder
. ومع ذلك ، إذا كنت تتوقع سلاسل طويلة ، استخدم الإصدار المعتاد.
ValueListBuilder
internal ref partial struct ValueListBuilder<T> { private Span<T> _span; private T[] _arrayFromPool; private int _pos; public ValueListBuilder(Span<T> initialSpan) { _span = initialSpan; _arrayFromPool = null; _pos = 0; } public int Length { get; set; } public ref T this[int index] { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(T item); public ReadOnlySpan<T> AsSpan(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose(); private void Grow(); }
النوع الثاني من البيانات الذي أريد ملاحظته بشكل خاص هو نوع ValueListBuilder
. يتم استخدامه عندما تحتاج إلى إنشاء مجموعة من العناصر لفترة قصيرة وتمريرها إلى خوارزمية للمعالجة.
أعترف ، أن هذه المهمة تشبه إلى حد كبير مهمة ValueStringBuilder
. ويتم حلها بطريقة مماثلة:
File ValueListBuilder.cs coreclr / src /../ Generic / 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+ والآخر للباقي.
File Span.Fast.cs ، .NET Core 2.0 coreclr /.../ System / Span.Fast.cs **
public readonly ref partial struct Span<T> { /// A reference to a .NET object or a pure pointer internal readonly ByReference<T> _pointer; /// The length of the buffer based on the pointer private readonly int _length; // ... }
ملف ؟؟؟ [decompiled]
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
مع السلاسل ، على سبيل المثال:
ملف MemoryExtensions.Fast.cs
coreclr /../ 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 /../ System / String.CoreCLR.cs
ملف مع تعريف GetRawStringData coreclr /../ 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
باستخدام هذا الإزاحة. تمت مناقشة الطريقة التي يمكنك بها حساب إزاحة السلاسل والمصفوفات في الفصل حول بنية الكائنات في الذاكرة (. \ ObjectsStructure.md).
تمتد <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; }
سوف مترجم يحظر ذلك. قبل أن أقول لماذا ، أود منك أن تخمن المشاكل التي يجلبها هذا البناء.
حسنًا ، أرجو أن تفكر ، خمنت وربما تفهمت السبب. إذا كانت الإجابة بنعم ، فإن جهودي في كتابة فصل مفصل حول [مكدس مؤشر الترابط] (./ThreadStack.md) تؤتي ثمارها. لأنه عندما تقوم بإرجاع إشارة إلى المتغيرات المحلية من طريقة تنهي عملها ، يمكنك استدعاء طريقة أخرى ، والانتظار حتى تنتهي من عملها أيضًا ، ثم قراءة قيم تلك المتغيرات المحلية باستخدام x [0.99].
لحسن الحظ ، عندما نحاول كتابة مثل هذه الشفرة ، يقوم مترجم CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
عن طريق التحذير: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope
. المحول البرمجي صحيح لأنك إذا تجاوزت هذا الخطأ ، فستكون هناك فرصة ، في مكون إضافي ، لسرقة كلمات مرور الآخرين أو رفع الامتيازات لتشغيل المكون الإضافي الخاص بنا.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .