بدءًا من .NET Core 2.0 و .NET Framework 4.5 ، يمكننا استخدام أنواع بيانات جديدة: Span
and Memory
. لاستخدامها ، تحتاج فقط إلى تثبيت حزمة nuget System.Memory
:
PM> Install-Package System.Memory
إن أنواع البيانات هذه ملحوظة لأن فريق CLR قام بعمل رائع لتنفيذ دعمهم الخاص داخل كود برنامج التحويل البرمجي .NET Core 2.1+ JIT من خلال دمج أنواع البيانات هذه في القلب. ما نوع أنواع البيانات هذه ولماذا يستحق الفصل بأكمله؟
إذا تحدثنا عن المشاكل التي جعلت هذه الأنواع تظهر ، أود أن أسمي ثلاثة منها. أول واحد هو رمز غير المدارة.
توجد كل من اللغة والنظام الأساسي لسنوات عديدة إلى جانب وسائل العمل باستخدام التعليمات البرمجية غير المُدارة. لذلك ، لماذا حرر واجهة برمجة تطبيقات أخرى للعمل مع تعليمات برمجية غير مُدارة إذا كان السابق موجودًا بشكل أساسي لسنوات عديدة؟ للإجابة على هذا السؤال ، يجب أن نفهم ما افتقدناه من قبل.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .
لقد حاول مطورو النظام الأساسي بالفعل تسهيل استخدام الموارد غير المُدارة بالنسبة لنا. قاموا بتطبيق أغلفة تلقائية للطرق المستوردة والتنظيم التي تعمل تلقائيًا في معظم الحالات. هنا ينتمي أيضًا إلى stackalloc
، والمشار إليها في الفصل حول مكدس الخيط. ومع ذلك ، كما أراها ، جاء أول مطوري C # من عالم C ++ (حالتي) ، لكنهم الآن ينتقلون من لغات عالية المستوى (أعرف مطورًا كتب في JavaScript من قبل). هذا يعني أن الناس أصبحوا أكثر تشككا في التعليمات البرمجية غير المدارة وبنيات C / C + ، وأكثر من ذلك بكثير إلى المجمع.
نتيجة لذلك ، تحتوي المشاريع على رمز أقل أمانًا وأقل وتزداد الثقة في واجهة برمجة التطبيقات للنظام الأساسي أكثر فأكثر. من السهل التحقق مما إذا كنا نبحث عن حالات استخدام 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
، بل أي نوع يشير إلى أن شيئًا ما قد يحدث خطأ ، ولكن هناك الكلمة الأساسية unsafe
byte* buffer = stackalloc byte[s_readBufferSize];
الخط (بالتحديد - byte*
) الذي يغير موقفنا. هذا محفز بالنسبة لك للتفكير: "ألم تكن هناك طريقة أخرى للقيام بذلك"؟ لذلك ، دعونا نتعمق أكثر في التحليل النفسي: لماذا قد تفكر بهذه الطريقة؟ من ناحية ، نستخدم تصميمات اللغة والبناء المعروض هنا بعيد عن ، على سبيل المثال ، C ++ / CLI ، والذي يسمح بأي شيء (حتى إدخال رمز Assembler الخالص). من ناحية أخرى ، فإن بناء الجملة هذا يبدو غير عادي.
العدد الثاني من مطوري البرامج الذين فكروا ضمنيًا أو صريحًا هو عدم توافق أنواع السلسلة و char []. على الرغم من أن السلسلة منطقية عبارة عن مجموعة من الأحرف ، لكن لا يمكنك إلقاء سلسلة على الأحرف []: يمكنك فقط إنشاء كائن جديد ونسخ محتوى السلسلة إلى صفيف. يتم تقديم عدم التوافق هذا لتحسين السلاسل من حيث التخزين (لا توجد صفائف للقراءة فقط). ومع ذلك ، تظهر مشاكل عند بدء العمل مع الملفات. كيف تقرأها؟ كسلسلة أو كصفيف؟ إذا اخترت صفيفًا ، فلا يمكنك استخدام بعض الأساليب المصممة للعمل مع السلاسل. ماذا عن القراءة كسلسلة؟ قد يكون طويلاً إذا كنت بحاجة إلى تحليلها ، فما هو المحلل اللغوي الذي يجب عليك اختياره لأنواع البيانات البدائية: لا ترغب دائمًا في تحليلها يدويًا (أعداد صحيحة ، تعويمات ، مقدمة بتنسيقات مختلفة). لدينا الكثير من الخوارزميات المؤكدة التي تفعل ذلك بشكل أسرع وأكثر كفاءة ، أليس كذلك؟ ومع ذلك ، غالبًا ما تعمل هذه الخوارزميات مع سلاسل لا تحتوي إلا على نوع بدائي بحد ذاته. لذلك ، هناك معضلة.
المشكلة الثالثة هي أن البيانات المطلوبة بواسطة خوارزمية نادراً ما تصنع شريحة بيانات متواصلة صلبة داخل قسم من صفيف يقرأ من مصدر ما. على سبيل المثال ، في حالة قراءة الملفات أو البيانات من مأخذ توصيل ، لدينا جزء من تلك التي تمت معالجتها بالفعل بواسطة خوارزمية ، يليها جزء من البيانات التي يجب معالجتها بواسطة طريقتنا ، ثم عن طريق البيانات التي لم تتم معالجتها بعد. من الناحية المثالية ، تريد طريقتنا فقط البيانات التي تم تصميم هذه الطريقة من أجلها. على سبيل المثال ، لن ترضى الطريقة التي تقوم بتوزيع الأعداد الصحيحة بسلسلة تحتوي على بعض الكلمات ذات العدد المتوقع في مكان ما بينها. هذه الطريقة تريد رقم ولا شيء غير ذلك. أو إذا مررنا صفيفًا كاملاً ، فهناك شرط للإشارة ، على سبيل المثال ، إلى إزاحة رقم من بداية المصفوفة.
int ParseInt(char[] input, int index) { while(char.IsDigit(input[index])) { // ... index++; } }
ومع ذلك ، فإن هذا النهج ضعيف ، حيث أن هذه الطريقة تحصل على بيانات غير ضرورية. بمعنى آخر ، يتم استدعاء الطريقة للسياقات التي لم يتم تصميمها ، ويجب أن تحل بعض المهام الخارجية. هذا تصميم سيء. كيفية تجنب هذه المشاكل؟ كخيار ، يمكننا استخدام نوع ArraySegment<T>
الذي يمكنه منح حق الوصول إلى قسم من مجموعة:
int ParseInt(IList<char>[] input) { while(char.IsDigit(input.Array[index])) { // ... index++; } } var arraySegment = new ArraySegment(array, from, length); var res = ParseInt((IList<char>)arraySegment);
ومع ذلك ، أعتقد أن هذا كثير للغاية سواء من حيث المنطق أو انخفاض في الأداء. تم تصميم ArraySegment
بشكل سيئ ويؤدي إلى إبطاء الوصول إلى العناصر 7 مرات أكثر مقارنة بنفس العمليات التي تمت باستخدام صفيف.
إذا كيف يمكننا حل هذه المشاكل؟ كيف نعيد المطورين إلى استخدام التعليمات البرمجية غير المُدارة ومنحهم أداة موحدة وسريعة للعمل مع مصادر البيانات غير المتجانسة: المصفوفات والسلاسل والذاكرة غير المُدارة. كان من الضروري منحهم شعور بالثقة أنهم لا يستطيعون ارتكاب خطأ دون علم. كان من الضروري منحهم أداة لا تقلل من أنواع البيانات الأصلية من حيث الأداء ولكنها تحل المشاكل المدرجة. Span<T>
Memory<T>
تمامًا في هذه الأدوات.
Span <T> ، ReadOnlySpan <T>
Span
type هي أداة للعمل مع البيانات داخل قسم من صفيف البيانات أو مع مجموعة فرعية من قيمها. كما في حالة الصفيف ، فإنه يتيح القراءة والكتابة لعناصر هذا النطاق الفرعي ، ولكن مع وجود قيد مهم واحد: يمكنك الحصول على أو إنشاء Span<T>
فقط للعمل المؤقت مع صفيف ، فقط للاتصال بمجموعة من الطرق . ومع ذلك ، للحصول على فهم عام ، دعونا نقارن أنواع البيانات التي تم تصميم 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(); 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, out var res3)) { Console.WriteLine(res3); } ----- 234 234 234
وهذا يعني أن Span<T>
هي أداة لتوحيد طرق العمل مع الذاكرة ، سواء المدارة أو غير المُدارة. إنه يضمن السلامة أثناء العمل مع هذه البيانات أثناء جمع القمامة. هذا إذا بدأت نطاقات الذاكرة مع بدء تشغيل الموارد غير المدارة ، فستكون آمنة.
ومع ذلك ، يجب أن نكون متحمسين جدا؟ هل يمكن أن نحقق هذا في وقت مبكر؟ على سبيل المثال ، في حالة الصفائف المدارة ، لا شك في ذلك: تحتاج فقط إلى التفاف صفيف في فصل دراسي واحد آخر (على سبيل المثال ، [ArraySegment] الموجود منذ فترة طويلة ( https://referencesource.microsoft.com/#mscorlib/system/ arraysegment.cs ، 31 )) وبالتالي إعطاء واجهة مماثلة وهذا هو عليه. علاوة على ذلك ، يمكنك أن تفعل الشيء نفسه مع الأوتار - لديهم الطرق اللازمة. مرة أخرى ، تحتاج فقط إلى التفاف سلسلة من نفس النوع وتوفير طرق للتعامل معها. ومع ذلك ، لتخزين سلسلة ، ومخزن مؤقت ومصفوفة في نوع واحد ، سيكون لديك الكثير لتفعله مع الاحتفاظ بالإشارات إلى كل متغير ممكن في مثيل واحد (مع متغير نشط واحد فقط ، بشكل واضح).
public readonly ref struct OurSpan<T> { private T[] _array; private string _str; private T * _buffer; // ... }
أو بناءً على الهندسة المعمارية ، يمكنك إنشاء ثلاثة أنواع تقوم بتطبيق واجهة موحدة. وبالتالي ، لا يمكن إنشاء واجهة موحدة بين أنواع البيانات هذه المختلفة عن Span<T>
والحفاظ على أقصى أداء.
بعد ذلك ، هناك سؤال حول ما هو ref struct
فيما يتعلق Span
؟ هذه هي بالضبط تلك "الهياكل الموجودة فقط على المكدس" التي نسمع عنها أثناء مقابلات العمل كثيرًا. هذا يعني أنه يمكن تخصيص نوع البيانات هذا على المكدس فقط ولا يمكن الانتقال إلى الكومة. هذا هو السبب في أن Span
، وهي بنية ref ، هي نوع بيانات للسياق يمكّن من عمل الأساليب وليس ككائنات في الذاكرة. هذا هو ما نحتاج إلى الاعتماد عليه عند محاولة فهمه.
الآن يمكننا تحديد نوع 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.
تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.
وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع
github / sidristij / dotnetbook .