
C#
هي لغة مرنة بشكل لا يصدق. على ذلك ، يمكنك كتابة ليس فقط الخلفية أو تطبيقات سطح المكتب. يمكنني استخدام C#
للعمل مع البيانات العلمية ، والتي تفرض متطلبات معينة على الأدوات المتاحة في اللغة. على الرغم من أن netcore
يستحوذ على جدول الأعمال (بالنظر إلى أنه بعد netstandard2.0
، لا يتم netstandard2.0
معظم ميزات كلتا اللغتين ووقت التشغيل في netframework
) ، إلا أنني أواصل العمل مع المشروعات القديمة.
في هذه المقالة ، أنظر إلى تطبيق واحد غير واضح (ولكن ربما يكون مرغوبًا فيه؟) لـ Span<T>
والفرق بين تطبيق Span<T>
في netframework
و netcore
بسبب تفاصيل clr
.
إخلاء المسؤولية 1مقتطفات الكود في هذه المقالة ليست مخصصة للاستخدام في مشاريع العالم الحقيقي.
الحل المقترح لمشكلة (بعيدة المنال؟) هو بالأحرى إثبات للمفهوم.
في أي حال ، من خلال تنفيذ هذا في مشروعك ، يمكنك القيام بذلك على مسؤوليتك الخاصة ومخاطرك.
إخلاء المسؤولية 2أنا متأكد تمامًا أنه في مكان ما ، في بعض الحالات ، سيؤدي هذا بالتأكيد إلى إطلاق النار على شخص ما في الركبة.
من C#
المرجح أن يؤدي تجاوز نوع الأمان في C#
إلى أي شيء جيد.
لأسباب واضحة ، لم أختبر هذا الرمز في جميع الحالات الممكنة ، ومع ذلك ، فإن النتائج الأولية تبدو واعدة.
لماذا أحتاج Span<T>
؟
يتيح لك Spen التعامل مع صفائف من الأنواع unmanaged
بشكل أكثر ملاءمة ، مما يقلل من عدد التخصيصات الضرورية. على الرغم من حقيقة أن الدعم netframework
في netframework
BCL
غائب تمامًا تقريبًا ، يمكن الحصول على العديد من الأدوات باستخدام System.Memory
و System.Buffers
و System.Runtime.CompilerServices.Unsafe
.
استخدام المسافات في مشروعي القديم محدود ، ومع ذلك ، فقد وجدت لهم استخدامًا غير واضح ، بينما بصق على أمان النوع.
ما هو هذا التطبيق؟ في مشروعي أعمل مع البيانات التي تم الحصول عليها من أداة علمية. هذه هي الصور ، والتي ، بشكل عام ، عبارة عن مجموعة من T[]
، حيث T
هو أحد الأنواع البدائية unmanaged
، على سبيل المثال Int32
(المعروف أيضًا باسم int
). لتسلسل هذه الصور على القرص بشكل صحيح ، أحتاج إلى دعم التنسيق القديم غير المريح بشكل لا يصدق ، والذي تم اقتراحه في عام 1981 ، ومنذ ذلك الحين لم يتغير كثيرًا. المشكلة الرئيسية لهذا التنسيق هو BigEndian . وبالتالي ، من أجل كتابة (أو قراءة) مجموعة غير مضغوطة من T[]
، تحتاج إلى تغيير نهاية كل عنصر. المهمة التافهة.
ما هي بعض الحلول الواضحة؟
- نكررها عبر الصفيف
T[]
، ندعو BitConverter.GetBytes(T)
، وسّع هذه البايتات القليلة ، وانسخها إلى الصفيف المستهدف. - نكررها عبر الصفيف
T[]
، وننفّذ عمليات احتيال للنموذج new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)};
(يجب أن تعمل على أنواع مزدوجة البايت) ، اكتب إلى الصفيف الهدف. - * لكن هل
T[]
صفيف؟ العناصر في صف واحد ، أليس كذلك؟ حتى تتمكن من الانتقال ، على سبيل المثال ، Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int));
. الأسلوب بنسخ الصفيف إلى الصفيف تجاهل تدقيق النوع. من الضروري فقط عدم تفويت الحدود والتخصيص. نحن مزيج البايتات نتيجة لذلك. - * يقولون أن
C#
هو (C++)++
. لذلك ، قم بتمكين /unsafe
، fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p;
والآن يمكنك تشغيل تمثيل البايت للصفيف المصدر وتغيير النهاية على الطاير وكتابة الكتل على القرص (إضافة stackalloc byte[]
أو ArrayPool<byte>.Shared
المؤقت الوسيط) دون تخصيص ذاكرة لصفيف بايت جديد بالكامل.
يبدو أن النقطة 4 تسمح لك بحل جميع المشكلات ، لكن الاستخدام الصريح للسياق unsafe
والعمل مع المؤشرات مختلف تمامًا. ثم Span<T>
يأتي لمساعدتنا.
Span<T>
يجب أن يوفر Span<T>
تقنيًا أدوات للعمل مع مؤامرات الذاكرة مثل العمل من خلال مؤشرات تقريبًا ، مع التخلص من الحاجة إلى "إصلاح" المصفوفة في الذاكرة. هذا مؤشر GC
-aware مع حدود مجموعة. كل شيء على ما يرام وآمن.
شيء واحد ولكن - على الرغم من ثروة System.Runtime.CompilerServices.Unsafe
، Span<T>
مسمر لكتابة T
بالنظر إلى أن الدوران هو في الأساس مؤشر طول + 1 ، ماذا لو قمت بسحب المؤشر الخاص بك ، وقمت بتحويله إلى نوع آخر ، وإعادة حساب الطول وجعل فترة جديدة؟ لحسن الحظ ، لدينا public Span<T>(void* pointer, int length)
.
دعنا نكتب اختبار بسيط:
[Test] public void Test() { void Flip(Span<byte> span) {} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); }
المطورين أكثر تقدما مما يجب أن أدرك على الفور ما هو الخطأ هنا. هل سيفشل الاختبار؟ الجواب ، كما يحدث عادة ، يعتمد .
في هذه الحالة ، يعتمد بشكل أساسي على وقت التشغيل. على netcore
يجب أن يعمل الاختبار ، ولكن على netframework
، كيف netframework
.
ومن المثير للاهتمام ، إذا قمت بإزالة بعض المقالات ، يبدأ الاختبار في العمل بشكل صحيح في 100 ٪ من الحالات.
هيا بنا
1 كنت مخطئا .
الجواب الصحيح: يعتمد
لماذا تعتمد النتيجة؟
دعنا نزيل جميع الأشياء غير الضرورية والكتابة هنا مثل هذا الرمز:
private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); }
يقبل أسلوب Write<T, U>
نطاقًا من النوع T
، ويقرأ عنوان العنصر الأول ، ويقرأ من خلال هذا المؤشر عنصرًا واحدًا من النوع U
بمعنى آخر ، Write<int, int>(ref x)
ستقوم بإخراج العنوان في الذاكرة + الرقم 999.
Write
العادية يطبع مجموعة.
الآن حول الطريقة باسم As<,>
:
private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); }
يدعم بناء جملة C#
الآن سجل الحالة fixed
عن طريق استدعاء أسلوب Span<T>.GetPinnableReference()
.
قم بتشغيل هذه الطريقة على netframework4.8
في وضع x64
. نحن ننظر إلى ما يحدث:
LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
في البداية ، يتصرّف كلا Span<byte>
(على الرغم من الأنواع المختلفة) بشكل متماثل ، ويمثل Span<byte>
، في جوهره ، طريقة عرض للصفيف الأصلي. ما تحتاجه.
حسنًا ، دعونا نحاول تحويل بداية الفترة إلى حجم IntPtr
واحد (أو 2 X int
على x64
) والقراءة. نحصل على العنصر الثالث للصفيف والعنوان الصحيح. ثم سنقوم بجمع القمامة ...
GC.Collect(0, GCCollectionMode.Forced, true, true);
العلم الأخير في هذه الطريقة يطلب من GC
ضغط الكومة. بعد استدعاء GC.Collect
ينتقل GC
الصفيف المحلي الأصلي. Span<int>
تعكس هذه التغييرات ، لكن Span<byte>
تستمر في الإشارة إلى العنوان القديم ، حيث من غير الواضح الآن ما. طريقة رائعة لاطلاق النار على نفسك كل ركبتيك دفعة واحدة!
الآن دعنا نلقي نظرة على نتيجة جزء الشفرة ذاته المحدد على netcore3.0.100-preview8
.
CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ]
كل شيء يعمل ، ويعمل بثبات ، بقدر ما أستطيع أن أرى. بعد الضغط ، يغيّر كلاهما مؤشرهما. ! ممتاز لكن كيف نجعلها تعمل في مشروع قديم؟
جيت جوهري
لقد نسيت تمامًا أن دعم netcore
يتم تنفيذه في netcore
خلال intrinsik . بمعنى آخر ، يمكن لـ netcore
إنشاء مؤشرات داخلية حتى لجزء صفيف وتحديث الروابط بشكل صحيح عندما يتحرك GC
. في netframework
، nuget
تنفيذ nuget
امتداد nuget
. في الواقع ، لدينا نوعان مختلفان: واحد يتم إنشاؤه من الصفيف ويتتبع روابطه ، والثاني من المؤشر وليس لديه فكرة عما يشير إليه. بعد تحريك الصفيف الأصلي ، يستمر مؤشر span في الإشارة إلى المكان الذي تم تمرير المؤشر فيه في اتجاه مُنشئه. للمقارنة ، هذا مثال على تنفيذ span في netcore
:
readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer;
وفي netframework
:
readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; }
يحتوي _pinnable
على إشارة إلى المصفوفة ، إذا تم تمريرها إلى المُنشئ ، فإن _byteOffset
يحتوي على تحول (حتى في _byteOffset
يحتوي على _byteOffset
يحتوي على تحول غير صفري يتعلق بالطريقة التي يتم تمثيل المصفوفة في الذاكرة بها ، على الأرجح ). إذا قمت بتمرير مؤشر void*
إلى المُنشئ ، _byteOffset
تحويله ببساطة إلى _byteOffset
مطلق. سيتم تسمير Span بإحكام في منطقة الذاكرة ، وستكون كل أساليب المثيل كثيرة بشروط مثل if(_pinnable is null) {/* */} else {/* _pinnable */}
. ماذا تفعل في مثل هذه الحالة؟
كيفية القيام بذلك لا يستحق كل هذا العناء ، لكنني ما زلت كذلك
هذا القسم مخصص netframework
المختلفة التي تدعمها netframework
، والتي تسمح بـ Span<T> -> Span<U>
، مع الاحتفاظ بجميع الروابط الضرورية.
أنا أحذرك: هذه منطقة برمجة غير طبيعية مع وجود أخطاء جوهرية وسلوك غير محدد في النهاية
الطريقة 1: ساذج
كما هو netframework
في المثال ، لن يؤدي تحويل المؤشرات إلى إعطاء النتيجة المرجوة على netframework
. نحن بحاجة إلى قيمة _pinnable
. حسنًا ، سنكشف عن التفكير من خلال سحب الحقول الخاصة (سيئة للغاية وغير ممكنة دائمًا) ، وسوف نكتبها في نسج جديد ، وسنكون سعداء. هناك مشكلة صغيرة واحدة فقط: spen عبارة عن ref struct
، ولا يمكن أن تكون حجة عامة ، ولا يمكن تعبئتها في object
. ستتطلب طرق الانعكاس القياسية ، بطريقة أو بأخرى ، دفع المدى إلى نوع المرجع. لم أجد طريقة بسيطة (حتى تفكر في التفكير في الحقول الخاصة).
الطريقة 2: نحن بحاجة للذهاب أعمق
كل شيء تم فعله بالفعل أمامي ( [1] ، [2] ، [3] ). Spen عبارة عن بنية ، بغض النظر عن T
تشغل ثلاثة حقول نفس مقدار الذاكرة ( على نفس البنية ). ماذا لو [FieldOffset(0)]
؟ لم يقل قال من القيام به.
[StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; }
ولكن عند بدء تشغيل البرنامج (أو بالأحرى ، عند محاولة استخدام نوع) ، يجتمع TypeLoadException
- لا يمكن أن يكون LayoutKind.Explicit
. حسنًا ، لا يهم ، دعنا نسير على الطريق الصعب:
[StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; }
الآن يمكنك القيام بذلك:
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; }
تعمل الطريقة مع مشكلة واحدة فقط - _length
نسخ حقل _length
كما هو ، لذلك عندما يكون _length
int
-> byte
فإن مساحة البايت أصغر 4 مرات من المصفوفة الحقيقية.
ليست مشكلة:
[StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Raw RawView; }
الآن من خلال RawView
يمكنك الوصول إلى كل حقل RawView
.
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; }
ويعمل كما ينبغي ، إذا تجاهلت استخدام الحيل القذرة. ناقص - لا يمكن إنشاء الإصدار العام للمحول ، يجب أن تكون راضيًا عن أنواع محددة مسبقًا.
الطريقة 3: مجنون
مثل أي مبرمج عادي ، أحب أتمتة الأشياء. الحاجة إلى كتابة المحولات لأي زوج من الأنواع unmanaged
لم ترضاني. ما الحل الذي يمكن تقديمه؟ هذا صحيح ، احصل على CLR
لكتابة رمز لك .
كيف تحقق هذا؟ هناك طرق مختلفة ، هناك مقالات . باختصار ، تبدو العملية كما يلي:
إنشاء مُنشئ بناء -> إنشاء مُنشئ وحدة -> إنشاء نوع -> {الحقول ، الطرق ، إلخ}} -> في الإخراج نحصل على مثيل لـ Type
.
لفهم الشكل الذي يجب أن يبدو عليه النوع (إنه ref struct
ildasm
) ، نستخدم أي أداة من نوع ildasm
. في حالتي ، كان dotPeek .
يبدو إنشاء منشئ كتابة شيئًا مثل هذا:
var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout
الآن الحقول. نظرًا لأنه لا يمكننا نسخ Span<T>
إلى Span<U>
نظرًا للاختلاف في الأطوال ، نحتاج إلى إنشاء نوعين من كل فريق
[StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; }
هنا Raw
يمكننا أن نعلن بأيدينا وإعادة استخدامها. لا تنسى عن IsByRefLikeAttribute
. مع الحقول ، كل شيء بسيط:
var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0);
هذا كل شيء ، أبسط نوع جاهز. الآن مخبأ وحدة التجميع. يتم تخزين الأنواع المخصصة مؤقتًا ، على سبيل المثال ، في القاموس ( T -> Generated_{nameof(T)}
). نقوم بإنشاء برنامج تغليف ، وفقًا TIn
و TOut
يولد نوعين من المساعدين ويقوم بتنفيذ العمليات اللازمة على النطاقات. هناك واحد ولكن. كما هو الحال في الانعكاس ، يكاد يكون من المستحيل استخدامه على مسافات (أو على ref struct
أخرى). أو لم أجد حلاً بسيطًا . كيف تكون؟
مندوبي الانقاذ
طرق الانعكاس عادة ما تبدو مثل هذا:
object Invoke(this MethodInfo mi, object @this, object[] otherArgs)
إنهم لا يحملون معلومات حول الأنواع ، لذلك إذا كانت الملاكمة (= التغليف) مقبولة لك ، فلا توجد مشاكل.
في حالتنا ، يجب أن تحتوي @this
و otherArgs
على ref struct
، والتي لم أستطع الالتفاف عليها.
ومع ذلك ، هناك طريقة أكثر بساطة. دعنا نتخيل أن نوعًا ما يحتوي على أساليب getter و setter (وليس خصائص ، ولكنه أنشأ طرقًا بسيطة يدويًا).
على سبيل المثال:
void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span;
بالإضافة إلى الطريقة ، يمكننا إعلان نوع المفوض (بشكل صريح في الكود):
delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged;
يتعين علينا القيام بذلك لأن الإجراء القياسي يجب أن يكون له توقيع Action<Span<T>>
، ولكن لا يمكن استخدام spenes كوسيطات عامة. SpanSetterDelegate
، ومع ذلك ، هو مفوض صالحة تماما.
إنشاء المفوضين اللازمة. للقيام بذلك ، قم بإجراء عمليات معالجة قياسية:
var mi = type.GetMethod("Method_Name");
الآن ، يمكن استخدام spanSetter(Span<T>.Empty);
مثل ، spanSetter(Span<T>.Empty);
. بالنسبة إلى @this
2 ، هذا هو مثيل @this
الديناميكي ، الذي تم إنشاؤه ، بالطبع ، من خلال Activator.CreateInstance(type)
، لأن الهيكل له مُنشئ افتراضي بدون وسيطات.
لذا ، فإن الحدود الأخيرة - نحتاج إلى إنشاء طرق ديناميكية.
2 قد تلاحظ أن هناك شيئًا ما يحدث هنا - Activator.CreateInstance()
بتعبئة مثيل ref struct
. انظر نهاية القسم التالي.
تلبية Reflection.Emit
أعتقد أنه يمكن إنشاء طرق باستخدام Expression
، كما تتكون أجساد حرفنا / المستوطنين التافهين من بعض التعبيرات حرفيًا. اخترت نهجا مختلفا وأكثر مباشرة.
إذا netframework4.8
نظرة على رمز IL الخاص بجهاز تافه ، يمكنك رؤية شيء مثل ( Debug
، X86
، netframework4.8
)
nop ldarg.0 ldfld /* - */ stloc.0 br.s /* */ ldloc.0 ret
هناك الكثير من الأماكن للتوقف والتصحيح.
في إصدار الإصدار ، يبقى فقط الأكثر أهمية:
ldarg.0 ldfld /* - */ ret
الوسيطة فارغة أسلوب المثيل هي ... this
. وبالتالي ، فإن ما يلي مكتوب باللغة IL :
1) قم بتنزيل this
2) تحميل قيمة الحقل
3) اعادته
هاه فقط؟ يحتوي Reflection.Emit
على حمل زائد خاص يأخذ ، بالإضافة إلى كود المرجع ، أيضًا معلمة واصف الحقل. تمامًا كما تلقينا سابقًا ، على سبيل المثال spanField
.
var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret);
بالنسبة إلى المضبط ، الأمر أكثر تعقيدًا قليلاً ، تحتاج إلى تحميل هذا على المكدس ، وتحميل الوسيطة الأولى من الوظيفة ، ثم استدعاء تعليمات الكتابة في الحقل وإرجاع أي شيء:
ldarg.0 ldarg.1 stfld ret
بعد القيام بهذا الإجراء بالنسبة للحقل Raw
، والإعلان عن المفوضين الضروريين (أو استخدام المفوضين القياسيين) ، نحصل على نوع ديناميكي وأربعة طرق وصول ، يتم من خلالها إنشاء المفوضين العامين الصحيحين.
نكتب فئة TIn
، باستخدام معلمتين TIn
( TIn
، TOut
) ، تستقبل مثيلات Type
التي تشير إلى الأنواع الديناميكية (المخزنة مؤقتًا) المقابلة ، وبعد ذلك تقوم بإنشاء كائن واحد من كل نوع وتوليد أربعة مفوضين عامين ، وهما
void SetSpan(Span<TIn> span)
لكتابة المدى المصدر للهيكلRaw GetRaw()
لقراءة محتويات مسافة كهيكل Raw
void SetRaw(Raw raw)
لكتابة بنية Raw
المعدلة إلى الكائن الثانيSpan<TOut> GetSpan()
لإرجاع مدى النوع المرغوب به مع تعيين الحقول وإعادة حسابها بشكل صحيح.
ومن المثير للاهتمام ، يجب إنشاء مثيلات الكتابة الديناميكية مرة واحدة. عند إنشاء مفوض ، يتم تمرير مرجع إلى هذه الكائنات كمعلمة @this
. هنا انتهاك للقواعد. Activator.CreateInstance
إرجاع object
. يبدو أن هذا يرجع إلى حقيقة أن النوع الديناميكي نفسه لم يتحول إلى ref
فعل يشبه ref
( type.IsByRef
Like == false
) ، ولكن كان من الممكن إنشاء حقول تشبه ref
. على ما يبدو ، يوجد مثل هذا التقييد في اللغة ، ولكن CLR
يهضمه. ربما هنا يتم إطلاق النار على الركبتين في حالة الاستخدام غير القياسي. 3
لذلك ، نحصل على مثيل من نوع عام يحتوي على أربعة مفوضين ومرجعين ضمنيين إلى مثيلات الفئات الديناميكية. يمكن إعادة استخدام المفوضين والهياكل عند إجراء نفس الطبقات في صف واحد. لتحسين الأداء ، نقوم بتخزين ذاكرة التخزين المؤقت مرة أخرى (بالفعل محول نوع) لزوج (TIn, TOut) -> Generator<TIn, TOut>
.
السكتة الدماغية هي الأخيرة: نعطي أنواعًا ، Span<TIn> -> Span<TOut>
public Span<TOut> Cast(Span<TIn> span) {
استنتاج
في بعض الأحيان - من أجل الاهتمام بالرياضة - يمكنك تجاوز بعض القيود المفروضة على اللغة وتنفيذ وظائف غير قياسية. بالطبع ، على مسؤوليتك الخاصة والمخاطر. تجدر الإشارة إلى أن الطريقة الديناميكية تسمح لك بالتخلي عن المؤشرات والسياقات unsafe / fixed
، والتي يمكن أن تكون مكافأة. الجانب السلبي الواضح هو الحاجة إلى التفكير وتوليد النوع.
بالنسبة لأولئك الذين قرأوا حتى النهاية.
نتائج المعيار الساذجوما مدى سرعة كل شيء؟
قارنت سرعة الطوائف في سيناريو غبي لا يعكس الاستخدام الفعلي / المحتمل لهذه الطوائف والشقوق ، ولكن على الأقل يعطي فكرة عن السرعة.
Cast_Explicit
يستخدم التحويل من خلال نوع معلن صراحة ، كما في الطريقة الثانية . يتطلب كل طبقة تخصيص بنائين صغيرين والوصول إلى الحقول ؛Cast_IL
تطبق الطريقة الثالثة ، ولكن في كل مرة يعيد فيها إنشاء مثيل Generator<TIn, TOut>
، مما يؤدي إلى إجراء عمليات بحث مستمرة في القواميس ، بعد أن يولد التمريرة الأولى جميع الأنواع ؛Cast_IL_Cached
يتم تخزين نسخة المحول مباشرة Generator<TIn, TOut>
، وهذا هو السبب في أنه يبدو أسرع في المتوسط ، لأن الطبقة الكاملة تتلخص في دعوات أربعة مندوبين ؛Buffer
, , . .
— int[N]
N/2
.
, , . , . , , . , unmanaged
.
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1
PS
3 , ref
, , . ( ) . ref
structs,
static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; }
, Reflection.Emit
. , ILGenerator.DeclareLocal
.
static Span<int> Generated_Int32.GetSpan(Raw raw);
delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged;
, , ref
— . لأن ,
var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>;
—
Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw);
UPD01: