تحية للجميع. لقد أعددنا ترجمة لمواد أخرى مفيدة عشية بدء الدورة "C # Developer" . استمتع بالقراءة.
نظرًا لأنني قمت مؤخرًا بوضع قائمة بأفضل الممارسات في C # لـ Criteo ، اعتقدت أنه سيكون من الجيد مشاركتها علنًا. الغرض من هذه المقالة هو توفير قائمة غير مكتملة من قوالب التعليمات البرمجية التي يجب تجنبها ، إما لأنها مشكوك فيها ، أو لأنها تعمل بشكل ضعيف. قد تبدو القائمة عشوائية بعض الشيء لأنها مأخوذة قليلاً من السياق ، ولكن تم العثور على جميع عناصرها في مرحلة ما في الكود الخاص بنا وتسببت في مشاكل في الإنتاج. آمل أن يكون هذا بمثابة منع جيد ومنع أخطائك في المستقبل.لاحظ أيضًا أن خدمات Criteo للويب تعتمد على كود عالي الأداء ، وبالتالي الحاجة إلى تجنب التعليمات البرمجية غير الفعالة. في معظم التطبيقات ، لن يكون هناك اختلاف ملموس ملحوظ عن استبدال بعض هذه القوالب.وأخيرًا وليس آخرًا ، ConfigureAwait
تمت بالفعل مناقشة بعض النقاط (على سبيل المثال ) في العديد من المقالات ، لذلك لن أتناولها بالتفصيل. الهدف هو تكوين قائمة مضغوطة من النقاط التي تحتاج إلى الانتباه إليها ، وعدم إعطاء وصف تقني مفصل لكل منها.انتظار رمز غير متزامن بشكل متزامن
لا تتوقع مهام غير منتهية بشكل متزامن. وينطبق هذا، ولكن ليس على سبيل الحصر: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll
.كتعميم: يمكن أن تتسبب أي علاقة متزامنة بين خيوط تجمع في استنفاد التجمع. يتم وصف أسباب هذه الظاهرة في هذه المقالة .ConfigureAwait
إذا كان من الممكن استدعاء الرمز الخاص بك من سياق المزامنة ، فاستخدم ConfigureAwait(false)
لكل من مكالماتك المنتظرة.يرجى ملاحظة أن هذا ConfigureAwait
مفيد فقط عند استخدام كلمة رئيسية await
.على سبيل المثال ، الكود التالي لا معنى له:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
فراغ غير متزامن
لا تستخدم أبدًاasync void
. الاستثناء الذي تم طرحه في async void
الطريقة ينتشر إلى سياق المزامنة وعادة ما يتسبب في تعطل التطبيق بالكامل.إذا لم تتمكن من إرجاع المهمة في طريقتك (على سبيل المثال ، لأنك تقوم بتنفيذ الواجهة) ، انقل الرمز غير المتزامن إلى طريقة أخرى واستدعها:interface IInterface
{
void DoSomething();
}
class Implementation : IInterface
{
public void DoSomething()
{
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
تجنب التزامن كلما أمكن ذلك
بسبب العادة أو بسبب الذاكرة العضلية ، يمكنك كتابة شيء مثل:public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
على الرغم من أن الشفرة صحيحة دلالة ، فإن استخدام كلمة رئيسية async
غير مطلوب هنا ويمكن أن يؤدي إلى زيادة كبيرة في البيئة المحملة للغاية. حاول تجنبه كلما أمكن:public Task CallAsync()
{
var client = new Client();
return _client.GetAsync();
}
ومع ذلك ، ضع في اعتبارك أنه لا يمكنك اللجوء إلى هذا التحسين عندما يتم تغليف التعليمات البرمجية الخاصة بك في كتل (على سبيل المثال ، try/catch
أو using
):public async Task Correct()
{
using (var client = new Client())
{
return await client.GetAsync();
}
}
public Task Incorrect()
{
using (var client = new Client())
{
return client.GetAsync();
}
}
في النسخة الخاطئة ( Incorrect()
) ، قد يتم حذف العميل قبل إتمام GetAsync
المكالمة ، لأن المهمة داخل كتلة الاستخدام غير متوقعة في الانتظار.المقارنات الإقليمية
إذا لم يكن لديك سبب لاستخدام المقارنات الإقليمية ، فاستخدم المقارنات الترتيبية دائمًا . على الرغم من ذلك ، نظرًا للتحسينات الداخلية ، فإن هذا لا يهم كثيرًا بالنسبة لأشكال عرض البيانات في الولايات المتحدة ، فإن المقارنة هي ترتيب أبطأ لأشكال عرض المناطق الأخرى (وما يصل إلى أمرين من الحجم على Linux!). نظرًا لأن مقارنة السلاسل هي عملية متكررة في معظم التطبيقات ، يزداد الحمل بشكل كبير.ConcurrentBag <
T>
لا تستخدم أبدًا ConcurrentBag<
T>
بدون قياس الأداء . تم تصميم هذه المجموعة لحالات استخدام محددة للغاية (عندما يتم استبعاد العنصر في معظم الوقت من قائمة الانتظار بواسطة مؤشر الترابط الذي وضعه في قائمة الانتظار) ويعاني من مشكلات خطيرة في الأداء إذا تم استخدامه لأغراض أخرى. إذا كنت في حاجة الى جمع ذات ألوان أو تفضل ConcurrentQueue <
T>
.ReaderWriterLock / ReaderWriterLockSlim <
T >
لا تستخدم أبدًا بدون قياس الأداء. ReaderWriterLock<T>
/ReaderWriterLockSlim<T>
على الرغم من أن استخدام هذا النوع من المزامنة المتخصصة البدائية عند العمل مع القراء والكتاب يمكن أن يكون مغريًا ، إلا أن تكلفته أعلى بكثير من التكلفة البسيطة Monitor
(المستخدمة مع الكلمة الرئيسية lock
). إذا لم يكن عدد القراء الذين يؤدون القسم الحاسم في نفس الوقت كبيرًا جدًا ، فلن يكون التزامن كافيًا لامتصاص زيادة الحمل ، وسيعمل الرمز بشكل أسوأ.
تفضل وظائف لامدا بدلا من مجموعات الطريقة
خذ بعين الاعتبار الرمز التالي:public IEnumerable<int> GetItems()
{
return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
يقترح Resharper إعادة كتابة الرمز بدون وظيفة لامدا ، والتي قد تبدو أكثر نظافة:public IEnumerable<int> GetItems()
{
return _list.Where(Filter);
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
لسوء الحظ ، يؤدي هذا إلى تخصيص ذاكرة ديناميكية لكل مكالمة. في الواقع ، يتم تجميع المكالمة على النحو التالي:public IEnumerable<int> GetItems()
{
return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
يمكن أن يكون لهذا تأثير كبير على الأداء إذا تم استدعاء الرمز في قسم تم تحميله بكثرة.يبدأ استخدام وظائف lambda بتحسين برنامج التحويل البرمجي ، والذي يخزن المفوض في ذاكرة التخزين المؤقت في حقل ثابت ، مع تجنب التخصيص. هذا يعمل فقط إذا كانت Filter
ثابتة. إذا لم يكن الأمر كذلك ، يمكنك التخزين المؤقت للمفوض بنفسك:private Predicate<int> _filter;
public Constructor()
{
_filter = new Predicate<int>(Filter);
}
public IEnumerable<int> GetItems()
{
return _list.Where(_filter);
}
private bool Filter(int element)
{
return i % 2 == 0;
}
تحويل التعدادات إلى سلاسل
داعيا Enum.ToString
في .net
غير مكلفة للغاية، لأنه يتم استخدام انعكاس لتحويل الداخل، واستدعاء الأسلوب الظاهري على يستفز هيكل التعبئة والتغليف. يجب تجنب هذا قدر الإمكان.غالبًا ما يمكن استبدال التعداد بسلاسل ثابتة:
public enum Numbers
{
One,
Two,
Three
}
public static class Numbers
{
public const string One = "One";
public const string Two = "Two";
public const string Three = "Three";
}
إذا كنت تحتاج حقًا إلى استخدام التعداد ، ففكر في تخزين القيمة المحولة في القاموس في ذاكرة التخزين المؤقت لاستهلاك النفقات العامة.مقارنة التعداد
ملاحظة: هذا لم يعد مناسبًا في .net core ، نظرًا لأن الإصدار 2.1 ، يتم تنفيذ التحسين بواسطة JIT تلقائيًا.
عند استخدام التعداد كعلامات ، قد يكون من المغري استخدام الطريقة Enum.HasFlag
:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
Option3 = 4
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
يثير هذا الرمز حزمتين مع تخصيص: واحدة للتحويل Options.Option2
إلى Enum
، والأخرى لمكالمة افتراضية HasFlag
للبنية. هذا يجعل هذا الرمز باهظ الثمن بشكل غير متناسب. بدلاً من ذلك ، يجب عليك التضحية بإمكانية القراءة واستخدام عوامل التشغيل الثنائية:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
تنفيذ طرق المقارنة للهياكل
عند استخدام بنية في المقارنات (على سبيل المثال ، عند استخدامها كمفتاح لقاموس) ، تحتاج إلى تجاوز الأساليب Equals/GetHashCode
. يستخدم التطبيق الافتراضي الانعكاس وهو بطيء جدًا. عادةً ما يكون التنفيذ الناتج عن Resharper جيدًا جدًا.يمكنك معرفة المزيد حول هذا الأمر هنا: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-cتجنب التغليف غير المناسب عند استخدام الهياكل ذات الواجهات
خذ بعين الاعتبار الرمز التالي:public class IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue(IValue value)
{
}
public void LogValue(IValue value)
{
}
IntValue
يمكن أن يكون إنشاء الهيكل مغريًا لتجنب تخصيص ذاكرة ديناميكية. ولكن منذ AddValue
و SendValue
يتوقع واجهة، واجهات لها دلالات مرجعية، وسوف تكون معبأة قيمة مع كل مكالمة، يلغي فوائد هذا "التحسين". في الواقع ، سيكون هناك تخصيصات ذاكرة أكبر مما لو IntValue
كانت فئة ، حيث سيتم تجميع القيمة بشكل مستقل لكل مكالمة.إذا كنت تكتب واجهة برمجة تطبيقات وتتوقع أن تكون بعض القيم هياكل ، فجرّب استخدام طرق عامة:public struct IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue<T>(T value) where T : IValue
{
}
public void LogValue<T>(T value) where T : IValue
{
}
على الرغم من أن تحويل هذه الأساليب إلى عالمي يبدو عديم الفائدة للوهلة الأولى ، إلا أنه يسمح لك في الواقع بتجنب التعبئة مع التخصيص في الحالة عندما IntValue
يكون هيكلًا.إلغاء الاشتراكات المنطوقة مضمنة دائمًا
عند الإلغاء CancellationTokenSource
، سيتم تنفيذ جميع الاشتراكات داخل سلسلة المحادثات الحالية. يمكن أن يؤدي هذا إلى فترات توقف غير مخطط لها أو حتى جمود ضمني.var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel();
لا يمكنك الهروب من هذا السلوك. لذلك ، عند الإلغاء CancellationTokenSource
، اسأل نفسك عما إذا كان يمكنك السماح بالتقاط خيطك الحالي بأمان. إذا كانت الإجابة لا ، فلف المكالمة Cancel
بالداخل Task.Run
لتنفيذها في تجمع سلاسل المحادثات.غالبًا ما تكون عمليات متابعة TaskCompletionSource مضمنة
مثل الاشتراكات CancellationToken
، TaskCompletionSource
غالبًا ما تكون عمليات المتابعة مضمنة. يعد هذا تحسينًا جيدًا ، ولكن يمكن أن يسبب أخطاء ضمنية. على سبيل المثال ، خذ بعين الاعتبار البرنامج التالي:class Program
{
private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
public static async Task Deadlock()
{
await ProcessAsync();
_mutex.Wait();
}
private static Task ProcessAsync()
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(2000);
tcs.SetResult(true);
_mutex.Set();
});
return tcs.Task;
}
static void Main(string[] args)
{
Deadlock().Wait();
Console.WriteLine("Will never get there");
}
}
tcs.SetResult
تؤدي المكالمة إلى استمرار await ProcessAsync()
التنفيذ في مؤشر الترابط الحالي. لذلك ، _mutex.Wait()
يتم تنفيذ العبارة بواسطة نفس مؤشر الترابط الذي يجب أن يستدعيه _mutex.Set()
، مما يؤدي إلى طريق مسدود. يمكن تجنب ذلك عن طريق تمرير المعلمة TaskCreationsOptions.RunContinuationsAsynchronously
c TaskCompletionSource
.إذا لم يكن لديك سبب وجيه لإهماله ، فاستخدم الخيار دائمًا TaskCreationsOptions.RunContinuationsAsynchronously
عند الإنشاء TaskCompletionSource
.كن حذرًا: سيتم أيضًا تجميع التعليمات البرمجية إذا كنت تستخدم TaskContinuationOptions.RunContinuationsAsynchronously
بدلاً من ذلك TaskCreationOptions.RunContinuationsAsynchronously
، ولكن سيتم تجاهل المعلمات ، وستظل عمليات الاستمرارية مضمنة. هذا خطأ شائع بشكل مدهش لأنه TaskContinuationOptions
يسبق TaskCreationOptions
الإكمال التلقائي.Task.Run / Task.Factory.StartNew
إذا لم يكن لديك سبب لاستخدامه Task.Factory.StartNew
، فاختر دائمًا Task.Run
تشغيل مهمة في الخلفية. Task.Run
يستخدم قيمًا افتراضية أكثر أمانًا ، والأهم من ذلك أنه يقوم تلقائيًا بتفريغ المهمة التي تم إرجاعها ، والتي يمكن أن تمنع الأخطاء غير الواضحة بالطرق غير المتزامنة. خذ بعين الاعتبار البرنامج التالي:class Program
{
public static async Task ProcessAsync()
{
await Task.Delay(2000);
Console.WriteLine("Processing done");
}
static async Task Main(string[] args)
{
await Task.Factory.StartNew(ProcessAsync);
Console.WriteLine("End of program");
Console.ReadLine();
}
}
على الرغم من مظهره ، سيتم عرض نهاية البرنامج قبل الانتهاء من المعالجة. هذا لأنه Task.Factory.StartNew
سيعود Task<Task>
، ويتوقع الرمز مهمة خارجية فقط. قد يكون الرمز الصحيح إما await Task.Factory.StartNew(ProcessAsync).Unwrap()
، أو await Task.Run(ProcessAsync)
.هناك ثلاث حالات استخدام صالحة فقط Task.Factory.StartNew
:- تشغيل مهمة في برنامج جدولة آخر.
- تنفيذ مهمة في خيط مخصص (باستخدام
TaskCreationOptions.LongRunning
). - (
TaskCreationOptions.PreferFairness
).
.