البحث في الموقع
المحتوى عن 'سلسلة net. للمحترفين'.
-
التقاط الاستثناءات Catching يُمكن للشيفرة -بل يَنبغي لها- أن تُبلِّغ عن استثناءات Exceptions في بعض الظروف الاستثنائية. مثلًا: مُحاولة قراءة مَجْرى مقروء محاولة قراءة ملف بدون تَوفُّر صلاحيات الولوج محاولة القيام بعملية غير صالحة مثل القسمة على الصفر حُدوث انتهاء مهلة timeout أثناء تحميل ملف عبر الشبكة العنكبوتية ينبغي لمُستَدعِي ما caller التقاط catch الاستثناءات المُحتملة والتعامل معها فقط عندما: يستطيع إصلاح الظرف الاستثنائي أو التعافي منه بشكل مناسب. إضافة معلومات عن السياق الذي حَدَثَ خلاله الاستثناء، قبل أن يَقوم بإعادة التبلِّيغ عنه مرة أُخْرى. (تُلْتقَط الاستثناءات المُعاد إلقائها عن طريق مُلتقِطي الاستثناءات بمُستوى أعلى بمَكْدَس الاستدعاءات call stack). إذا أردت التقاط الاستثناءات المُلقاة من شيفرة معينة، اِستخدِم الصيغة التالية: try { } catch (ExceptionType) { } تُتضمَّن الشيفرة ذاتها داخل كتلة try، بينما تُتضمَّن الشيفرة المسئولة عن التقاط استثناء معين والتعامل معه بكتلة catch الخاصة به، كالمثال التالي: Console.Write("Please enter a filename: "); string filename = Console.ReadLine(); Stream fileStream; try { fileStream = File.Open(filename); } catch (FileNotFoundException) { Console.WriteLine("File '{0}' could not be found.", filename); } لا يُعدّ عدم التقاط استثناء معين خطأً بالضرورة، إذا كنت تنوي التقاطه والتعامل معه بمُستوى أعلى بمَكْدَس الاستدعاءات call stack. إعادة التبليغ Re-throwing إذا أردت تَنفيذ أمر مُعين في حالة حدوث استثناء، ولكنك في نفس الوقت لا تستطيع إكمال تَّنفيذ الشيفرة بسبب هذا الاستثناء، فببساطة يُمكنك التقاط الاستثناء ثم إعادة التبليغ عنه بعد تَّنفيذ الأمر الذي تُريده، وبذلك سيلتقِطه مُلتقِط الاستثناءات التالي بحسب مَكْدَس الاستدعاءات call stack. هناك عدة طرائق لفعل ذلك، بعضها جيد والآخر سيء: في المثال التالي، سيُبلَّغ عن استثناء من نوع DivideByZeroException. تَلتقِط كتلة catch الاستثناء وتَستخدِم الكلمة المفتاحية throw -بمُفردها وبدون تحدّيد قيمة الاستثناء- لإعادة التبليغ عن الاستثناء الذي قد اِلتَقَطته للتو. تُعدّ هذه الطريقة سليمة حيث يتم فيها الاحتفاظ بتعقبات المكدس stack trace بصورة سليمة. private static void AskTheUltimateQuestion() { try { var x = 42; var y = x / (x - x); // will throw a DivideByZeroException } catch (DivideByZeroException) { Console.WriteLine("Dividing by zero would destroy the universe."); throw; } } static void Main() { try { AskTheUltimateQuestion(); } catch { } } كالمثال السابق، تَلتقِط كتلة catch الاستثناء، لكن هنا تُعِيد التبليغ عنه باستخدام throw ex. في الواقع، رغم شيوع هذه الطريقة فهي غير سليمة؛ لأنها تُغيّر من تعقبات المكدس stack trace، التي ستُشير الآن إلى السطر الذي أعاد التبليغ عن الاستثناء بدلًا من الإشارة إلى المكان الأصلي المُتسبب به، لذلك لا تَستخدِم هذه الطريقة. private static void AskTheUltimateQuestion() { try { var x = 42; var y = x / (x - x); // will throw a DivideByZeroException Console.WriteLine("The secret to life, the universe, and everything is {1}", y); } catch (Exception ex) { Console.WriteLine("Something else horrible happened. The exception: " + ex.Message); throw ex; } } static void Main() { try { AskTheUltimateQuestion(); } catch { } } في الأمثلة السابقة، أُعيدّ التبليغ عن استثناءات من نفس النوع، أما إذا أردت أن تُغيّر نوع الاستثناء مع تَضمِين الاستثناء الأصلي بداخله، استخدِم التعبير throw new ExceptionType. في هذه الحالة، ستُشير تعقبات المكدس stack trace إلى السطر المُتسبب بالاستثناء الخارجي. ولكن يُمكنك بسهولة استخدام الخاصية InnerException للولوج إلى تعقبات المكدس stack trace الخاصة بالاستثناء الداخلي والتي ستُشير إلى السطر الأصلي المُتسبب به. كالمثال التالي: private static void AskTheUltimateQuestion() { try { var x = 42; Console.WriteLine("The secret to life, the universe, and everything is {1}", y); } catch (FormatException ex) { throw new InvalidOperationException("Watch your format string indexes.", ex); } } static void Main() { try { AskTheUltimateQuestion(); } catch { } } ترشيح الاستثناءات تُرَشَح الاستثناءات بناءً على النوع. مثلًا في المثال التالي، إذا بُلِّغ عن استثناء ما فسيتم تَّنفيذ أول كتلة catch مُتوافقة مع نوع الاستثناء. يُمكنك إضافة كتلة catch بالنهاية للتعامل مع النوع Exception، بحيث يُلجَئ إليها إذا لم تَتوافق معه كتلة أُخْرى أكثر تحدّيدًا: private static void AskTheUltimateQuestion() { try { var x = 42; var y = x / (x - x); Console.WriteLine("The secret to life, the universe, and everything is {1}", y); } catch (DivideByZeroException) { } catch (FormatException ex) { } catch (Exception ex) { } } static void Main() { try { AskTheUltimateQuestion(); } catch { } } لا تُرَشَح الاستثناءات فقط بناءً على النوع، فبدءًا من الإصدار 6 للغة c#، يُرشِح العَامِل when الاستثناءات بناءً على خاصياتها كذلك. كالتالي: try { // ... } catch (Exception e) when (e.InnerException != null) // Any condition can go in here. { // ... } يُشبه ذلك استخدام جمل شرطية بداخل كتلة catch، لكن دون التسبب بأي تَغيِير بالمَكْدَس في حالة عدم تَحقُّق الشرط. إعادة التبليغ عن استثناء بواسطة تابع آخر قد تَحتاج أحيانًا إلى التقاط استثناء وإعادة التبليغ عنه بواسطة تابع أو خيط thread آخر، وفي ذات الوقت تريد أن تحتفظ برصة التتبع الأصلية. يُستخدَم التابع ExceptionDispatchInfo.Capture للقيام بذلك كالتالي: using System.Runtime.ExceptionServices; void Main() { ExceptionDispatchInfo capturedException = null; try { throw new Exception(); } catch (Exception ex) { capturedException = ExceptionDispatchInfo.Capture(ex); } Foo(capturedException); } void Foo(ExceptionDispatchInfo exceptionDispatchInfo) { // Do stuff if (capturedException != null) { // Exception stack trace will show it was thrown from Main() and not from Foo() exceptionDispatchInfo.Throw(); } } استخدام كتلة finally تُنفَّذ كتلة finally {...} دائما بغض النظر عما إذا كان قد تم التبليغ عن استثناء أم لا أثناء تَّنفيذ الشيفرة الموجودة بكتلة try {...} -يُستثني من ذلك حدوث استثناء من نوع StackOverflowException أو إذا اُستدْعِيَ التابع Environment.FailFast()-، لذلك هي ملائمة لتحرير أيّ موارد resources حُجزت بكتلة try بطريقة آمنة. Console.Write("Please enter a filename: "); string filename = Console.ReadLine(); Stream fileStream = null; try { fileStream = File.Open(filename); } catch (FileNotFoundException) { Console.WriteLine("File '{0}' could not be found.", filename); } finally { if (fileStream != null) { fileStream.Dispose(); } } ترجمة -وبتصرف- للفصل Exceptions والفصل ForEach من كتاب .NET Framework Notes for Professionals
-
يُوفِّر إطار عمل .NET الصنف SpeechRecognitionEngine بفضاء الاسم System.Speech.Recognition لتَميِيز الكلام تلقائيًا. يُدعِّم مُمَيِّز الكلام -من النوع SpeechRecognitionEngine- عِدة أحَدَاث (events) تُثار تلقائيًا عند حُدوث ظرف معين. يُمكِن لمُعالِجات الأحَدَاث (event handlers) التَسجيل بتلك الأحداث، مما يَسمَح بإجراء العمليات المطلوبة بشكل غَيْر مُتزامِن (asynchronous). يُعدّ الحَدَث SpeechRecognized أحد أهم أحداث النوع SpeechRecognitionEngine، والذي يُثَار عندما يَستقبِل مُمَيِّز الكلام دَخْلًا يَتوَافق مع إحدى قواعده المُفعَّلة. تُخصَّص تلك القواعد من خلال كائن من النوع Grammar يُمرَّر للتابع LoadGrammar لوضع قيود على عملية تَميِيز الكلام. تَعرِض الشيفرة بالأسفل طريقة اِستخدَام النوع SpeechRecognitionEngine لبناء مُعالِج للحَدَث SpeechRecognized يقوم بإرسال الكلام المُمَيَّز إلى مَجْرى الخَرْج. تنشئة مُمَيِّز الكلام كالتالي: SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); التَسجيل بالحَدث SpeechRecognized كالتالي: recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; بدء تشغيل المُمَيِّز كالتالي: recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); تمييز مقيد بمجموعة جمل باستخدام GrammarBuilder SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); GrammarBuilder builder = new GrammarBuilder(); builder.Append(new Choices("I am", "You are", "He is", "She is", "We are", "They are")); builder.Append(new Choices("friendly", "unfriendly")); recognitionEngine.LoadGrammar(new Grammar(builder)); recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); تمييز حر بدون قيود (free text) باستخدام DictationGrammar SpeechRecognitionEngine recognitionEngine = new SpeechRecognitionEngine(); recognitionEngine.LoadGrammar(new DictationGrammar()); recognitionEngine.SpeechRecognized += delegate(object sender, SpeechRecognizedEventArgs e) { Console.WriteLine("You said: {0}", e.Result.Text); }; recognitionEngine.SetInputToDefaultAudioDevice(); recognitionEngine.RecognizeAsync(RecognizeMode.Multiple); ملاحظات: مُعامِلات التابع LoadGrammar: grammar: من النوع Grammar يُشير إلى القواعد التي يَنبغي تَحميلها. مثلًا، يُستخدَم كائن من النوع DictationGrammar -المُشتَق من النوع Grammar- لتمييز حُر دون قيود. مُعامِلات باني النوع Grammar: builder: من النوع GrammarBuilder. مُعامِلات التابع GrammarBuilder.Append: choices: من النوع Choices. يُلحِق هذا التابع مُكَوِّنًا جديد بتَسَلسُل القواعد (grammar sequence)، مع السَماح بعدة بدائل (choices) ضِمْن ذلك المُكَوِّن. يعني ذلك أنه عند اِستقبَال دَخْل صوتي من المُستخدِم، يُمكِن للمُمَيِّز اتباع عدة بدائل من القواعد. مُعامِلات باني النوع Choices: choices: مصفوفة من البدائل. مُعامِلات التابع RecognizeAsync: mode: من النوع تعداد RecognizeMode، لتحديد عدد عمليات التَميِيز المُمكن إجرائها إِمّا Single أو Multiple ترجمة -وبتصرف- للفصل SpeechRecognitionEngine class to recognize speech من كتاب .NET Framework Notes for Professionals
-
خدمات استدعاء المنصة Platform Invocation Services يُشار إلى الشيفرة التي تُنفَّذ داخل بيئة التَّنفيذ المشتركة (CLR) باسم الشيفرة المُدارة (managed code) بخلاف الشيفرة التي تُنفَّذ خارجها، والتي يُشار إليها بطبيعة الحال باسم الشيفرة غير المُدارة (unmanaged code). تَتوفَّر العديد من الطرائق لتَسهِيل العمليات بين الشيفرات (interoperability) من كِلَا النوعين. تُعدّ خدمات اِستدعاء المنصة Platform Invocation Services إحداها. استدعاء شيفرة غير مُدارة من أُخرى مُدارة يُمكِنك استدعاء إحدى الدوال غير المُدارة مثل دوال واجهة ويندوز لبرمجة التطبيقات Windows API -في حالة عدم وجودها بمكتبات الأصناف المُدارة (class libraries)- عن طريق التََّصرِيح عن تابع خارجي ساكن (static extern) يَحمِل نفس بَصمة الدالة المطلوب استدعائها. سيُعدّ هذا التابع بمثابة مُمثِل للدالة غير المُدارة بحيث تستطيع استدعائها باستدعائه. لتحقيق ذلك، لابُدّ أن يُزَخرَف هذا التابع بالسمة DllImportAttribute مع تمرير اسم مكتبة dll التي توجد بها تلك الدالة كمُعامِل للسمة. عند استدعائك لهذا التابع، ستُحمِّل خدمات استدعاء المنصة Platform Invocation Services مكتبة dll المُخصَّصة، ثم تَستدعِي الدالة غير المُدارة المُناظِرة للتابع. ستَحتاج إلى تَضمِين فضاء الاسم System.Runtime.InteropServices. انظر المثال التالي: using System.Runtime.InteropServices; class PInvokeExample { [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern uint MessageBox(IntPtr hWnd, String text, String caption, int options); public static void test() { MessageBox(IntPtr.Zero, "Hello!", "Message", 0); } } اِستعِن بتوثيق pinvoke.net قبل التَّصرِيح عن تابع خارجي مُمثِل لإحدى دوال واجهة ويندوز لبرمجة التطبيقات Windows API، فغالبًا ستَجِدْ معلومات عن الطريقة المُلائمة للتَّصرِيح عنه مع جميع الأنواع المطلوبة كمُعامِلات أو كنوع للقيمة المُعادة بالإضافة إلى بعض الأمثلة التوضيحية. ترتيب الأنواع (Marshalling) في حالة وجود مُعامِلات للدوال غير المُدارة أو قيم مُعادَة منها، فغالبًا ما تَتَمَكَّن خدمات استدعاء المنصة Platform Invocation Services من تحويل أنواع .NET المُدارة -بالتحديد الأنواع البسيطة- إلى أنواع المكتبة المُستخدَمة ضِمْن الاستدعاء والعكس تحويلًا أتوماتيكيًا وبدون تعليمات إضافية. تُعرَف هذه العملية باسم الترتيب (Marshalling). أما إذا كان المُعامِل من نوع مُركَّب مثل struct أو union، فستَحتَاج إلى التَّصرِيح عن صنف جديد struct أو class بشيفرة الـ c# الخاصة بك. يَعمَل هذا الصنف كمُمثِل للنوع غير المُدار، ولابُدّ أن يُزخرَف بالسمة StructLayoutAttribute لإعلام المُرَتِّب (marshaler) بطريقة رَبْط الحُقول (mapping). قد تحتاج أيضًا إلى مزيد من التَخصِيص. ترتيب النوع union يَستعرِض المثال التالي تَّصرِيح مكتبة c++ عن صنف union مُكَّون من حقول من نوع القيمة فقط: typedef union { char c; int i; } CharOrInt في هذه الحالة، تستطيع اِستخدَام LayoutKind.Explicit كقيمة لمُعامِل السمة StructLayout، كالتالي: [StructLayout(LayoutKind.Explicit)] public struct CharOrInt { [FieldOffset(0)] public byte c; [FieldOffset(0)] public int i; } أما إذا كان الصنف union مُكَّون من حقول من نَوعي القيمة والمَرجِع، كالتالي: typedef union { char text[128]; int i; } TextOrInt; في هذه الحالة، لا يُمكِنك مجرد اِستخدَام السمة FieldOffset كالمثال الأسبق. وإنما ستحتاج في الغالب إلى تَخصِيص عملية الترتيب (marshaling). مع ذلك، يُعدّ المثال بالأعلى بسيطًا نوعا ما، وتَتوفَّر طريقة مُبسَّطة باِستخدَام القيمة LayoutKind.Sequential كقيمة لمُعامِل السمة StructLayout مما يعني طريقة تسلسُليّة لرَبْط الحقول (mapping). قد تحتاج بعض أنواع حقول الصنف إلى الزَخرَفة باستخدَام السمة MarshalAs لتَخصِيص النوع غير المُدار المُناظِر. [StructLayout(LayoutKind.Sequential)] public struct TextOrInt { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)] public byte[] text; public int i { get { return BitConverter.ToInt32(text, 0); } } } ترتيب النوع struct يستعرض المثال التالي تَّصرِيح مكتبة c++ عن صنف struct: typedef struct _PERSON { int age; char name[32]; } PERSON, *LP_PERSON; void GetSpouse(PERSON person, LP_PERSON spouse); بصورة مشابهة للأنواع unions. ستحتاج إلى تَعرِيف نوع جديد مُزخرَف باستخدام السمة StructLayout يُمثِل نَظيره غير المُدار، كالتالي: [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct PERSON { public int age; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string name; } [DllImport("family.dll", CharSet = CharSet.Auto)] public static extern bool GetSpouse(PERSON person, ref PERSON spouse); ترتيب حقل مصفوفة غير مَعْلومة الحجم (إرسال) بصمة c++: typedef struct { int length; int *data; } VECTOR; void SetVector(VECTOR &vector); في هذه الحالة، ينبغي أن يُمرَّر حقل المصفوفة غير مَعْلومة الحجم كقيمة من النوع IntPtr مع الاستدعاء الصريح للتابعين Marshal.AllocHGlobal() و Marshal.FreeHGlobal() من أجل تَخصِيص مساحة بالذاكرة لهذا الحقل وتَّفرِيغها على الترتيب، كالتالي: [StructLayout(LayoutKind.Sequential)] public struct VECTOR : IDisposable { int length; IntPtr dataBuf; public int[] data { set { FreeDataBuf(); if (value != null && value.Length > 0) { dataBuf = Marshal.AllocHGlobal(value.Length * Marshal.SizeOf(value[0])); Marshal.Copy(value, 0, dataBuf, value.Length); length = value.Length; } } } void FreeDataBuf() { if (dataBuf != IntPtr.Zero) { Marshal.FreeHGlobal(dataBuf); dataBuf = IntPtr.Zero; } } public void Dispose() { FreeDataBuf(); } } [DllImport("vectors.dll")] public static extern void SetVector([In]ref VECTOR vector); ترتيب حقل مصفوفة غير مَعْلومة الحجم (استقبال) بصمة C++: typedef struct { char *name; } USER; bool GetCurrentUser(USER *user); عند استقبال شيفرة مُدارة لمصفوفة غير مَعْلومة الحجم من أُخرى غير مُدارة، فإن مساحة الذاكرة التي تَشغَلها المصفوفة تكون بطبيعة الحال مُخصَّصة بواسطة دوال الشيفرة غير المُدارة. في هذه الحالة، ينبغي للشيفرة المُدارة أن تَستقبِل البيانات إلى مُتغير من النوع IntPrt ثم تَقرَّأ بيانات المُخزِّن المؤقت (buffer) إلى مصفوفة مُدارة. إذا كانت المصفوفة من النوع string، يُمكن استخدام التابع Marshal.PtrToStringAnsi() المُخَصَّص لهذا الغرض. [StructLayout(LayoutKind.Sequential)] public struct USER { IntPtr nameBuffer; public string name { get { return Marshal.PtrToStringAnsi(nameBuffer); } } } [DllImport("users.dll")] public static extern bool GetCurrentUser(out USER user); ترتيب مصفوفة إذا كانت مصفوفة من نوع بسيط: [DllImport("Example.dll")] static extern void SetArray( [MarshalAs(UnmanagedType.LPArray, SizeConst = 128)] byte[] data); إذا كانت مصفوفة من النوع string: [DllImport("Example.dll")] static extern void SetStrArray(string[] textLines); ترجمة -وبتصرف- للفصل Platform Invoke من كتاب .NET Framework Notes for Professionals
-
تتكون تقنية Forms من عدة مكتبات تُسهِل من عملية تطوير التطبيقات. النموذج أو الاستمارة (form) هو ببساطة واجهة مُستخدِم مُكَوَّنة من عدة عناصر منفصلة (controls). تُستخدَم هذه العناصر لعَرْض معلومات للمُستخدِم أو استقبال مُدخَلات منه. تَعتمِد برمجة تطبيقات Forms بشكل أساسي على مفهوم الأحَدَاث (events)، والتي تُثار تلقائيًا عند ظرف معين أو نتيجة تَفاعُل المُستخدِم مع البرنامج. بالتالي، لابُد للبرنامج أن يُعالِج هذه الأحداث (event handlers) بتَّنْفيذ الإجراء المُتناسب. برنامج "أهلا بالعالم" على سبيل المثال، يُثار الحَدَث Load مرة وحيدة عند تَحمِيل النموذج (form) وعَرضه لأول مرة. في المقابل، يُثار الحَدَث Shown في كل مرة يُعرَض فيها النموذج. لعَرْض مربع رسالة (message box) في كل مرة يُعرَض فيها النموذج، اِستخدِم الشيفرة التالية: Public Class Form1 Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown MessageBox.Show("Hello, World!") End Sub End Class لِعَرْضِها مرة واحدة فقط عند تَحمِيل النموذج، اِستخدِم الشيفرة التالية: Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load MessageBox.Show("Hello, World!") End Sub End Class يُثار أيضًا الحَدَث Activate في كل مرة يقوم فيها المُستخدِم بتَفعيل النموذج. تنبيه: يُثار الحَدَث Load قبل الحَدَث Show. مع ذلك، إذا اِستدعَى مُعالِج الحَدَث Show التابع msgBox لِعَرْض مربع رسالة، فقد يؤدي ذلك إلى تَّنْفيذ التابع msgBox قبل انتهاء تنفيذ مُعالِج الحََدَث Load. لذلك، عامةً، لا يُنصَح بالاعتماد على ترتيب إثارة الحَدَثين Load و Show. المؤقت Timer لنفترض أن لدينا نموذج (form) يحتوي على العناصر التالية: زر، وعنوان (label)، ومؤقت (timer). يُهيئ المثال التالي مُعالِج حَدَث الضغط على الزر. يَعرِض ذلك المُعالِج التوقيت الحالي للمُستخدِم ثم يَضبُط مؤقت لمدة دقيقة، بعد انتهائها يُثار الحدث Tick تلقائيًا، مما يؤدي إلى تَّنْفيذ مُعالِج الحدث Tick الذي يُحدِّث قيمة الوقت المعروض: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False Timer1.Interval = 60 * 1000 'one minute intervals 'start timer Timer1.Start() Label1.Text = DateTime.Now.ToLongTimeString End Sub Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick Label1.Text = DateTime.Now.ToLongTimeString End Sub End Class مثال آخر هو محاولة اِستخدَام عنصر المؤقت Timer لمحاكاة مؤقت للعد التنازلي (countdown) لمدة معينة -مثلًا 3 دقائق. كالمثال السابق، يُضبَط المؤقت عند الضغط على الزر. لكن، في هذا المثال، ستُستخدََم أيضًا ساعة توقيت من النوع Stopwatch لموازنة مدة المؤقت المُحدَّدة مع الوقت الفعلي المُنصرِم منذ لحظة ضَبْط المؤقت وحتى تنْفيذ مُعالِج الحدث Tick. يَعرِض مُعالِج الحدث Tick الزمن المُنصرم بوحدة الثواني: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False ctSecs = 0 'clear count Timer1.Interval = 1000 'one second in ms. 'start timers stpw.Reset() stpw.Start() Timer1.Start() End Sub Dim stpw As New Stopwatch Dim ctSecs As Integer Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick ctSecs += 1 If ctSecs = 180 Then 'about 2.5 seconds off on my PC! 'stop timing stpw.Stop() Timer1.Stop() 'show actual elapsed time 'Is it near 180? Label1.Text = stpw.Elapsed.TotalSeconds.ToString("n1") End If End Sub End Class قد تَفترِض أنها لابُدّ وأن تكون مساوية للقيمة 180؟ في الواقع، ليس هذا ضروريًا. مثلًا قد عَرَضَ حاسوبي القيمة 182.5. السبب وراء هذا التفاوت هو أن عنصر المؤقت Windows.Forms.Timer أحادي الخيط (single-threaded)، ومحدود بدقة تصل إلى 55 ميللي ثانية، مما يعني أنه غير ملائم لتسجيل الزمن وتدقيقه، ولا ينبغي استخدامه لأغراض حساسة جدًا للزمن. يُمكن الحصول على نتائج أفضل باستخدام المؤقت وساعة الإيقاف بطريقة مختلفة، كالتالي: Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Button1.Enabled = False Timer1.Interval = 100 'one tenth of a second in ms. 'start timers stpw.Reset() stpw.Start() Timer1.Start() End Sub Dim stpw As New Stopwatch Dim threeMinutes As TimeSpan = TimeSpan.FromMinutes(3) Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick If stpw.Elapsed >= threeMinutes Then '0.1 off on my PC! 'stop timing stpw.Stop() Timer1.Stop() 'show actual elapsed time 'how close? Label1.Text = stpw.Elapsed.TotalSeconds.ToString("n1") End If End Sub End Class يوجد مؤقتات أخرى يمكن اِستخدَامها. قد تُساعِدك نتائج هذا البحث بهذا الخصوص. بعض النصائح للمبتدئين نَستعرِض بعض النصائح التي ينبغي أن يَتبِعهَا المبتدئون حتى يحصلوا على بداية موفقة مع VB.NET: اضبط الخيارات التالية: 'can be permanently set ' Tools / Options / Projects and Soluntions / VB Defaults Option Strict On Option Explicit On Option Infer Off Public Class Form1 End Class اِستخدِم العامل & وليس + لضم السَلاسَل النصية. عامةً ينبغي دراسة النوع String بشئ من التفصيل نظرًا للحاجة إلى استخدامه بكثرة. اقض بعض الوقت لاستيعاب الفرق بين أنواع القيمة وأنواع المَرجِع. لا تَستخدِم التابع Application.DoEvents أبدًا. ألق نظرة على الفقرة التنبيهية بالرابط. عندما تكون متأكدًا من حاجتك الماسة لاستخدامه، اسأل. التوثيق هو رفيق دربك. ترجمة -وبتصرف- للفصل VB Forms من كتاب .NET Framework Notes for Professionals
-
يُوفِّر فضاء الاسم System.Diagnostics العديد من الأصناف بهَدَف التَشخِيص، مثال أصناف للتَعامُل مع عمليات النظام (processes)، وأُخرى تَعمَل كعَدادات لقياس الأداء. أوامر الصدفة (shell commands) تنفيذ أوامر الصَّدَفَة يُستخدَم التابع Process.Start لتَّنْفيذ أمر صَّدَفَة (shell commands) تَّنْفيذًا برمجيًا. يتم ذلك من خلال إرساله للأمر المُمرَّر إليه إلى برنامج سطر الأوامر cmd.exe، كالتالي: using System.Diagnostics; string strCmdText = "/C copy /b Image1.jpg + Archive.rar Image2.jpg"; Process.Start("CMD.exe",strCmdText); تَتحَكَم الخاصية WindowStyle بحالة نافذة سطر الأوامر (cmd) عند تَّنْفيذ الأمر، فمثلًا يُمكِن إخفائها كالتالي: using System.Diagnostics; Process process = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.WindowStyle = ProcessWindowStyle.Hidden; startInfo.FileName = "cmd.exe"; startInfo.Arguments = "/C copy /b Image1.jpg + Archive.rar Image2.jpg"; process.StartInfo = startInfo; process.Start(); إرسال أوامر إلى سطر الأوامر واستقبال خرجها يُعيد التابع SendCommand -المُعرَّف بالمثال التالي- سِلسِلة نصية تتَضمَن مُحتوَى كلًا من مَجْرى الخَرْج القياسي (STDOUT) ومَجْرى الخَطأ القياسي (STDERR) بعد تَّنْفيذ أمر صدَفَة، بالاعتماد على الحَدَثين OutputDataReceived و ErrorDataReceived، كالتالي: private static string SendCommand(string command) { var cmdOut = string.Empty; var startInfo = new ProcessStartInfo("cmd", command) { WorkingDirectory = @"C:\Windows\System32", WindowStyle = ProcessWindowStyle.Hidden, // لإخفاء نافذة سطر الأوامر UseShellExecute = false, // لا تستخدم طرفية نظام التشغيل لبدء العملية CreateNoWindow = true, // ابدأ العملية بنافذة جديدة RedirectStandardOutput = true, // مطلوب لإتاحة مجرى الخرج القياسي RedirectStandardError = true // مطلوب لإتاحة مجرى الخطأ القياسي }; var p = new Process {StartInfo = startInfo}; p.Start(); p.OutputDataReceived += (x, y) => cmdOut += y.Data; p.ErrorDataReceived += (x, y) => cmdOut += y.Data; p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); return cmdOut; } يُمكِن استدعاء التابع بالأعلى كالآتي: var servername = "SVR-01.domain.co.za"; var currentUsers = SendCommand($"/C QUERY USER /SERVER:{servername}") يَكُون الخَرْج كالتالي: string currentUsers = "USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME Joe.Bloggs ica-cgp#0 2 Active 24692+13:29 25/07/2016 07:50 Jim.McFlannegan ica-cgp#1 3 Active . 25/07/2016 08:33 Andy.McAnderson ica-cgp#2 4 Active . 25/07/2016 08:54 John.Smith ica-cgp#4 5 Active 14 25/07/2016 08:57 Bob.Bobbington ica-cgp#5 6 Active 24692+13:29 25/07/2016 09:05 Tim.Tom ica-cgp#6 7 Active . 25/07/2016 09:08 Bob.Joges ica-cgp#7 8 Active 24692+13:29 25/07/2016 09:13" ملحوظة: سيُعيد التابع المُعرَّف بالأعلى مُحتوَى كلًا من مَجْرى الخَرْج القياسي (STDOUT) ومَجْرى الخٍَطأ القياسي (STDERR) مَضْمومين بسِلسِلة نصية واحدة؛ حيث يُلحِق كلًا من الحَدَثين OutputDataReceived و ErrorDataReceived البيانات المُستلَمة إلى نفس المُتَغيّر cmdOut. يَقتصِر أحيانًا الولوج إلى الخادم المَعنِّى على مُستخدِمين بعينهم. إذا كان لديك بيانات دخول مُستخدِم مُعين، فمن المُمكن أن تُرسِل اِستعلامات (queries) إلى ذلك الخادم كالتالي: private static string SendCommand(string command) { var cmdOut = string.Empty; var startInfo = new ProcessStartInfo("cmd", command) { WorkingDirectory = @"C:\Windows\System32", // This does not actually work in conjunction with "runas" - the console window will still appear! WindowStyle = ProcessWindowStyle.Hidden, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, Verb = "runas", Domain = "doman1.co.za", UserName = "administrator", Password = GetPassword() }; var p = new Process {StartInfo = startInfo}; p.Start(); p.OutputDataReceived += (x, y) => cmdOut += y.Data; p.ErrorDataReceived += (x, y) => cmdOut += y.Data; p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); return cmdOut; } التابع GetPassword لجلْب كلمة السر: static SecureString GetPassword() { var plainText = "password123"; var ss = new SecureString(); foreach (char c in plainText) { ss.AppendChar(c); } return ss; } ضبط الإعداد ProcessThread.ProcessorAffinity تُعبر الخاصية ProcessorAffinity من النوع IntPtr عن المُعالج (processor) الذي يُنفِّذ خيط العملية (process thread). يَعتمِد هذا الإعداد افتراضيًا على عدد مُعالِجات الحاسوب. اِستخدِم الشيفرة التالية لجَلْب خاصية ProcessorAffinity لعملية: public static int GetProcessAffinityMask(string processName = null) { Process myProcess = GetProcessByName(ref processName); int processorAffinity = (int)myProcess.ProcessorAffinity; Console.WriteLine("Process {0} Affinity Mask is : {1}", processName, FormatAffinity(processorAffinity)); return processorAffinity; } public static Process GetProcessByName(ref string processName) { Process myProcess; if (string.IsNullOrEmpty(processName)) { myProcess = Process.GetCurrentProcess(); processName = myProcess.ProcessName; } else { Process[] processList = Process.GetProcessesByName(processName); myProcess = processList[0]; } return myProcess; } private static string FormatAffinity(int affinity) { return Convert.ToString(affinity, 2) .PadLeft(Environment.ProcessorCount, '0'); } يُستخدَم كالآتي: private static void Main(string[] args) { GetProcessAffinityMask(); Console.ReadKey(); } الخَرْج: // Output: // Process Test.vshost Affinity Mask is : 11111111 اِستخدِم الشيفرة التالية لضبط خاصية ProcessorAffinity لعملية: public static void SetProcessAffinityMask(int affinity, string processName = null) { { Process myProcess = GetProcessByName(ref processName); Console.WriteLine("Process {0} Old Affinity Mask is : {1}", processName, FormatAffinity((int)myProcess.ProcessorAffinity)); myProcess.ProcessorAffinity = new IntPtr(affinity); Console.WriteLine("Process {0} New Affinity Mask is : {1}", processName, FormatAffinity((int)myProcess.ProcessorAffinity)); } يُستخدَم كالآتي: private static void Main(string[] args) { int newAffinity = Convert.ToInt32("10101010", 2); SetProcessAffinityMask(newAffinity); Console.ReadKey(); } الخَرْج: // Output : // Process Test.vshost Old Affinity Mask is : 11111111 // Process Test.vshost New Affinity Mask is : 10101010 قياس الأداء باستخدام النوع Stopwatch يُمكِن اِستخدَام النوع Stopwatch بفضاء الاسم System.Diagnostics لقياس أداء (benchmark) كُتلة من الشيفرة، كالتالي: using System; using System.Diagnostics; public class Benchmark : IDisposable { private Stopwatch sw; public Benchmark() { sw = Stopwatch.StartNew(); } public void Dispose() { sw.Stop(); Console.WriteLine(sw.Elapsed); } } public class Program { public static void Main() { using (var bench = new Benchmark()) { Console.WriteLine("Hello World"); } } } مواصفات اصطلاحية للشيفرة (code contracts) يُوفِّر فضاء الاسم System.Diagnostics.Contracts العديد من الأصناف لتَعزيز الشيفرة الخاصة بك بمزيد من الشروط (conditions) اللازم تَحقيقها إمّا خلال وقت التَصْريف أو التَّنْفيذ، مما يُحسِن من فَحْص الشيفرة واكتشاف الأخطاء. تثبيت المواصفات الاصطلاحية للشيفرة وتفعيلها يأتي فضاء الاسم System.Diagnostics.Contracts ضِمْن إطار عمل .NET. لكن ما زلت في حَاجة إلى تَثْبيت الإضافة Code Contracts Tools ببيئة التطوير المتكاملة فيجوال ستوديو (Visual Studio IDE) حتى تستطيع اِستخدَام المُواصَفَات الاصطلاحيّة للشيفرة (code contracts). يُمكنك البحث عن Code Contracts بنافذة الإضافات والتحديثات Extensions and Updates بفيجوال ستوديو. يجب أن تُفعِّل خاصية المُواصَفَات الاصطلاحيّة للشيفرة (code contracts) بحل المشروع بعد الإنتهاء من تَثْبيت الإضافة. تحتاج إلى تَّفْعِيل خاصية الفَحْص الساكن (Static Checking) -فَحْص ما بعد البناء (build)- على الأقل. قد تَرغَب أيضًا بتَّفْعِيل خاصية الفَحْص أثناء التشغيل (Runtime Checking) بالتحديد إذا كنت تُطَوِّر مكتبة (library) ستُستَعَمَل من قِبل حلول (solutions) أُخرى. الشروط المسبقة (Preconditions) يَضمَن استخدام الشروط المُسبَقة للتوابع (preconditions) الحد الأدنى من مُتطلَّبات قيم مُعامِلات الدَخْل لتلك التوابع. انظر المثال التالي: void DoWork(string input) { Contract.Requires(!string.IsNullOrEmpty(input)); //do work } نتائج تحليل الفَحْص الساكن: الشروط اللاحقة (Postconditions) يَضمَن اِستخدَام الشروط اللاحقة للتوابع (postconditions) تَوَافُق النتائج التي تُعيدها تلك التوابع مع التَعرِيف المُخصص للنتائج المتوقعة. يُساعد ذلك على تنْفيذ مُبسَط من خلال إمداد المُحلِّل الساكن (static analyizer) بالعديد من النتائج المُحتَملة. انظر المثال التالي: string GetValue() { Contract.Ensures(Contract.Result<string>() != null); return null; } نتائج تحليل الفَحْص الساكن: إضافة مواصفات اصطلاحيّة للشيفرة إلى الواجهات (interfaces) يُمكِن أيضًا فَرْض مُواصَفَات اصطلاحيّة للشيفرة (code contracts) على وَاجِهة (interface) عن طريق الإعلان عن صنف مُجرَّد (abstract class) يُنفِّذ هذه الواجهة بشرط أن تُزخرَف الواجهة والصنف المُجرَّد بالسمتين ContractClassAttribute و ContractClassForAttribute على الترتيب. انظر المثال التالي: [ContractClass(typeof(MyInterfaceContract))] public interface IMyInterface { string DoWork(string input); } [ContractClassFor(typeof(IMyInterface))] internal abstract class MyInterfaceContract : IMyInterface { private MyInterfaceContract() { } public string DoWork(string input) { Contract.Requires(!string.IsNullOrEmpty(input)); Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); throw new NotSupportedException(); } } public class MyInterfaceImplmentation : IMyInterface { public string DoWork(string input) { return input; } } في المثال بالأعلى، تُعلِّن الواجهة IMyInterface عن التابع DoWork الذي يَستقبِل مُعامِلًا من النوع string. في الحالة العادية، يُمكنك أن تُمرِّر القيمة الفارغة null إليه. لكن لا يُصبِح ذلك مُمكنًا بعد إضافة المُواصفة الإصطلاحيّة بالأعلى والتي تَستخدِم التابع Contract.Requires لفَرْض شرط مُسبَق (precondition) بألا يَكون المُعامِل المُمرّر فارغًا. نتائج تحليل الفَحْص الساكن: ترجمة -وبتصرف- للفصول System.Diagnostics - Code Contracts - Process and Thread Affinity setting من كتاب .NET Framework Notes for Professionals
-
تُستخدَم الشجرة التعبيرية (Expression Tree) عند الحاجة لإنشاء تعبيرات (expressions) خلال زمن التشغيل (runtime)، مما يجعلها مناسبة للأغراض التالية: مع الواجهات IEnumerable و IQueryable لفَحْص خَبَر (predicate) معين. مع Entity Framework أو LINQ to SQL لتنشئة عبارة Where لفَحْص خَبَر معين. يُمكِن إنشاء شجرة تعبيرية (Expression Tree) بطريقتين أساسيتين: الأولى: اِستخدَام واجهة برمجة التطبيقات (API) الخاصة بالنوع Expression (طريقة يدوية). الثانية: إِسْناد دالة مُجرَّدة (lambda expression) إلى مُتغيّر من النوع Expression (طريقة مُولدَّة آليًا). إنشاء النوع BinaryExpression باستخدام واجهة برمجة التطبيقات على سبيل المثال، إذا كان لديك كُلًا من الخَبَر _ => _.Field وسِلسِلة نصية تَحمِل القيمة "VALUE"، يُمكِنك إنشاء التعبير _ => _.Field == "VALUE" أثناء زمن التشغيل (runtime) لفَحْص قيمة الخَبَر. في الشيفرة التالية، عُرِّف التابع BuildEqualPredicate والذي يَستخدِم واجهة برمجة التطبيقات الخاصة بالصنف Expression، فيَستدعِي التابع Expression.Equal ليُنشِئ شجرة تعبيرية من النوع BinaryExpression تَفحَص ما إذا كانت قيمة المُتغيّر Field مُساوية للسِلسِلة النصية "VALUE": public static Expression<Func<T, bool>> BuildEqualPredicate<T>( Expression<Func<T, string>> memberAccessor, string term) { var toString = Expression.Convert(Expression.Constant(term), typeof(string)); Expression expression = Expression.Equal(memberAccessor.Body, toString); var predicate = Expression.Lambda<Func<T, bool>>( expression, memberAccessor.Parameters); return predicate; } يُمكن تَمرير الخَبَر المُنشَئ (predicate) كمُعامِل للتابع المُوسِع Where، كالتالي: var predicate = PredicateExtensions.BuildEqualPredicate<Entity>( _ => _.Field, "VALUE"); var results = context.Entity.Where(predicate).ToList(); تنشئة النوع LambdaExpression بإسناد دالة مجردة إلى متغير من النوع Expression عادةً ما تُسْنَد الدوال المُجرَّدة (lambda expressions) إلى مُتغيرات من النوع Delegate تَعمَل كمُفوِّض قابل للاستدعاء. في المُقابل، يُمكنك إِسنادها إلى مُتَغيّر من النوع Expression، وفي هذه الحالة، يُولِّد مُصرِّف C# شجرة تعبيرية (Expression Tree) مُكافئة، فمثلًا: Expression<Func<int, int>> expression = a => a + 1; يُولَّد عن المثال بالأعلى شجرة تعبيرية (Expression Tree) من النوع LambdaExpression مُكافِئة للشيفرة التالية: ParameterExpression parameterA = Expression.Parameter(typeof(int), "a"); var expression = (Expression<Func<int, int>>)Expression.Lambda( Expression.Add( parameterA, Expression.Constant(1)), parameterA); تُمثِل الشجرة التعبيرية من النوع LambdaExpression دالة مُجرَّدة تتكون من مَتْن الدالة body وقائمة المُتغيّرات. مثلًا، في المثال بالأعلى، تَستقبِل الدالة المُمثَّلة مُعامِلًا وحيدًا يُدعى a بينما يتكون المَتن من عبارة وحيدة من النوع BinaryExpression بخاصية NodeType من النوع Add. يُمثِل هذا التعبير بدوره عملية جَمع تتكون من تعبيرين فرعيين (sub-expressions) يُشار إليهما بالتعبيرين الأيمن والأيسر. التعبير الأيسر هو من النوع ParameterExpression يُمثِل المُعامِل a المُمرَّر، أما التعبير الأيمن فهو من النوع ConstantExpression بقيمة تساوي الواحد. أبسط ما يُمكنك القيام به هو طباعة قيمة التَعبير (expression)، والذي بدوره يَطبع شيفرة C# المكافئة كالتالي: Console.WriteLine(expression); //prints a => (a + 1) يُستخدَم التابع Compile لتَصرِّيف الشجرة التعبيرية (expression tree) إلى مُتغيّر من النوع Delegate، قابل للاستدعاء ببيئة التَّنفيذ المُشتركة (CLR)، كالتالي: Func<int, int> lambda = expression.Compile(); Console.WriteLine(lambda(2)); //prints 3 إنشاء النوع MemberExpression باستخدام واجهة برمجة التطبيقات عادةً ما تُترجَم التعبيرات (expressions) إلى لغات آخرى مثل SQL، لكن يُمكِن اِستخدَامها أيضًا لاستدعاء أعضاء الأصناف (members) سواء كانت هذه الأعضاء خاصة (private) أو (internal) أو (protected) وسواء ما كانت الأنواع عَلَّنية (public) أم لا، كطريقة بديلة للانعكاس (Reflection). بفرض أن لديك الصنف التالي: public TestClass { public static string StaticPublicField = "StaticPublicFieldValue"; } يمكن استرجاع قيمة الخاصية StaticPublicField الساكنة (static) كالتالي: var fieldExpr = Expression.Field(null, typeof(TestClass), "StaticPublicField"); var labmda = Expression.Lambda<Func<string>>(fieldExpr); يُمكن تَصرِيف الشجرة التعبيرية لمُفوِّض يُمكِن استدعائه للولوج لقيمة الخاصية: Func<string> retriever = lambda.Compile(); var fieldValue = retriever(); إنشاء النوع InvocationExpression باستخدام واجهة برمجة التطبيقات يُستخدَم التابع الساكن Expression.Invoke لإنشاء شجرة تعبيرية من النوع InvocationExpression. يُمَكِّنك هذا النوع من استدعاء دوال مُجرَّدة أُخرى (lambda expressions) مُضمنة بالشجرة التعبيرية ذاتها (Expression tree). المشكلة: نريد الوصول إلى العناصر التي تحتوي خاصية Description الخاصة بهم على السِلسِلة النصية "car". نحتاج إلى التأكد من أن تلك الخاصية ليست فارغة null قبل البحث فيها عن السِلسِلة النصية، لكن لا نريد أن نُفْرِط في استدعائها لأن الكلفة قد تكون عالية. using System; using System.Linq; using System.Linq.Expressions; public class Program { public static void Main() { var elements = new[] { new Element { Description = "car" }, new Element { Description = "cargo" }, new Element { Description = "wheel" }, new Element { Description = null }, new Element { Description = "Madagascar" }, }; var elementIsInterestingExpression = CreateSearchPredicate( searchTerm: "car", whereToSearch: (Element e) => e.Description); Console.WriteLine(elementIsInterestingExpression.ToString()); var elementIsInteresting = elementIsInterestingExpression.Compile(); var interestingElements = elements.Where(elementIsInteresting); foreach (var e in interestingElements) { Console.WriteLine(e.Description); } var countExpensiveComputations = 0; Action incCount = () => countExpensiveComputations++; elements .Where( CreateSearchPredicate( "car", (Element e) => ExpensivelyComputed( e, incCount ) ).Compile() ) .Count(); Console.WriteLine("Property extractor is called {0} times.", countExpensiveComputations); } private class Element { public string Description { get; set; } } private static string ExpensivelyComputed(Element source, Action count) { count(); return source.Description; } private static Expression<Func<T, bool>> CreateSearchPredicate<T> (string searchTerm, Expression<Func<T, string>> whereToSearch) { var extracted = Expression.Parameter(typeof(string), "extracted"); Expression<Func<string, bool>> coalesceNullCheckWithSearch = Expression.Lambda<Func<string, bool>>( Expression.AndAlso( Expression.Not( Expression.Call(typeof(string), "IsNullOrEmpty", null, extracted) ), Expression.Call(extracted, "Contains", null, Expression.Constant(searchTerm)) ), extracted); var elementParameter = Expression.Parameter(typeof(T), "element"); return Expression.Lambda<Func<T, bool>>( Expression.Invoke( coalesceNullCheckWithSearch, Expression.Invoke(whereToSearch, elementParameter) ), elementParameter ); } } الخَرْج: element => Invoke( extracted => (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")), Invoke(e => e.Description, element)) car cargo Madagascar Predicate is called 5 times. تم تَضمِين الولوج للخاصية Description بداخل التابع Invoke كالتالي: Invoke(e => e.Description, element) وهذا هو المكان الوحيد الذي يتم التعامل فيه مع الخاصية Description مباشرة، فقد تم استخراج متُغير آخر من النوع String وتمريره للتابع التالي. (Not(IsNullOrEmpty(extracted)) AndAlso extracted.Contains("car")) من المهم أن تكون على دراية بكيفية عمل العَامِل AndAlso. إذا آلت قيمة المُعامِل الأيسر للقيمة المنطقية false، فإن التابع AndAlso يُعيد نفس ذات القيمة false دون أن يحسِب قيمة المُعامِل الأيمن. يُعدّ استخدام العَامِل And أحد أكثر الأخطاء شيوعًا. فبالإضافة إلى حِسابه لقيمة كلا المُعامِلين دومًا بغض النظر عن قيمتهما، فإنه أيضًا قد يُبلِّغ عن اعتراض من النوع NullReferenceException إذا اُستخدِم بشكل مشابه للمثال بالأعلى. ترجمة -وبتصرف- للفصل Expression Trees من كتاب .NET Framework Notes for Professionals
-
خادم HTTP إنشاء خادم HTTP باستخدام الصنف HttpListener يُستخدَم النوع HttpListener لإنشاء مُستمِع (listener) مُبسط للرد على طلبات HTTP. نُنشِئ نسخة من هذا النوع كالتالي: listener = new HttpListener(); تُستخدَم الخاصية Prefixes لتخصيص الرابط (url) الذي يَستمِع إليه الخادم والذي ستُرسَل إليه طلبات الـ HTTP. listener.Prefixes.Add("http://*:" + port + "/"); listener.Start(); عندما يَستلِم الخادم طَلَبًا (http request) مُعينًا، فإنه بالضرورة يحتاج إلى معلومات عن الطلب حتى يقوم بمُعالجته. تَتوَفر تلك المعلومات من خلال التابع GetContext(). var context = listener.GetContext(); var request = context.Request; response = context.Response; نظرًا لأن الهدف من خادم الملفات هو إرسال الملفات عند طلبها، سيقوم الخادم أولًا بتحديد اسم الملف المطلوب: var fileName = request.RawUrl.Substring(1); ثم يُرسِل مُحتويات الملف إلى مَجْرى مَتْن الرد (response body) كالتالي: using (var fileStream = File.OpenRead(fullFilePath)) { response.ContentType = "application/octet-stream"; response.ContentLength64 = (new FileInfo(fullFilePath)).Length; response.AddHeader("Content-Disposition", "Attachment; filename=\"" + Path.GetFileName(fullFilePath) + "\""); fileStream.CopyTo(response.OutputStream); } response.OutputStream.Close(); المثال بالكامل: using System; using System.IO; using System.Net; class HttpFileServer { private static HttpListenerResponse response; private static HttpListener listener; private static string baseFilesystemPath; static void Main(string[] args) { if (!HttpListener.IsSupported) { Console.WriteLine( "*** HttpListener requires at least Windows XP SP2 or Windows Server 2003."); return; } if(args.Length < 2) { Console.WriteLine("Basic read-only HTTP file server"); Console.WriteLine(); Console.WriteLine("Usage: httpfileserver <base filesystem path> <port>"); Console.WriteLine("Request format: http://url:port/path/to/file.ext"); return; } baseFilesystemPath = Path.GetFullPath(args[0]); var port = int.Parse(args[1]); listener = new HttpListener(); listener.Prefixes.Add("http://*:" + port + "/"); listener.Start(); Console.WriteLine("--- Server stated, base path is: " + baseFilesystemPath); Console.WriteLine("--- Listening, exit with Ctrl-C"); try { ServerLoop(); } catch(Exception ex) { Console.WriteLine(ex); if(response != null) { SendErrorResponse(500, "Internal server error"); } } } static void ServerLoop() { while(true) { var context = listener.GetContext(); var request = context.Request; response = context.Response; var fileName = request.RawUrl.Substring(1); Console.WriteLine("--- Got {0} request for: {1}", request.HttpMethod, fileName); if (request.HttpMethod.ToUpper() != "GET") { SendErrorResponse(405, "Method must be GET"); continue; } var fullFilePath = Path.Combine(baseFilesystemPath, fileName); if(!File.Exists(fullFilePath)) { SendErrorResponse(404, "File not found"); continue; } Console.Write(" Sending file..."); using (var fileStream = File.OpenRead(fullFilePath)) { response.ContentType = "application/octet-stream"; response.ContentLength64 = (new FileInfo(fullFilePath)).Length; response.AddHeader("Content-Disposition", "Attachment; filename=\"" + Path.GetFileName(fullFilePath) + "\""); fileStream.CopyTo(response.OutputStream); } response.OutputStream.Close(); response = null; Console.WriteLine("Ok!"); } } static void SendErrorResponse(int statusCode, string statusResponse) { response.ContentLength64 = 0; response.StatusCode = statusCode; response.StatusDescription = statusResponse; response.OutputStream.Close(); Console.WriteLine("*** Sent error: {0} {1}", statusCode, statusResponse); } } إنشاء خادم HTTP باستخدام ASP.NET Core بالمثل، نُنشِئ خادم ببروتوكول HTTP لقراءة الملفات (file server) مُشابه للمثال بالأعلى لكن باستخدام بيئة عمل ASP.NET Core المتطورة. أولًا: اِنشِئ مجلد فارغ، سنُضيف إليه ملفات المشروع المُنشئة خلال الخطوات التالية. ثانيًا: اِنشِئ ملف باسم project.json وأَضِف إليه المحتويات التالية: { "dependencies": { "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final", "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:60000" }, "frameworks": { "dnxcore50": { } }, "fileServer": { "rootDirectory": "c:\\users\\username\\Documents" } } ثالثًا: اِنشِئ ملف باسم Startup.cs وأضف إليه الشيفرة التالية: using System; using Microsoft.AspNet.Builder; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.StaticFiles; using Microsoft.Extensions.Configuration; public class Startup { public void Configure(IApplicationBuilder app) { var builder = new ConfigurationBuilder(); builder.AddJsonFile("project.json"); var config = builder.Build(); var rootDirectory = config["fileServer:rootDirectory"]; Console.WriteLine("File server root directory: " + rootDirectory); var fileProvider = new PhysicalFileProvider(rootDirectory); var options = new StaticFileOptions(); options.ServeUnknownFileTypes = true; options.FileProvider = fileProvider; options.OnPrepareResponse = context => { context.Context.Response.ContentType = "application/octet-stream"; context.Context.Response.Headers.Add( "Content-Disposition", $"Attachment; filename=\"{context.File.Name}\""); }; app.UseStaticFiles(options); } } لاحظ اِستخدَام التابع UseStaticFiles لتزويد الخادم بخاصية قراءة الملفات الساكنة من مجلد معين. رابعًا: اِفتَح سطر الأوامر (command prompt) في المجلد الذي قُمت لتوك بإنشائه، ونفذ الأوامر التالية: dnvm use 1.0.0-rc1-final -r coreclr -p dnu restore تُنْفَّذ الأوامر بالأعلى مرة واحدة فقط. خامسًا: شَغِّل الخادم باستخدَام الأمر dnx web. كلمة web هنا هي مُجرد كلمة مُخصَّصة ضِمْن حَقْل الأوامر commands بالملف project.json وتُستخدَم اسمًا تعريفيًا لأمر مُخصَّص، وبالتالي ما يُنْفَّذ بالفعل هو قيمة هذا الحقل بالملف. `Microsoft.AspNet.Server.Kestrel --server.urls http://localhost:60000 والآن تستطيع إرسال طلبات (requests) إلى الخادم من خلال الرابط http://localhost:60000 وهو نفس الرابط (url) المُخصَّص بالأعلى. لاحظ أننا لغرض التبسيط قد افترضنا أن أسماء جميع الملفات ستكون بترميز ASCII، بالإضافة إلى أننا لم نُعالِج الأخطاء المُحتمَلة أثناء الولوج للملفات. عميل HTTP يَتوَفر الصنف HttpClient من خلال حزمة مكتبات مايكروسوفت Microsoft HTTP Client Libraries. إرسال طلب GET باستخدام HttpClient.GetAsync يُرسِل التابع GetAsync طلب GET إلى خَادِم (server) عن طريق رابط يُمرَّر إليه كمُعامِل، ويُعيد قيمة من النوع Task<HttpResponseMessage> تُمثِل رد الخادم (response). يُمكن قراءة الرد كـسِلسِلة نصية string باستخدام التابع response.Content.ReadAsStringAsync، كالمثال التالي: string requestUri = "http://www.example.com"; string responseData; using (var client = new HttpClient()) { using(var response = client.GetAsync(requestUri).Result) { response.EnsureSuccessStatusCode(); responseData = response.Content.ReadAsStringAsync().Result; } } إرسال طلب GET باستخدام HttpClient.GetStreamAsync يُرسِل التابع HttpClient.GetStreamAsync طلب GET إلى خَادِم (server) عن طريق رابط يُمرَّر إليه كمُعامِل، ولكنه يُعيد مَتْن الرد (response body) في صورة مَجْرى (stream). private static async Task DownloadAsync(string fromUrl, string toFile) { using (var fileStream = File.OpenWrite(toFile)) { using (var httpClient = new HttpClient()) { Console.WriteLine("Connecting..."); using (var networkStream = await httpClient.GetStreamAsync(fromUrl)) { Console.WriteLine("Downloading..."); await networkStream.CopyToAsync(fileStream); await fileStream.FlushAsync(); } } } } يُمكن استدعاء الدالة المُعرَّفة بالأعلى كالتالي: using System; using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; class HttpGet { static void Main(string[] args) { try { Run(args).Wait(); } catch (Exception ex) { if (ex is AggregateException) ex = ((AggregateException)ex) .Flatten().InnerExceptions.First(); Console.WriteLine("--- Error: " + (ex.InnerException?.Message ?? ex.Message)); } } static async Task Run(string[] args) { if (args.Length < 2) { Console.WriteLine("Basic HTTP downloader"); Console.WriteLine(); Console.WriteLine("Usage: httpget <url>[<:port>] <file>"); return; } await DownloadAsync(fromUrl: args[0], toFile: args[1]); Console.WriteLine("Done!"); } } إرسال طلب POST باستخدام HttpClient.SendAsync يُهيَّّئ الطلب في صورة كائن من النوع HttpRequestMessage، فمثلًا تُسْنَد قيمة الرابط للخاصية RequestUri بينما تُسنَد قيمة المَتْن (request body) للخاصية Content. يُرسِل التابع SendAsync طلب POST إلى الخَادِم مع قيمة الطلب المُهيَّئ، ويُعيد قيمة من النوع Task<HttpResponseMessage> تُمثِل رد الخادم (response)، كالمثال التالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; using (var client = new HttpClient()) { var request = new HttpRequestMessage { RequestUri = requestUri, Method = requestMethod, }; byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestBodyString); request.Content = new ByteArrayContent(requestBodyBytes); request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); HttpResponseMessage result = client.SendAsync(request).Result; result.EnsureSuccessStatusCode(); } إرسال طلب GET باستخدام HttpWebRequest.GetResponse يُهيِّئ التابع WebRequest.Create طلب GET إلى خادم (server) بمُعامِل الرابط المُمرَّر إليه، ثم يتم الارسال الفعلي بواسطة التابع GetResponse وتُعاد قيمة من النوع WebResponse تُمثِل رد الخادم. في المثال التالي: string requestUri = "http://www.example.com"; string responseData; HttpWebRequest request =(HttpWebRequest)WebRequest.Create(parameters.Uri); WebResponse response = request.GetResponse(); يُمكن تَحويل رد الخَادِم إلى سِلسِلة نصية كالتالي: using (StreamReader responseReader = new StreamReader(response.GetResponseStream())) { responseData = responseReader.ReadToEnd(); } إرسال طلب POST باستخدام HttpWebRequest.GetResponse يُهيِّئ التابع WebRequest.Create طلب POST إلى خادم (server) بمُعامِل الرابط المُمرر إليه. قد تحتاج إلى تهيئة مَتْن الطلب (request body) أيضًا. للقيام بذلك، استخدم التابع GetRequestStream لاستعادة مَتْن الطلب بصورة مَجْرى (stream) يُكتَّب عليه المُحتوَى المطلوب. بعد انتهاء التهيئة، يتم الارسال الفعلي بواسطة التابع GetResponse وتُعاد قيمة من النوع WebResponse تُمثِل رد الخادم. في المثال التالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri) { Method = requestMethod, ContentType = contentType, }; byte[] bytes = Encoding.UTF8.GetBytes(requestBodyString); Stream stream = request.GetRequestStream(); stream.Write(bytes, 0, bytes.Length); stream.Close(); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); رفع ملفات إلى خادم باستخدام HttpWebRequest.GetResponseAsync تُرفَّع الملفات إلى الخوادم من خلال إرسال طلب POST إلى الخادم مع إرفاق مُحتوَى الملف ضِمْن مَتْن الطلب. يُهيِّئ التابع WebRequest.CreateHttp طلب (request) إلى خادم (server) بمُعامِل الرابط المُمرَّر إليه. var request = WebRequest.CreateHttp(url); قد تحتاج إلى تهيئة إضافية للطلب مثل إضافة مَتْن إليه (request body). للقيام بذلك، اِستخدِم التابع GetRequestStream لاستعادة مَتْن الطلب بصورة مَجْرى (stream). تستطيع الكتابة على المَجْرى مباشرة أو تَضْمِينه داخل كائن من النوع StreamWriter ثم تَكتِب عليه المُحتوَى المطلوب. using (var requestStream = request.GetRequestStream()) using (var writer = new StreamWriter(requestStream)) { await writer.WriteAsync(""); } نَحتَاج لرَفع ملف إلى الخادم، مما يعني كتابة مُحتوَيات هذا الملف على مَجْرى مَتْن الطلب. يُفتَح الملف أولًا في صورة مَجْرى ثم تُنقَل محتوياته لمَجْرى الطلب، كالتالي: using (var fileStream = File.OpenRead(filename)) await fileStream.CopyToAsync(requestStream); بعد انتهاء التهيئة، يتم الارسال الفعلي للطلب بواسطة التابع GetResponseAsync وتُعاد قيمة من النوع Task<WebResponse> تُمثِل رد الخادم، كالتالي: var response = (HttpWebResponse) await request.GetResponseAsync(); تستعرض الشيفرة التالية المثال بالكامل: using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; public async Task<string> UploadFile(string url, string filename, Dictionary<string, object> postData) { var request = WebRequest.CreateHttp(url); var boundary = $"{Guid.NewGuid():N}"; request.ContentType = $"multipart/form-data; {nameof(boundary)}={boundary}"; request.Method = "POST"; using (var requestStream = request.GetRequestStream()) using (var writer = new StreamWriter(requestStream)) { foreach (var data in postData) await writer.WriteAsync( $"\r\n--{boundary}\r\nContent-Disposition: " + $"form-data; name=\"{data.Key}\"\r\n\r\n{data.Value}"); await writer.WriteAsync( // file header $"\r\n--{boundary}\r\nContent-Disposition: " + $"form-data; name=\"File\"; filename=\"{Path.GetFileName(filename)}\"\r\n" + "Content-Type: application/octet-stream\r\n\r\n"); await writer.FlushAsync(); using (var fileStream = File.OpenRead(filename)) await fileStream.CopyToAsync(requestStream); await writer.WriteAsync($"\r\n--{boundary}--\r\n"); } using (var response = (HttpWebResponse) await request.GetResponseAsync()) using (var responseStream = response.GetResponseStream()) { if (responseStream == null) return string.Empty; using (var reader = new StreamReader(responseStream)) return await reader.ReadToEndAsync(); } } إرسال طلب GET باستخدام WebClient.DownloadString يُرسِل التابع DownloadString طلب GET إلى خادم (server) عن طريق مُعامِل الرابط المُمرر إليه، ويُعيد قيمة من النوع string تُمثِل رد الخادم. string requestUri = "http://www.example.com"; string responseData; using (var client = new WebClient()) { responseData = client.DownloadString(requestUri); } إرسال طلب POST باستخدام WebClient.UploadData يُرسِل التابع UploadData طلب POST إلى خادم (server) مع مَتْن الطلب المُمرر له، كالتالي: string requestUri = "http://www.example.com"; string requestBodyString = "Request body string."; string contentType = "text/plain"; string requestMethod = "POST"; byte[] responseBody; byte[] requestBodyBytes = Encoding.UTF8.GetBytes(requestBodyString); using (var client = new WebClient()) { client.Headers[HttpRequestHeader.ContentType] = contentType; responseBody = client.UploadData(requestUri, requestMethod, requestBodyBytes); } عميل SMTP لإرسال بريد إلكتروني يُمكِنك بسهولة إنشاء كائن من النوع MailMessage بحيث يَحمِل معلومات البريد الإلكتروني المَطلوب إرساله، ثم مَرِّره إلى كائن من النوع SmtpClient حتى يقوم بالإرسال الفعلي. يَحتوِي النوع MailMessage على الخاصيات: To From ReplyToList CC Bcc Subject Body IsBodyHtml Attachments Priority والتي يُمكن ضَبْط قيمها كأيّ بريد الكتروني عادي. using(MailMessage MyMail = new MailMessage()) { MyMail.From = new MailAddress(mailfrom); MyMail.To.Add(mailto); MyMail.ReplyToList.Add(replyto); MyMail.CC.Add(mailcc); MyMail.Bcc.Add(mailbcc); MyMail.Subject = subject; MyMail.IsBodyHtml = true; MyMail.Body = body; MyMail.Priority = MailPriority.Normal; // } في المقابل، يَحتوِي النوع SmtpClient على الخاصيات Host و Port و Credentials لتخصيص بيانات خَادِم الـ SMTP المُستخدَم لإرسال البريد الإلكتروني. SmtpClient smtpMailObj = new SmtpClient(); smtpMailObj.Host = "your host"; smtpMailObj.Port = 25; smtpMailObj.Credentials = new System.Net.NetworkCredential("uid", "pwd"); الشيفرة بالكامل: public class clsMail { private static bool SendMail(string mailfrom, List<string>replytos, List<string> mailtos, List<string> mailccs, List<string> mailbccs, string body, string subject, List<string> Attachment) { try { using(MailMessage MyMail = new MailMessage()) { MyMail.From = new MailAddress(mailfrom); foreach (string mailto in mailtos) MyMail.To.Add(mailto); if (replytos != null && replytos.Any()) { foreach (string replyto in replytos) MyMail.ReplyToList.Add(replyto); } if (mailccs != null && mailccs.Any()) { foreach (string mailcc in mailccs) MyMail.CC.Add(mailcc); } if (mailbccs != null && mailbccs.Any()) { foreach (string mailbcc in mailbccs) MyMail.Bcc.Add(mailbcc); } MyMail.Subject = subject; MyMail.IsBodyHtml = true; MyMail.Body = body; MyMail.Priority = MailPriority.Normal; if (Attachment != null && Attachment.Any()) { System.Net.Mail.Attachment attachment; foreach (var item in Attachment) { attachment = new System.Net.Mail.Attachment(item); MyMail.Attachments.Add(attachment); } } SmtpClient smtpMailObj = new SmtpClient(); smtpMailObj.Host = "your host"; smtpMailObj.Port = 25; smtpMailObj.Credentials = new System.Net.NetworkCredential("uid", "pwd"); smtpMailObj.Send(MyMail); return true; } } catch { return false; } } } يُدعم النوع MailMessage إضافة المُرفَقات من خلال الخاصية Attachments كالتالي: using System.Net.Mail; using(MailMessage myMail = new MailMessage()) { Attachment attachment = new Attachment(path); myMail.Attachments.Add(attachment); } عميل UDP لمزامنة التوقيت باستخدام خادم SNTP يُمكِن لعميل إرسال طلبات لخادم SNTP لمُزامنة التوقيت مع ذلك الخادم. اطلع على RFC 2030 للمزيد من المعلومات عن بروتوكول SNTP. تجهيز طلب SNTP كالتالي: var sntpRequest = new byte[48]; sntpRequest[0] = 0x23; //LI=0 (no warning), VN=4, Mode=3 (client) إرسال الطلب من خلال الصنف UDPClient: var udpClient = new UdpClient(); udpClient.Client.ReceiveTimeout = 5000; udpClient.Send( dgram: sntpRequest, bytes: sntpRequest.Length, hostname: args[0], port: SntpPort); مُزامنة التوقيت: var date = BaseDate.AddSeconds(numberOfSeconds).AddHours(localTimeZoneInHours); الشيفرة بالكامل: using System; using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; class SntpClient { const int SntpPort = 123; static DateTime BaseDate = new DateTime(1900, 1, 1); static void Main(string[] args) { if(args.Length == 0) { Console.WriteLine("Simple SNTP client"); Console.WriteLine(); Console.WriteLine("Usage: sntpclient <sntp server url> [<local timezone>]"); Console.WriteLine(); Console.WriteLine("<local timezone>: a number between -12 and 12 as hours from UTC"); Console.WriteLine("(append .5 for an extra half an hour)"); return; } double localTimeZoneInHours = 0; if(args.Length > 1) localTimeZoneInHours = double.Parse(args[1], CultureInfo.InvariantCulture); var udpClient = new UdpClient(); udpClient.Client.ReceiveTimeout = 5000; var sntpRequest = new byte[48]; sntpRequest[0] = 0x23; //LI=0 (no warning), VN=4, Mode=3 (client) udpClient.Send( dgram: sntpRequest, bytes: sntpRequest.Length, hostname: args[0], port: SntpPort); byte[] sntpResponse; try { IPEndPoint remoteEndpoint = null; sntpResponse = udpClient.Receive(ref remoteEndpoint); } catch(SocketException) { Console.WriteLine("*** No response received from the server"); return; } uint numberOfSeconds; if(BitConverter.IsLittleEndian) numberOfSeconds = BitConverter.ToUInt32( sntpResponse.Skip(40).Take(4).Reverse().ToArray(), 0); else numberOfSeconds = BitConverter.ToUInt32(sntpResponse, 40); var date = BaseDate.AddSeconds(numberOfSeconds).AddHours(localTimeZoneInHours); Console.WriteLine( $"Current date in server: {date:yyyy-MM-dd HH:mm:ss} UTC{localTimeZoneInHours:+0.#;-0.#;.}"); } } خادم وعميل TCP لتنشئة برنامج دردشة باستخدام الأنواع TcpListener و TcpClient و NetworkStream. using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; class TcpChat { static void Main(string[] args) { if(args.Length == 0) { Console.WriteLine("Basic TCP chat"); Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine("tcpchat server <port>"); Console.WriteLine("tcpchat client <url> <port>"); return; } try { Run(args); } catch(IOException) { Console.WriteLine("--- Connection lost"); } catch(SocketException ex) { Console.WriteLine("--- Can't connect: " + ex.Message); } } static void Run(string[] args) { TcpClient client; NetworkStream stream; byte[] buffer = new byte[256]; var encoding = Encoding.ASCII; if(args[0].StartsWith("s", StringComparison.InvariantCultureIgnoreCase)) { var port = int.Parse(args[1]); var listener = new TcpListener(IPAddress.Any, port); listener.Start(); Console.WriteLine("--- Waiting for a connection..."); client = listener.AcceptTcpClient(); } else { var hostName = args[1]; var port = int.Parse(args[2]); client = new TcpClient(); client.Connect(hostName, port); } stream = client.GetStream(); Console.WriteLine("--- Connected. Start typing! (exit with Ctrl-C)"); while(true) { if(Console.KeyAvailable) { var lineToSend = Console.ReadLine(); var bytesToSend = encoding.GetBytes(lineToSend + "\r\n"); stream.Write(bytesToSend, 0, bytesToSend.Length); stream.Flush(); } if (stream.DataAvailable) { var receivedBytesCount = stream.Read(buffer, 0, buffer.Length); var receivedString = encoding.GetString(buffer, 0, receivedBytesCount); Console.Write(receivedString); } } } } ترجمة -وبتصرف- للفصول: HTTP servers HTTP clients Upload file and POST data to webserver System.Net.Mail Networking من كتاب .NET Framework Notes for Professionals
-
تُعدّ .NET Core بيئة تطوير مُتَعدِّدة الأغراض، مُصانة بواسطة مايكروسوفت Microsoft ومجتمع الـ .NET على GitHub. تتميز بكْونها مُتَعدِّدة المنصات (cross-platform) حيث تُدعِّم أنظمة التشغيل: ويندوز Windows وماك macOS ولينكس Linux. ويُمكِن اِستخدَامها بسياقات مُتَعدِّدة: على الأجهزة أو على السحاب (cloud) أو على الأجهزة المُدْمَجة (embedded) أو بإنترنت الأشياء (IoT). عندما تُفكِر ببيئة التطوير .NET Core، فينبغي لمُصطلحات مثل سهولة النشر (flexible deployment) وتَعدُّد المنصات (cross-platform) وأدوات سطر الأوامر والمصدر المفتوح (open source) أن تَكون أول ما يطرأ بذهنك. على الرغم من كَوْنها مفتوحة المصدر، تُدعمها مايكروسوفت بشكل فعال. الاختيار ما بين .NET Framework و .NET Core تُعرِّف .NET Standard مُواصَفَات واجهة تطوير التطبيقات (.NET APIs) والتي تُمثِل مجموعة من المواصفات الاصطلاحية (contracts) تُصرَّف شيفرتك وفقًا لها. يَتوفَّر أكثر من مُنفِّذ لهذه المواصفات منها بيئتي العمل .NET Framework و .NET Core. يعني ذلك أنهما يشتركان في العديد من المُكوِنات ولكن توجد أيضًا بعض الاختلافات الجوهرية والتي يُساعدك الإطلاع عليها على الاختيار بينهما. اختيار بيئة عمل .NET Core يُفضَّل استخدام بيئة عمل .NET Core في الحالات التالية: الحاجة لتشغيل تطبيقك على منصات متعددة مثل ويندوز ولينكس وماك. تُدعِّم بيئة عمل .NET Core أيضًا إمكانية التطوير على الأنظمة المذكورة سلفًا. استهداف الخدمات المُصغرة (microservices). على الرغم من إمكانية استخدام بيئة عمل .NET Framework داخل حاويات ويندوز (Windows containers)، تُعدّ .NET Core أكثر مثالية للحاويات (containers) لما تتميز به من صغر الحجم وخفة الوزن. بالإضافة إلى كونها متعددة المنصات مما يُمكِنك من نشر تطبيقك على حاويات Docker بنظام لينكس على سبيل المثال. تُعدّ .NET Core خيارك الأفضل إذا كنت في حاجة إلى بناء أنظمة عالية المستوى قابلة للتوسع (scalable)، . اختيار بيئة عمل .NET Framework على الرغم من أن بيئة عمل .NET Core تُوفِّر الكثير من المميزات خاصة للتطبيقات الجديدة، مع ذلك تَظل بيئة عمل .NET Framework الخيار الطبيعي في كثير من الحالات. مثلًا: إذا كان تطبيقك يعمل بالفعل على بيئة عمل .NET Framework، فأنت عادة لست في حاجة إلى تحويله. على الرغم من توجه المكتبات السريع لتبَنّى مواصفات .NET Standard والذي سيؤدي إلى توافق المكتبة مع جميع المُنفِّذين (implementations)، فقد يَستخدِم تطبيقك مكتبات .NET من طرف ثالث (third-party) أو حزم NuGet غير مُتاحة ببيئة عمل .NET Core. يُمكنك في هذه الحالات استخدام بيئة عمل .NET Framework. اِستخدَام تطبيقك لبعض تقنيات .NET غير المُتوفِّرة ببيئة عمل .NET Core. اِستخدَام تطبيقك لمنصة (platform) غير مُدعَّمة ببيئة عمل .NET Core. التحويل من .NET Framework إلى .NET Core عملية تحويل الشيفرة المُطورة أساسًا لاستهداف بيئة عمل .NET Framework بحيث تُصبح متوافقة مع بيئة عمل .NET Core هي عملية بسيطة نسبيًا بغالبية المشروعات خاصة إذا كان نموذج المشروع (app model) متوفرًا بكلتا البيئتين كالمكتبات (libraries) وتطبيقات الطرفية (Console Applications)، أما المشروعات التي تتطلب إنشاء نموذج مشروع (app model) جديد كالانتقال من ASP.NET إلى ASP.NET Core فإن العملية قد تحتاج إلى مزيد من العمل ولكنها بالنهاية تَتَبع نمطًا ثابتًا. تحويل حل (solution) إذا كنت تعمل على حل (solution) به أكثر من مشروع، فقد تبدو العملية معقدة. لذلك فمن الأفضل أن تُحوِّل المشروعات تحويلًا تصاعديًا من أسفل لأعلى، بمعنى أن تبدأ أولًا بالمشروعات التي لا تَعتمِد على مشروعات اخرى، ثم تنتقل إلى المشروعات التي تَعتمِد على المشروعات المُحوَّلة وهكذا إلى أن تُحوِّل الحل (solution) بالكامل. يُمكِنك اِستخدَام احدى الطرائق التالية لتتعرف على الترتيب الذي ينبغي أن تُحوِّل به المشروعات: تُنشِئ أداة Dependency Diagrams بفيجوال ستوديو رسم بياني مُوجَّه (directed graph) لشيفرة الحل. يُولِد الأمر التالي ملف بصيغة تبادل البيانات (JSON) يتضَمَّن قائمة بمَراجِع (references) المشروعات: msbuild _SolutionPath_ /t:GenerateRestoreGraphFile /p:RestoreGraphOutputPath=graph.dg.json تُعيد أداة .NET Portability Analyzer مُخطط للتبعيات (dependency diagram) عند تفعيل الخاصية -r DGML . خطوات تحويل مشروع (project) يُنصَح باتباع الخطوات التالية عند تحويل مشروع بحيث يُصبِح متوافقًا مع .NET Core: غَيِّر طريقة تَضْمِين التبعيات من صيغة بيئة عمل .NET Framework والموجودة بملف packages.config إلى صيغة PackageReference المُستخدَمة ببيئة عمل .NET Core. لاحظ أن بيئة عمل .NET Core تُضمِن فقط التبعيات المُستخدمة فعليًا بالمشروع بغرض تقليل عدد التبعيات المُضافة. قد تَستخدِم أداة تحويل متوفرة بفيجوال ستوديو للقيام بذلك. حَوِل صيغة ملف المشروع (project file) إلى الصيغة المُستخدَمة ببيئة عمل .NET Core، والتي تُعدّ أبسط من تلك المُستخدَمة ببيئة عمل .NET Framework. تَسمَح الصيغة الجديدة بتخصيص بيئة العمل المُستهدَفة أثناء بناء المشروع مما يعني أنه من الممكن أن تَستمر باستهداف بيئة عمل .NET Framework حتى تنتهي من عملية التحويل. اِستهدف بيئة عمل .NET Framework إصدار 4.7.2 أو أحدث بجميع المشروعات. حَدِث جميع تَبعِيات المشروع إلى آخِر إصدار، فربما لا تُدعِّم الإصدارات القديمة من تلك التبعيات -والتي قد يكون مشروعك مُعتمِدًا عليها- مواصفات واجهة تطوير التطبيقات .NET Standard بينما قد تُدعِّمها الإصدارات الأحدث. قد تَضطرك بعض تلك التحديثات إلى إجراء تعديلات بالشيفرة. اِستخدِم أداة .NET Portability Analyzer لتحليل شيفرات التجميع (assemblies)، ومعرفة ما إذا كانت قابلة للنقل (portable) إلى بيئة عمل .NET Core، حيث تُولِد هذه الأداة تقرير يحتوي على ملخص لجميع واجهات تطوير التطبيقات (APIs) غير المُتوفرة بـ NET Core والمُستخدَمة بالمشروع. يَتوفَّر لغالبية تلك الواجهات نظير ببيئة عمل .NET Core ستحتاج إلى استخدامه كبديل. تُمكنك أداة .NET API analyzer من تحديد ما إذا كنت تَستخدِم أي من واجهات تطوير التطبيقات (APIs) بطريقة تَتسبب بحدوث اعتراض من النوع PlatformNotSupportedException أثناء زمن التشغيل. أخيرًا غَيّر بيئة العمل المستهدفة بملف المشروع من: <TargetFramework>net472</TargetFramework> إلى .NET Core أو .NET Standard: <TargetFramework>netcoreapp3.1</TargetFramework> يَعتمِد الاختيار ما بين .NET Core و .NET Standard على الغرض من المشروع. إذا كان المشروع هو مُجرد مكتبة (library) ستُستخدَم بواسطة تطبيقات اخرى، يُفضَّل عندها استهداف مواصفات واجهة تطوير التطبيقات .NET Standard. في المقابل، ستضطر إلى استهداف بيئة عمل .NET Core إذا كان المشروع يَعتمِد على بعض الواجهات المتوفرة فقط بتلك البيئة، وفي تلك الحالة يُمكِنك أيضًا توفير بناء ثانوي يَستهِدف .NET Standard لكن بخاصيات أقل. تطبيق طرفية تَستعرِض الشيفرة التالية تطبيق طرفية بسيط (Console App) باستخدام بيئة العمل .NET Core: public class Program { public static void Main(string[] args) { Console.WriteLine("\nWhat is your name? "); var name = Console.ReadLine(); var date = DateTime.Now; Console.WriteLine("\nHello, {0}, on {1:d} at {1:t}", name, date); Console.Write("\nPress any key to exit..."); Console.ReadKey(true); } } ترجمة -وبتصرف- للفصل 24 من كتاب .NET Framework Notes for Professionals
-
التشفير (Encryption) تشفير البيانات وفكها باستخدام النوع Aes تَستعرِض الشيفرة التالية مثال طرفية توضيحي من شبكة مطوري مايكروسوفت (MSDN). يَشرَح هذا المثال طريقة تشفير سِلسِلة نصية ثُم فَكّ التشفير باِستخدَام الخوارزمية القياسية "معيار التشفير المُتقدِم Advanced Encryption Standard"، وتُسمَى اختصارًا AES. يُوفِّر إطار عمل .NET النوع Aes، والذي يُنْفِّذ خوارزمية معيار التشفير المُتقدِم AES. تَتكون كُلًا من شيفرتي التشفير وفَكُه مِن عدة خطوات مُشتَركة. نحتاج عامةً لإِنشاء مَجْرى (stream) بيانات ستمُرّ عَبره البيانات المَطلوب تَشفيِرها أو فَكّ تَشفيِرها. using (MemoryStream msEncrypt = new MemoryStream()) كذلك سنحتاج إلى تنشئة عملية تَحوِيل مَجْرى (stream transform) -إِمّا مُشفِر أو مُفكِّك للتشفير- من النوع ICryptoTransform بحيث تُطبَّق على البيانات أثناء مرُّورَها بالمَجْرى. يُستخدَم التابعين CreateEncryptor وCreateDecryptor لإنشاء المُشفِر ومُفكِّك التشفير على الترتيب. ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); أخيرًا، نُنشِئ مَجْرى تَشفير من النوع CryptoStream يُوصِل عملية تَحوِيل المَجْرى (stream transform) بمَجْرى البيانات مع تحديد وَضْع التَوصِيل إِمّا للقراءة أو للكتابة. using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) مثلًا، في حالة التشفير، سنَكتُب البيانات المطلوب تشفيرها على مَجْرى التشفير CryptoStream. يتم تَّنْفيذ عملية تَحوِيل المَجْرى (stream transform) -مُشفِر في هذه الحالة- على البيانات، وتُكتَب النتيجة المُشفَّرة على المَجْرى المُمرَّر لمَجْرى التشفير بوَضْع الكتابة. using (MemoryStream msEncrypt = new MemoryStream()) using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); encrypted = msEncrypt.ToArray(); } في حالة فَكّ التشفير، سيَقرأ مَجْرى التشفير CryptoStream البيانات المطلوب فَكّ تشفيرها من المَجْرى المُمرَّر له بوَضْع القراءة. يتم تَّنْفيذ عملية تَحوِيل المَجْرى (stream transform) -مُفكِّك شَفرة في هذه الحالة- على البيانات. أخيرًا نقرأ البيانات بعد فَكّ التشفير من خلال مَجْرى التشفير. using (MemoryStream msDecrypt = new MemoryStream(cipherText)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (StreamReader srDecrypt = new StreamReader(csDecrypt)) plaintext = srDecrypt.ReadToEnd(); الشيفرة بالكامل: using System; using System.IO; using System.Security.Cryptography; namespace Aes_Example { class AesExample { public static void Main() { try { string original = "Here is some data to encrypt!"; // أنشئ كائن من النوع Aes المُستخدَم لتوليد كلا من المفتاح ومتجه التهيئة using (Aes myAes = Aes.Create()) { // قم بتشفير سلسلة نصية إلى مصفوفة بايتات byte[] encrypted = EncryptStringToBytes_Aes(original, myAes.Key, myAes.IV); // قم بفك تشفير مصفوفة بايتات إلى سلسلة نصية string roundtrip = DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV); Console.WriteLine("Original: {0}", original); Console.WriteLine("Round Trip: {0}", roundtrip); } } catch (Exception e) { Console.WriteLine("Error: {0}", e.Message); } } } } static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV) { // Check arguments. if (plainText == null || plainText.Length <= 0) throw new ArgumentNullException("plainText"); if (Key == null || Key.Length <= 0) throw new ArgumentNullException("Key"); if (IV == null || IV.Length <= 0) throw new ArgumentNullException("IV"); byte[] encrypted; // أنشئ كائن من النوع Aes باستخدام المفتاح ومتجه التهيئة المحددين using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Key; aesAlg.IV = IV; // أنشئ مُشفر والذي سيستخدم كمحول للمجرى ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // انشئ المجاري المستخدمة خلال عملية التشفير using (MemoryStream msEncrypt = new MemoryStream()) using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); encrypted = msEncrypt.ToArray(); } } // أعد مصفوفة البايتات المشفرة المُنشأة من مجرى الذاكرة return encrypted; } static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV) { // تحقق من الوسائط if (cipherText == null || cipherText.Length <= 0) throw new ArgumentNullException("cipherText"); if (Key == null || Key.Length <= 0) throw new ArgumentNullException("Key"); if (IV == null || IV.Length <= 0) throw new ArgumentNullException("IV"); string plaintext = null; // أنشئ كائن من النوع Aes باستخدام المفتاح ومتجه التهيئة المحددين using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Key; aesAlg.IV = IV; // أنشئ مفكك الشفرة الذي سيستخدم كمحول للمجرى ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // انشئ المجاري المستخدمة خلال عملية فك التشفير using (MemoryStream msDecrypt = new MemoryStream(cipherText)) using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (StreamReader srDecrypt = new StreamReader(csDecrypt)) // قم بقراءة البايتات من مجرى فك التشفير وأسْندها إلى متغير من النوع string plaintext = srDecrypt.ReadToEnd(); } return plaintext; } تُحدِّد الخوارزمية AES طريقة لتشفير البيانات الالكترونية. أُسِست عام 2001 بواسطة المعهد الوطني للمعايير والتقنية NIST بالولايات المتحدة الامريكية، ومازالت تُعدّ الخوارزمية القياسية للتشفير التَماثلي (symmetric encryption). ملاحظات: تَتوفَّر عِدة أوضاع تَشفير cipher mode ضِمْن خوارزمية AES. تَستطيع تَحديد وَضْع التشفير بإِسْناد إِحدى قيم التعداد CipherMode إلى الخاصية Mode. لا تَستخدِم أبدًا وَضْع التشفير Electronic codebook - ECB - مما يَعنِي عدم إِختيار CipherMode.ECB-؛ لأنه يُنتِج عملية تَحوِيل مَجْرى ضعيفة. يجب أن تَستخدِم مُولِّد تَشفير عشوائي -أو اِستخدِم الشيفرة بالأسفل (إنشاء مفتاح من كلمة سرّ / سلسلة نصية إضافية (Salt) عشوائية)- لتنشئة مفتاح (Key) جيد غيْر ضعيف. يُفضَّل أيضًا أن يَكُون حجم المفتاح 256 بت. تستطيع تخصيص حَجم المفتاح مِن خلال الخاصية KeySize كما تُوفِّر الخاصية LegalKeySizes قائمة بالأحجام المُدعَّمة. يُمكِنك اِستخدَام سِلسِلة نصية إضافية (salt) -كالمثال بالأسفل (إنشاء مفتاح من كلمة سرّ / سلسلة نصية إضافية (Salt) عشوائية)- لتهيئة مُتّجَه التهيئة (initialization vector - IV). مثال آخر باستخدام خوارزمية AES شيفرة التشفير: public static string Encrypt(string cipherText) { if (cipherText == null) return null; byte[] clearBytes = Encoding.Unicode.GetBytes(cipherText); using (Aes encryptor = Aes.Create()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(CryptKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); encryptor.Key = pdb.GetBytes(32); encryptor.IV = pdb.GetBytes(16); using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(clearBytes, 0, clearBytes.Length); cs.Close(); } cipherText = Convert.ToBase64String(ms.ToArray()); } } return cipherText; } شيفرة فَكّ التشفير: public static string Decrypt(string cipherText) { if (cipherText == null) return null; byte[] cipherBytes = Convert.FromBase64String(cipherText); using (Aes encryptor = Aes.Create()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(CryptKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); encryptor.Key = pdb.GetBytes(32); encryptor.IV = pdb.GetBytes(16); using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(), CryptoStreamMode.Write)) { cs.Write(cipherBytes, 0, cipherBytes.Length); cs.Close(); } cipherText = Encoding.Unicode.GetString(ms.ToArray()); } } return cipherText; } تُستخدَم كالتالي: var textToEncrypt = "TestEncrypt"; var encrypted = Encrypt(textToEncrypt); var decrypted = Decrypt(encrypted); تشفير البيانات وفكها باستخدام النوع RijndaelManaged يَتطلَّب فضاء الاسم System.Security.Cryptography private class Encryption { private const string SecretKey = "topSecretKeyusedforEncryptions"; private const string SecretIv = "secretVectorHere"; public string Encrypt(string data) { return string.IsNullOrEmpty(data) ? data : Convert.ToBase64String( this.EncryptStringToBytesAes(data, this.GetCryptographyKey(), this.GetCryptographyIv()) ); } public string Decrypt(string data) { return string.IsNullOrEmpty(data) ? data : this.DecryptStringFromBytesAes(Convert.FromBase64String(data), this.GetCryptographyKey(), this.GetCryptographyIv()); } private byte[] GetCryptographyKey() { return Encoding.ASCII.GetBytes(SecretKey.Replace('e', '!')); } private byte[] GetCryptographyIv() { return Encoding.ASCII.GetBytes(SecretIv.Replace('r', '!')); } } private byte[] EncryptStringToBytesAes(string plainText, byte[] key, byte[] iv) { MemoryStream encrypt; RijndaelManaged aesAlg = null; try { aesAlg = new RijndaelManaged { Key = key, IV = iv }; var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); encrypt = new MemoryStream(); using (var csEncrypt = new CryptoStream(encrypt, encryptor, CryptoStreamMode.Write)) using (var swEncrypt = new StreamWriter(csEncrypt)) swEncrypt.Write(plainText); } finally { aesAlg?.Clear(); } return encrypt.ToArray(); } private string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv) { RijndaelManaged aesAlg = null; string plaintext; try { aesAlg = new RijndaelManaged { Key = key, IV = iv }; var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); using (var msDecrypt = new MemoryStream(cipherText)) using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) using (var srDecrypt = new StreamReader(csDecrypt)) plaintext = srDecrypt.ReadToEnd(); } finally { aesAlg?.Clear(); } return plaintext; } يُستخدَم كالتالي: var textToEncrypt = "hello World"; //-> zBmW+FUxOvdbpOGm9Ss/vQ== var encrypted = new Encryption().Encrypt(textToEncrypt); //-> hello World var decrypted = new Encryption().Decrypt(encrypted); تنبيه: يُنفِّذ النوع Rijndael النسخة الأقدم من خوارزمية AES، ولذلك ينبغي استعمال النوع Aes الذي يُنفِّذ النسخة الحديثة. إنشاء مفتاح من كلمة سر / سلسلة نصية إضافية (Salt) عشوائية تَستعرِض الشيفرة التالية مثال طرفية توضيحي من شبكة مطوري مايكروسوفت (MSDN). يَشرح هذا المثال كيفية تنشئة مفتاح (key) آمِن اعتمادًا على كلمة سرّ مُحدَّدة من قِبَل المُستخدِم، بالإضافة إلى طريقة تنشئة سِلسِلة نصية إضافية (salt أو يدعى غفل باللغة العربية، انظر كتاب «علم التعمية واستخراج المعمى عند العرب») عشوائية باِستخدَام مُولِّد تشفير عشوائي. using System; using System.Security.Cryptography; using System.Text; public class PasswordDerivedBytesExample { public static void Main(String[] args) { // اجلب كلمة السر من المستخدم Console.WriteLine("Enter a password to produce a key:"); byte[] pwd = Encoding.Unicode.GetBytes(Console.ReadLine()); byte[] salt = CreateRandomSalt(7); // TripleDESCryptoServiceProvider أنشئ كائنًا من النوع TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider(); try { Console.WriteLine("Creating a key with PasswordDeriveBytes..."); // أنشئ كائنًا من النوع PasswordDeriveBytes ثم أنشئ مفتاح لخوارزمية TripleDES // من كلمة سر وسلسلة نصية إضافية PasswordDeriveBytes pdb = new PasswordDeriveBytes(pwd, salt); // أنشئ المفتاح وأسنده إلى الخاصية Key الموجودة بكائن موفر خدمة التشفير tdes.Key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, tdes.IV); Console.WriteLine("Operation complete."); } catch (Exception e) { Console.WriteLine(e.Message); } finally { ClearBytes(pwd); ClearBytes(salt); tdes.Clear(); } Console.ReadLine(); } } /// توليد غفل (سلسلة نصية إضافية) بالطول المحدد public static byte[] CreateRandomSalt(int length) { byte[] randBytes; if (length >= 1) randBytes = new byte[length]; else randBytes = new byte[1]; // RNGCryptoServiceProvider إنشاء كائن من النوع RNGCryptoServiceProvider rand = new RNGCryptoServiceProvider(); // املأ المخزن بالبايتات العشوائية rand.GetBytes(randBytes); return randBytes; } /// امسح البايتات من المخزن لكي لا تُقرَأ مستقبلًا من الذاكرة public static void ClearBytes(byte[] buffer) { if (buffer == null) throw new ArgumentNullException("buffer"); // اضبط قيمة كل بايت إلى القيمة 0 for (int x = 0; x < buffer.Length; x++) { buffer[x] = 0; } } ملاحظات: تَستقبِل الدالة PasswordDeriveBytes المَبنية مُسبَقًا (built-in) كلمة سرّ، وتَستخدِم خوارزمية PBKDF1 القياسية لتولِّيد المفتاح. تَقوم هذه الدالة بشكل افتراضي بمائة تكرار أثناء تولِّيد المفتاح؛ وذلك لتُبطئ من فاعلية هَجمَات القوى الغاشمة (brute force attacks). بالإضافة إلى ذلك، يُعزز اِستخدَام السِلسِلة النصية الإضافية (salt) المُولَّدة عشوائيًا من قوة المفتاح. تَستخدِم الدالة CryptDeriveKey خوارزمية التَقطيع (hashing) المُمرَّرة إليها -تم اختيار SHA1 بالمثال- لتَحوِيل المفتاح المُولَّد من الدالة PasswordDeriveBytes إلى مفتاح مُتوافِق مع خوارزمية التشفير المُمرَّرة إليها -تم اختيار TripleDES بالمثال-، يمكنك أيضًا تخصيص كلًا من حجم المفتاح والقيمة المبدئية لمُتٍّجَه التهيئة (initialization vector - IV) بتمريرهما كمُعامِلين للدالة. في المثال بالأعلى، مُرِّرت القيمة 192 بايت كما اُستخدِم مُوفِّر خدمة التشفير من النوع TripleDESCryptoServiceProvider لتهيئة مُتّجَه التهيئة. عندما تحتاج إلى مفتاح قوي مُولَّد عشوائيًا لاِستخدَامُه لتشفير كمية ضخمة من البيانات، اِستخدِم هذه الطريقة لتولِّيده مِن مجرد كلمة سرّ. يُمكِن أيضًا اِستخدَام هذه الطريقة لتوليد كلمات سر لعِدّة مُستخدِمين للولوج إلى نفس البيانات. للأسف، لا تُدعِّم الدالة CryptDeriveKey خوارزمية AES حاليًا (تَحقَّق هنا). يُمكِن التحايل على ذلك بالاعتماد على حَاوِي خوارزمية TripleDES، مع أنه سيُؤدِي إلى تَبَعيّة لهذه الخوارزمية وسيُقصِرك على مُستَوَى حمايتها، فمثلًا لن تستطيع تَوليد مفاتيح بأحجام أكبر من تلك المُدعَّمة بخوارزمية TripleDES على الرغم من دَعََّم تلك الأحجام بخوارزمية AES. دوال التعمية (Hashing) توليد تدقيق المجموع (checksum) لملف باستخدام خوارزمية SHA1 تَتطلَّب فضاء الاسم System.Security.Cryptography public string GetSha1Hash(string filePath) { using (FileStream fs = File.OpenRead(filePath)) { SHA1 sha = new SHA1Managed(); return BitConverter.ToString(sha.ComputeHash(fs)); } } توليد القيمة المعماة (hash أو المقطعة) لسِلسِلة نصية public static string TextToHash(string text) { var sh = SHA1.Create(); var hash = new StringBuilder(); byte[] bytes = Encoding.UTF8.GetBytes(text); byte[] b = sh.ComputeHash(bytes); foreach (byte a in b) { var h = a.ToString("x2"); hash.Append(h); } return hash.ToString(); } ملحوظة: الشيفرة بالكامل موجودة بـمستودع mahdiabasi/SHA1Tool في GitHub. ترجمة -وبتصرف- للفصلين Encryption / Cryptography و Work with SHA1 in C# من كتاب .NET Framework Notes for Professionals
-
السلسلة إلى JSON استخدام Json.NET Newtonsoft.Json هي حزمة قوية، وسريعة، وسهلة الاستخدام مما جعلها الأداة الأكثر شيوعًا عند التعامل مع "السَلاسِل النصية بصيغة تبادل البيانات (JSON)" بإطار عمل .NET. يُعدّ اِستخدامها سهلًا نوعًا ما؛ لكونها تعتمد على التوابع الساكنة (static) سواء للسَلسَلة (serialize)، أو لإلغاء السَلسَلة (de-serialize). بالتحديد يُسَلسِل التابع JsonConvert.SerializeObject الكائن المُمرَّر له ويُعيد سِلسِلة نصية، بينما يُلغي التابع JsonConvert.DeserializeObject<T> سَلسَلة المُعامِل المُمرَّر له ويُحاول تحليله إلى نوع يُحدَّد من خلال مُعامِل النوع (type parameter) كالمثالين التاليين: using Newtonsoft.Json; var rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; var fibo = JsonConvert.DeserializeObject<Dictionary<string, object>>(rawJSON); var rawJSON2 = JsonConvert.SerializeObject(fibo); internal class Sequence{ public string Name; public List<int> Numbers; } string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; Sequence sequence = JsonConvert.DeserializeObject<Sequence>(rawJSON); اطلع على مزيد من المعلومات عن JSON.NET من خلال موقعها الرسمي. يُدعِّم اطار عمل .NET النوع JSON.NET منذ إصدار 2. استخدام السمات (attributes) مع Json.NET تتوفَّر بعض السمات (attributes) لزخرفة (decoration) كلًا من الأصناف وخاصياتها؛ مما يُمكنك من التحكم بشكل "السِلسِلة النصية بصيغة JSON" النهائية الناتجة عن السَلسَلة. مثلًا، تُخصِص السمة JsonProperty اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي، في حين تُهمَل الخاصيات المزخرفة بالسمة JsonIgnore. في المثال التالي: لا يحتوي ناتج السَلسَلة {"name":"Andrius","age":99} على الخاصية Address؛ لأنها مُزخرفة بالسمة JsonIgnore، كما اُستُخدِمت الأسماء name و age بدلًا من الأسماء الأصلية للخاصيات لأنها زُخرِفت بالسمة JsonProperty. [JsonObject("person")] public class Person { [JsonProperty("name")] public string PersonName { get; set; } [JsonProperty("age")] public int PersonAge { get; set; } [JsonIgnore] public string Address { get; set; } } Person person = new Person { PersonName = "Andrius", PersonAge = 99, Address = "Some address" }; string rawJson = JsonConvert.SerializeObject(person); Console.WriteLine(rawJson); // {"name":"Andrius","age":99} اطلع على مزيد من المعلومات عن سمات السَلسَلة التي يُمكنك استخدامها. استخدام المُعامِل JsonSerializerSettings مع Json.NET تتوفَّر بصمة أُخْرَى من التابع JsonConvert.SerializeObject والتي تَستقبِل -بالإضافة إلى الكائن المراد سَلسَلته- مُعامِلًا آخَر من النوع JsonSerializerSettings للتحكم بعملية السَلسَلة. يحتوي النوع JsonSerializerSettings على العديد من الخاصيات المُصممة خصيصًا لحل بعض أكثر المشاكل شيوعًا. مثلًا، الخاصية ContractResolver من نوع الواجهة IContractResolver، والتي يتوفَّر لها أكثر من مُنفِّذ (implementation). إحداها هو النوع DefaultContractResolver المُستخدَم افتراضيًا، بالإضافة إلى النوع CamelCasePropertyNamesContractResolver الذي قد يُفيدك عند التحويل من كائنات c# إلى صيغة JSON والعكس؛ نظرًا لشيوع استخدَام النمط PascalCase لدى مبرمجي c# بينما في الغالب ما تكون صيغة تبادل البيانات JSON بنمط سنام الجمل CamelCase. using Newtonsoft.Json; using Newtonsoft.Json.Serialization; public class Person { public string Name { get; set; } public int Age { get; set; } [JsonIgnore] public string Address { get; set; } } public void ToJson() { Person person = new Person { Name = "Andrius", Age = 99, Address = "Some address" }; var resolver = new CamelCasePropertyNamesContractResolver(); var settings = new JsonSerializerSettings { ContractResolver = resolver }; string json = JsonConvert.SerializeObject(person, settings); Console.WriteLine(json); // {"name":"Andrius","age":99} } مثال آخر هو الخاصية NullValueHandling المُستخدَمة لضَبط طريقة التعامُل مع القيم الفارغة null، كالمثال التالي: public static string Serialize(T obj) { string result = JsonConvert.SerializeObject(obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore}); return result; } مثال أخير هو الخاصية ReferenceLoopHandling التي تُستخدَم لضَبط طريقة التعامل مع تكرار الإشارة الذاتية (self referencing loop). مثلًا، إذا أردت تَمثيل برنامج دراسي مُلحق به عدد من الطلبة، سيكون لديك النوع Student يحوي خاصية من النوع Course والذي بدوره يحمل خاصية Students من النوع List<Student>. public static string Serialize(T obj) { string result = JsonConvert.SerializeObject( obj, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore}); return result; } يُمكنك تخصيص أكثر من خاصية سَلسَلة كالتالي: public static string Serialize(T obj) { string result = JsonConvert.SerializeObject( obj, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); return result; } الربط الديناميكي (Dynamic Binding) تُوفِّر Json.NET خاصية الربط الديناميكي، مما يَسمَح بإلغاء سَلسَلة "سِلسِلة نصية بصيغة JSON" وتحويلها إلى نوع ديناميكي لم يُعلَّن عن خاصياته صراحةً. يتم ذلك إِما باستخدام النوع DynamicObject أو النوع ExpandoObject. السَلسَلة: dynamic jsonObject = new ExpandoObject(); jsonObject.Title = "Merchent of Venice"; jsonObject.Author = "William Shakespeare"; Console.WriteLine(JsonConvert.SerializeObject(jsonObject)); في المثال التالي، حُُوِّلَت المفاتيح (keys) الموجودة "بالسِلسِلة النصية بصيغة JSON" إلى مُتَغيّرات أعضاء (member variables) بالنوع الديناميكي: var rawJson = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; dynamic parsedJson = JObject.Parse(rawJson); Console.WriteLine("Name: " + parsedJson.Name); Console.WriteLine("Name: " + parsedJson.Numbers.Length); قد يكون الربط الديناميكي مفيدًا خاصةً إذا كان البرنامج يَستقبِل ويُنتج العديد من "السَلاسِل النصية بصيغة JSON"، ولكن من الأفضل التحقق (validate) من تلك السَلاسِل النصية الناتجة عن السَلسَلة وكذلك من الكائنات الديناميكية الناتجة عن إلغائها. استخدام JavascriptSerializer يتوفَّر أيضًا النوع JavascriptSerializer -الموجود بفضاء الاسم System.Web.Script.Serialization- والذي يُعرِّف التابع Deserialize<T>(input). يُلِّغي هذا التابع سَلسَلة "سِلسِلة نصية بصيغة تبادل البيانات (JSON)"، ويُحَوِّلها لكائن نوعه مُحدَّد بمُعامِل النوع (type parameter). using System.Collections; using System.Web.Script.Serialization; string rawJSON = "{\"Name\":\"Fibonacci Sequence\",\"Numbers\":[0, 1, 1, 2, 3, 5, 8, 13]}"; JavaScriptSerializer JSS = new JavaScriptSerializer(); Dictionary<string, object> parsedObj = JSS.Deserialize<Dictionary<string, object>>(rawJSON); string name = parsedObj["Name"].toString(); ArrayList numbers = (ArrayList)parsedObj["Numbers"] يُدعِّم اطار عمل .NET النوع JavaScriptSerializer منذ إصدار 3.5. السلسلة إلى XML استخدام XmlSerializer يُستخدَم النوع XmlSerializer -الموجود بفضاء الاسم System.Xml.Serialization - لسَلسَلة كائن إلى ملف (document) نصي بصيغة لغة الترميز القابلة للامتداد Extensible Markup Language - XML. public void SerializeFoo(string fileName, Foo foo) { var serializer = new XmlSerializer(typeof(Foo)); using (var stream = File.Open(fileName, FileMode.Create)) { serializer.Serialize(stream, foo); } } يُمكن إلغاء السَلسَلة كالتالي: public Foo DeserializeFoo(string fileName) { var serializer = new XmlSerializer(typeof(Foo)); using (var stream = File.OpenRead(fileName)) { return (Foo)serializer.Deserialize(stream); } } استخدام السمات مع XmlSerializer تتوفَّر بعض السمات (attributes) لزخرفة (decoration) خاصيات الأنواع؛ مما يُمكنك من التحكم بالصورة النهائية لملف XML الناتج عن السَلسَلة. مثلًا، تُخصِص السمة XmlArray اسمًا للخاصيات من النوع Array -أو أي نوع قد يُعيد Array- بدلًا من اسمها الأصلي. في المثال التالي، اُستخدِم الاسم Articles بدلًا من الاسم Products نظرًا لأن الخاصية المُناظِرة زُخرِفت بالسمة XmlArray. public class Store { [XmlArray("Articles")] public List<Product> Products {get; set; } } <Store> <Articles> <Product/> <Product/> </Articles> </Store> في المثال التالي، تُخصِص السمة XmlElement اسمًا للخاصية المُزخَرفة بدلًا من اسمها الأصلي. public class Foo { [XmlElement(ElementName="Dog")] public Animal Cat { get; set; } } <Foo> <Dog/> </Foo> في المثال التالي، اُستخدِم جَالْب الخاصية لتحديد صيغة مُخصَّصة للقيمة العائدة بدلًا من الافتراضية. public class Dog { private const string _birthStringFormat = "yyyy-MM-dd"; [XmlIgnore] public DateTime Birth {get; set;} [XmlElement(ElementName="Birth")] public string BirthString { get { return Birth.ToString(_birthStringFormat); } set { Birth = DateTime.ParseExact(value, _birthStringFormat, CultureInfo.InvariantCulture); } } } أخيرًا، تُهمَل الخاصيات المُزخَرفة بالسمة XmlIgnore. سلسلة أصناف فرعية بشكل ديناميكي المشكلة: أحيانًا قد لا نستطيع استخدَام السِمات (attributes) لإمداد اطار عمل XmlSerializer بكل البيانات الوصفية (metadata) التي يحتاجها لإجراء السَلسَلة. مثلًا، بفرض أن لدينا صنف أساسي (base class) مطلوب سَلسَلة كائناته. على الرغم من تَوفّر بعض السمات التي قد تُستخدَم للإشارة إلى أنواعه الفرعية، أحيانًا، قد لا تكون جميع الأصناف الفرعية مَعلومَة أثناء تصميم الصنف الأساسي؛ فمن المُحتمل وجود فريق آخر يَعمَل على تطوير بَعضًا من تلك الأنواع الفرعية. كما أنها حتى وإن تَوفَّرت فمِن المُفترض ألا يعلم الصنف الأساسي شيئًا عن أصنافه الفرعية. حَل مُقْترَح: نحتاج لإمداد المُسَلسِلات (serializers) بالأنواع المَعلومَة بطريقة أُخْرى، مثلًا، باستخدَام بَصمَة أُخْرى من باني الكائنات XmlSerializer(type, knownTypes). تَستقبِل هذه البَصمة مصفوفة تحتوي على الأنواع المَعلومَة كمُعامِل ثان. قد تَفي هذه الطريقة بالغرض ولكنها بتعقيد زمني O(N^2) على الأقل -إذا كان لدينا عدد N من المُسَلسِلات-، فقط لاكتشاف جميع الأنواع المُمرَّرة للمُعامِل. var allSerializers = allTypes.Select(t => new XmlSerializer(t, allTypes)); var serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); لاحظ أنه في المثال بالأعلى، لا علم للصنف الأساسي بالأصناف المشتقة منه، وهو ما يُعدّ أمرًا عاديًا -بل ومطلوبًا- بالبرمجة كائنية التوجه OOP. حل أكثر كفاءة: هناك لحسن الحظ طريقة تَعني بحل هذه المشكلة حلًا أكثر كفاءة عن طريق تَوفير مصفوفة بالأصناف المَعلومَة لعِدة مُسَلسِلات (serializers). تَستخدِم هذه الطريقة التابع FromTypes(Type[])، والذي يَسمَح بإنشاء مصفوفة من المُسَلسِلات من النوع XmlSerializer لمُعالجة مصفوفة من الأنواع (Type objects) بكفاءة. var allSerializers = XmlSerializer.FromTypes(allTypes); var serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); بفرض وجود الأصناف التالية: public class Container { public Base Base { get; set; } } public class Base { public int JustSomePropInBase { get; set; } } public class Derived : Base { public int JustSomePropInDerived { get; set; } } نُنشئ كائن من النوع Container: var sampleObject = new Container { Base = new Derived() }; نحاول أولًا سَلسَلته دون إمداد المُسَلسِل (serializer) بمعلومات عن النوع Derived: var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray()); Serialize(sampleObject); سيُنتَج عن ذلك رسالة خطأ. نحاول مُجددًا سَلسَلته مع إمداد المُسَلسِل بجميع الأنواع: var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; SetupSerializers(allTypes); Serialize(sampleObject); ستتم العملية بنجاح. الشيفرة بالكامل: using System; using System.Collections.Generic; using System.Xml.Serialization; using System.Linq; using System.Linq; public class Program { public class Container { public Base Base { get; set; } } public class Base { public int JustSomePropInBase { get; set; } } public class Derived : Base { public int JustSomePropInDerived { get; set; } } public void Main() { var sampleObject = new Container { Base = new Derived() }; var allTypes = new[] { typeof(Container), typeof(Base), typeof(Derived) }; Console.WriteLine("Trying to serialize without a derived class metadata:"); SetupSerializers(allTypes.Except(new[] { typeof(Derived) }).ToArray()); try { Serialize(sampleObject); } catch (InvalidOperationException e) { Console.WriteLine(); Console.WriteLine("This error was anticipated,"); Console.WriteLine("we have not supplied a derived class."); Console.WriteLine(e); } Console.WriteLine("Now trying to serialize with all of the type information:"); SetupSerializers(allTypes); Serialize(sampleObject); Console.WriteLine(); Console.WriteLine("Slides down well this time!"); } static void Serialize<T>(T o) { serializerDictionary[typeof(T)].Serialize(Console.Out, o); } private static Dictionary<Type, XmlSerializer> serializerDictionary; static void SetupSerializers(Type[] allTypes) { var allSerializers = XmlSerializer.FromTypes(allTypes); serializerDictionary = Enumerable.Range(0, allTypes.Length) .ToDictionary(i => allTypes[i], i => allSerializers[i]); } } الخْرج: Trying to serialize without a derived class metadata: <?xml version="1.0" encoding="utf-16"?> <Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" This error was anticipated, we have not supplied a derived class. System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type Program+Derived was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically. at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write2_Base(String n, String ns, Base o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write3_Container(String n, String ns, Container o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.Write4_Container(Object o) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id) --- End of inner exception stack trace --- at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces) at Program.Serialize[T](T o) at Program.Main() Now trying to serialize with all of the type information: <?xml version="1.0" encoding="utf-16"?> <Container xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Base xsi:type="Derived"> <JustSomePropInBase>0</JustSomePropInBase> <JustSomePropInDerived>0</JustSomePropInDerived> </Base> </Container> Slides down well this time! تقترح رسالة الخطأ الآتي: "اِستخدِم السمة XmlInclude أو السمة SoapInclude لتخصيص الأنواع غير المَعلومَة بشكل ثابت (statically)" في الواقع، لا تستطيع دائمًا القيام بذلك -كما أشرنا من قبل-، وحتى إن كان ذلك مُمكنا، لا يُنصح بالقيام بذلك؛ لأن ليس من المُفترَض أن يعلم الصنف الأساسي شيئًا عن الأصناف المُشتقة منه فضلًا عن الإشارة إليها. هذه هي الطريقة التي يَظهر بها النوع المُشتق بملف XML: <Base xsi:type="Derived"> في المثال بالأعلى، تُشير كلمة Base إلى النوع المُصرَّح عنه للخاصية داخل الصنف Container، بينما كلمة Derived فهي تُشير إلى نوع النسخة المُسْنَدة للخاصية. (إليك مثال حي) ترجمة -وبتصرف- للفصل Serialization من كتاب .NET Framework Notes for Professionals
-
التجميعات (Assemblies) تُصرَّف (compile) الأصناف التي تُعرِّفها، مَصحُوبة بتوابعها وخواصها وملفات البايتكود (bytecode) الخاصة بها، وتُحزَّم بداخل تجميعة (Assembly) تكون في صورة ملف يتضمن شيفرة مُصرَّفة جزئيًا بامتداد .dll او .exe. هذه التجميعات (Assemblies) هي المُكوِّن الأساسي لأي برنامج يتم تشغيله من خلال بيئة التنفيذ المشتركة (CLR). تُعدّ التجميعات ذاتية التَوْثيق، فهي لا تَحتوِي على اﻷنواع وتوابعها وملفات اللغة الوسيطة (IL code) الخاصة بها فقط، بل أيضًا تَضُمّ البيانات الوَصفيّة (metadata) الضرورية للفَحْص والاستهلاك خلال زمني التَصرِّيف (compile time) والتشغيل (runtime). تَملُك كل تجميعة (Assembly) اسم يَصِف هويتها المتفردة توصيفًا كاملًا: // Will print: "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" تُعدّ أسماء التجميعات التي تحتوي على PublicKeyToken أسماء صارمة (strong). لِمَنح تجميعة اسمًا صارمًا (strong-naming)، لابّد أن يكون لديك زوجًا من المفاتيح، أحدهما عام (public key) والآخر سري (private key). يُوزَّع المفتاح العام مع التجميعة أما المفتاح السري فيُستخدَم لانشاء بصمة (signature) تُضاف لبيان التجميعة (Assembly manifest)، والذي يحتوي على أسماء جميع ملفات التجميعة وقيمها المقطّعة (hashes)، كما تُصبِح قيمة PublicKeyToken جزءًا من اسمها. التجميعات التي تَملُك نفس الاسم الصارم هي بالضرورة مُتطابقة، ومِنْ ثَمَّ يُمكِن الاعتماد على ذلك لتَجنُب تضارب أسماء التجميعات (assembly conflicts) وكذلك للإصدارة (versioning). تنشئة تجميعة (assembly) ديناميكيًا يُوفِّر إطار عمل .NET عددًا من الأصناف والتوابع بفضاء الاسم System.Reflection.Emit، والتي يُمكِن اِستخدَامِها لإنشاء تجميعة (assembly) بشكل ديناميكي. عامةً، تَضمّ أي تجميعة (assembly) وَحدة (module) واحدة أو أكثر، كلًا منها قد يتَضمَن صنف واحد أو أكثر. مثلًا، يَتوفَّر التابع ModuleBuilder.DefineType الذي يُمكِن اِستخدَامه لإضافة نوع جديد، ويُعيد قيمة من النوع TypeBuilder. يُوفِّر هذا النوع بدوره العديد من التوابع لإضافة أعضاء (members) بالنوع المُنشَئ. فمثلًا، يُستخدَم التابع TypeBuilder.DefineField لإضافة حَقْل، بينما يُستخدَم التابع TypeBuilder.DefineProperty لإضافة خاصية. يَتوفَّر أيضًا التابعين TypeBuilder.DefineMethod و TypeBuilder.DefineConstructor لإضافة التوابع وبواني الكائن على الترتيب. في المثال التالي، نَستعرِض طريقة تَنشئة تجميعة تَضُمّ وَحدة (module) وَاحدة تَشتمِل على تَعرِيف لنوع واحد يَحمِل الاسم MyDynamicType. يتكون هذا النوع من: حَقْل وحيد يُسمَى m_number من النوع العددي int. خاصية مُناظِرة لهذا الحقل تَحمِل الاسم Number لها ضَابِط (setter) وجَالِب (getter). بانيين للكائن (constructor) أحدهما بدون مُعامِلات والآخر يَستقبِل مُعامِل وحيد لتهيئة قيمة الحَقْل المبدئية. التابع MyMethod والذي يَستقبِل مُعامِل من النوع العددي int، ويُعيد حاصل ضرب قيمة المُعامِل في قيمة الحَقْل m_number. وبالتالي، يَكُون النوع المُراد إنشائه كالتالي: public class MyDynamicType { private int m_number; public MyDynamicType() : this(42) {} public MyDynamicType(int initNumber) { m_number = initNumber; } public int Number { get { return m_number; } set { m_number = value; } } public int MyMethod(int multiplier) { return m_number * multiplier; } } سنحتاج أولًا لاستدعاء التابعين DefineDynamicAssembly و DefineDynamicModule لتنشئة كائنين من النوع AssemblyBuilder و ModuleBuilder على الترتيب، يُمثل هذين التابعين كُلًا من التجميعة (assembly) والوَحدة (module) المُراد إنشائها، كالتالي: AssemblyName aName = new AssemblyName("DynamicAssemblyExample"); AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( aName, AssemblyBuilderAccess.RunAndSave ); // عادةً ما يكون اسم الوحدة هو نفسه اسم ملف التجميع عند تنشئة ملف تجميع من وحدة واحدة ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll"); للإعلان عن النوع MyDynamicType داخل الوَحدة المُنشَأة، نَستخدِم الشيفرة التالية: TypeBuilder tb = mb.DefineType("MyDynamicType", TypeAttributes.Public); لإضافة الحَقْل m_number بالنوع الجديد، نَستخدِم الشيفرة التالية: FieldBuilder fbNumber = tb.DefineField( "m_number", typeof(int), FieldAttributes.Private); لإضافة الخاصية Number المُناظِرة للحَقْل، نَستخدِم الشيفرة التالية: PropertyBuilder pbNumber = tb.DefineProperty( "Number", // اسم الخاصية PropertyAttributes.None, typeof(int), // نوع الخاصية new Type[0]); لإضافة ضَابِط (setter) للخاصية المُنشَئة للتو، نَستخدِم الشيفرة التالية: MethodBuilder mbSetNumber = tb.DefineMethod( "set_Number", // لعدم السماح باستدعاء الضابط لأنه تابع من نوع خاص MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(void), // لا يُعيد الضابط قيمة // يستقبل الضابط قيمة من النوع العددي new[] { typeof(int) }); // سنستخدم مولد الشيفرة الوسيطة IL generator للحصول على متن التابع il = mbSetNumber.GetILGenerator(); // لابد من تحميل this لأنه المُعامِل الأول لجميع التوابع il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); // حمل المعامل الثاني الذي يمثل القيمة المراد إسنادها للحقل il.Emit(OpCodes.Stfld, fbNumber); // خزن القيمة الجديدة المحملة للتو بالحقل il.Emit(OpCodes.Ret); // عُد // وأخيرًا، اربط التابع بضابط الخاصية pbNumber.SetSetMethod(mbSetNumber); عادةً ما يَكُون اسم الضَابِط هو set_Property. بالمثل، لإضافة جَالِب (getter) لنفس الخاصية، نَستخدِم الشيفرة التالية: MethodBuilder mbGetNumber = tb.DefineMethod( "get_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(int), new Type[0]); il = mbGetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); // حمل قيمة الحقل il.Emit(OpCodes.Ret); // أعد القيمة المحملة // وأخيرًا، اربط التابع بجالب الخاصية pbNumber.SetGetMethod(mbGetNumber); لإضافة بواني الكائن بالنوع الجديد، نَضيِف الشيفرة التالية: ConstructorBuilder intConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(int) }); il = intConstructor.GetILGenerator(); // حمل this il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // اِستدعي باني الأب // حمل this il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); // حمل قيمة المعامل الثاني الذي يمثل القيمة الممررة لباني الكائن il.Emit(OpCodes.Stfld, fbNumber); // خزن القيمة المحملة بالحقل il.Emit(OpCodes.Ret); var parameterlessConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new Type[0]); il = parameterlessConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4_S, (byte)42); // حمل القيمة 42 // استدعي this(42) il.Emit(OpCodes.Call, intConstructor); il.Emit(OpCodes.Ret); لاحِظ أنه لابُدّ للبواني من استدعاء باني الصنف الأساسي أو بَانِي آخر بنفس الصنف. لإضافة التابع MyMethod، نَستخدِم الشيفرة التالية: MethodBuilder mbMyMethod = tb.DefineMethod( "MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int) }); ILGenerator il = mbMyMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); // حمل قيمة الحقل il.Emit(OpCodes.Ldarg_1); // حمل قيمة المعامل الممرر il.Emit(OpCodes.Mul); // احسب حاصل ضرب قيمة الحقل بقيمة المعامل il.Emit(OpCodes.Ret); // عُد وأخيرًا نقوم بالتَنشئِة الفِعلّية للنوع الجديد عن طريق التابع CreateType: Type ourType = tb.CreateType(); الشيفرة بالكامل: using System; using System.Reflection; using System.Reflection.Emit; class DemoAssemblyBuilder { public static void Main() { AssemblyName aName = new AssemblyName("DynamicAssemblyExample"); AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( aName, AssemblyBuilderAccess.RunAndSave ); ModuleBuilder mb = ab.DefineDynamicModule(aName.Name, aName.Name + ".dll"); TypeBuilder tb = mb.DefineType( "MyDynamicType", TypeAttributes.Public); FieldBuilder fbNumber = tb.DefineField( "m_number", typeof(int), FieldAttributes.Private); MethodBuilder mbMyMethod = tb.DefineMethod( "MyMethod", MethodAttributes.Public, typeof(int), new[] { typeof(int) }); ILGenerator il = mbMyMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Mul); il.Emit(OpCodes.Ret); PropertyBuilder pbNumber = tb.DefineProperty( "Number", PropertyAttributes.None, typeof(int), new Type[0]); MethodBuilder mbSetNumber = tb.DefineMethod( "set_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(void), new[] { typeof(int) }); il = mbSetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stfld, fbNumber); il.Emit(OpCodes.Ret); pbNumber.SetSetMethod(mbSetNumber); MethodBuilder mbGetNumber = tb.DefineMethod( "get_Number", MethodAttributes.PrivateScope | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName, typeof(int), new Type[0]); il = mbGetNumber.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, fbNumber); il.Emit(OpCodes.Ret); pbNumber.SetGetMethod(mbGetNumber); ConstructorBuilder intConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(int) }); il = intConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stfld, fbNumber); il.Emit(OpCodes.Ret); var parameterlessConstructor = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new Type[0]); il = parameterlessConstructor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4_S, (byte)42); il.Emit(OpCodes.Call, intConstructor); il.Emit(OpCodes.Ret); Type ourType = tb.CreateType(); object ourInstance = Activator.CreateInstance(ourType); Console.WriteLine(ourType.GetProperty("Number") .GetValue(ourInstance)); ab.Save(@"DynamicAssemblyExample.dll"); var myDynamicType = tb.CreateType(); var myDynamicTypeInstance = Activator.CreateInstance(myDynamicType); Console.WriteLine(myDynamicTypeInstance.GetType()); var numberField = myDynamicType.GetField("m_number", BindingFlags.NonPublic | BindingFlags.Instance); numberField.SetValue (myDynamicTypeInstance, 10); Console.WriteLine(numberField.GetValue(myDynamicTypeInstance)); } } الانعكاس (Reflection) يُوفِّر الانعكاس العديد من الأصناف -منها الصنف Assembly- والذي يَعمَل كمُغلِّف للتجميعة (assemblies). يُمكِن لبعض هذه الأصناف تَوفِير معلومات عن التجميعات المُحمَّلة من خلال البيانات الوَصفيّة (metadata) ببيان تلك التجميعات (Assembly manifest). يُمكِن استخدام بعض الأصناف الأُخرى لتَحمِيل ديناميكي للتجميعات، بل ولإنشاء أنواع جديدة واستدعائها ديناميكيا أثناء وقت التشغيل. جَلْب بيانات عن تجميعة باستخدام الانعكاس اِستخدِم الشيفرة التالية لجلب كائن Assembly الخاص بصنف معين: using System.Reflection; Assembly assembly = this.GetType().Assembly; اِستخدِم الشيفرة التالية لجَلْب كائن Assembly الخاص بالشيفرة قيد التنفيذ: Assembly assembly = Assembly.GetExecutingAssembly(); يُوفِّر الصنف Assembly التابع GetTypes المُستخدَم لجَلْب قائمة بجميع الأصناف المُعرَّفة ضِمْن التجميعة: foreach (var type in assembly.GetTypes()) { Console.WriteLine(type.FullName); } موازنة كائنين باستخدام الانعكاس في المثال التالي، يُستخَدم الانعكاس لموازنة كائنين. بالتحديد، يُستخدم التابع GetType لجلْب قيمة من الصنف Type تُحدد نوع الكائن، والتي بدورها تُستخدَم لجلْب قائمة بحقول الكائن باستدعاء التابع GetFields من خلالها، ثم يتم موازنة قيم تلك الحقول مع نظيراتها بالكائن الآخر. public class Equatable { public string field1; public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; var type = obj.GetType(); if (GetType() != type) return false; var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (var field in fields) if (field.GetValue(this) != field.GetValue(obj)) return false; return true; } public override int GetHashCode() { var accumulator = 0; var fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); foreach (var field in fields) accumulator = unchecked ((accumulator * 937) ^ field.GetValue(this).GetHashCode()); return accumulator; } } لاحظ أنه بغرض التبسيط، فإن المثال باﻷعلى يُجري موازنة معتمدة على الحقول فقط (يتجاهل الحقول الساكنة [static fields]، والخاصيات [properties]). جَلْب سِمة تعداد (enum) باستخدام الانعكاس (وتخزينها بصورة مؤقتة caching) تُعدّ السمات (attributes) مفيدة للإشارة إلى بعض البيانات الوَصفيّة (metadata) بالتعدادات (enums). يُستخدَم عادة التابع GetCustomAttributes لجَلْب قيم تلك السمات والذي قد يكون بطيئًا، لذلك من المهم الاستعانة بالذاكرة المخبئية لتخزين تلك القيم (caching)، كالتالي: private static Dictionary<object, object> attributeCache = new Dictionary<object, object>(); public static T GetAttribute<T, V>(this V value) where T : Attribute where V : struct { object temp; // حاول جلب قيمة السمة من الذاكرة المخبئية أولًا if (attributeCache.TryGetValue(value, out temp)) { return (T) temp; } else { // اجلب النوع Type type = value.GetType(); FieldInfo fieldInfo = type.GetField(value.ToString()); // اجلب سمات هذا النوع T[] attribs = (T[])fieldInfo.GetCustomAttributes(typeof(T), false); // أعد أول سمة تجدها var result = attribs.Length > 0 ? attribs[0] : null; // خزن النتيجة بالذاكرة المخبئية attributeCache.Add(value, result); return result; } } ضَبْط خواص الكائنات باستخدام الانعكاس بفرض أن لدينا الصنف التالي Classy الذي يَملك الخاصية Propertua: public class Classy { public string Propertua {get; set;} } لضبْط الخاصية Propertua الموجودة بكائن من النوع Classy باستخدام الانعكاس، يمكن استخدام التابع SetValue: var typeOfClassy = typeof (Classy); var classy = new Classy(); var prop = typeOfClassy.GetProperty("Propertua"); prop.SetValue(classy, "Value"); تنشئة كائن من النوع T باستخدام الانعكاس باستخدام باني الكائنات الافتراضي (default constructor): T variable = Activator.CreateInstance(typeof(T)); باستخدام بَانِي ذات معاملات غير محدَّدة النوع (parameterized constructor): T variable = Activator.CreateInstance(typeof(T), arg1, arg2); الإطار المُدار القابل للتوسيع (MEF) الإطار المُدار القَابِل للتوسيع Managed Extensibility Framework - MEF هو مكتبة لإنشاء برامج صغيرة الحجم وقابلة للتوسيع. عادةً ما تُسجَّل التَبَعيّات (dependencies) داخِل ملفات إعداد بالشيفرة المصدرية (hardcoding). يَترتَب على ذلك أنه لا يُصبِح بالإمكان تَغيير تلك التَبَعيّات إلا عن طريق تعديل الشيفرة وإعادة تَجمِيعها. على العكس من ذلك، يَسمَح الإطار المُدار القابل للتوسيع MEF باكتشاف التَبَعيّات أثناء زمن التشغيل (runtime) ضِمْنِيًّا، واستخدامها دون إعداد مُسبَق. يَسمَح MEF لعِدة مُكَوِّنات (components) بالتواصل معًا بانسيابية وسهولة. يَستخدِم كل مُكَوِّن سِمات مُعينة (attributes) للإعلان عن تَبَعيّاته وقُدراته، أي ما يَحتاج إلى اِستيراده (imports) وما يقوم بتَصديره (exports)-إن وُجِدَ- على الترتيب. يُعلَّن عن كُلًا من الاستيرادات والتصديرات بصورة مُواصَفَة اِصطلاحيّة (contract). يَنبغي لمُصَدِّر ومُستورِد مُعينين الإعلان عن نفس المُواصَفَة الاصطلاحيّة لعَدِّهما نَظيرين. لاحظ أنه لمّا كانت كل هذه المَعلومات مُتوفِّرة بالبيانات الوصفية (metadata) للمُكَوِّن، أَصبَح مِن المُمكن اكتشافها أثناء زمن التشغيل (runtime). يُزوِّد مُحرِك MEF المُكَوِّنات باستيراداتها (imports) المُعلَّن عنها اعتمادًا على حَاوِي التركيب (composition container) الذي يَضُمّ كتالوجات (catalogs) تَشتمِل على معلومات عن جميع المُكَوِّنات المُصدَّرة والمُتاحة للتركيب. تصدير صنف (Exporting) يُمكن لأي مُكَوِّن استخدام السمة ExportAttribute للاعلان عن تَصدير (export). في المثال التالي، صُدِّرَ النوع UserProvider كمُحقِّق للمُواصَفَة الاصطلاحيّة IUserProvider: using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; namespace Demo { [Export(typeof(IUserProvider))] public sealed class UserProvider : IUserProvider { public ReadOnlyCollection<User> GetAllUsers() { return new List<User> { new User(0, "admin"), new User(1, "Dennis"), new User(2, "Samantha"), }.AsReadOnly(); } } } في المثال بالأعلى، يُمكن تَعرِيف الصنف UserProvider بأيّ مكان؛ فالمهم هو تَزْوِيد الكتالوج (ComposablePartCatalogs) -الذي يُنشئه البرنامج- بطريقة يَستطيع مِن خلالها اكتشاف هذا الصنف. استيراد صنف (Importing) يُمكن لأي مُكَوِّن استخدَام السمة ImportAttribute للاعلان عن استيراد (import) أو تَبَعيّة. انظر المثال التالي: using System; using System.ComponentModel.Composition; namespace Demo { public sealed class UserWriter { [Import(typeof(IUserProvider))] private IUserProvider userProvider; public void PrintAllUsers() { foreach (User user in this.userProvider.GetAllUsers()) { Console.WriteLine(user); } } } } في المثال بالأعلى، يُعلِّن الصنف UserWriter عن استيراد لصنف يُحقِّق المُواصَفَة الاصطلاحيّة IUserProvider كقيمة للحَقْل userProvider. لاحِظ أنه ليس مُهمًا أين تقوم بتَعرِيف الصنف المُناظِر؛ فالمهم هو تَزْوِيد الكتالوج (ComposablePartCatalogs) -الذي يُنشئه البرنامج- بطريقة يستطيع من خلالها اكتشاف هذا الصنف. الرَبطْ (مثال بسيط) using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; namespace Demo { public static class Program { public static void Main() { using (var catalog = new ApplicationCatalog()) using (var exportProvider = new CatalogExportProvider(catalog)) using (var container = new CompositionContainer(exportProvider)) { exportProvider.SourceProvider = container; UserWriter writer = new UserWriter(); // at this point, writer's userProvider field is null container.ComposeParts(writer); // now, it should be non-null (or an exception will be thrown). writer.PrintAllUsers(); } } } } في المثال بالأعلى، تم تَزْوِيد حَاوِي التركيب (composition container) بكتالوج من النوع ApplicationCatalog، والذي يَعتمِد -في بَحْثه عن التَصديرات المُناظِرة- على ملفات التجميعات بامتداد .exe و DLL الموجودة بمجلد البرنامج. لذلك طالما تَوَفَّرت السِمة [Export(typeof(IUserProvider))] باحدى ملفات التجميعات بالمجلد، ستَنجَح عملية استيراد المُواصَفَة الاصطلاحيّة IUserProvider المُعلَّن عنها بداخل الصنف UserWriter. تَتوفَّر أنواع أُخرى من الكتالوجات مثل DirectoryCatalog، والتي يُمكِن استخدَامِها كبديل أو كإضافة للنوع ApplicationCatalog لتَوسِيع دائرة البَحْث عن تَصدِيرات مُناظِرة للاستيرادات المطلوبة. ترجمة -وبتصرف- للفصول Reflection - Managed Extensibility Framework - System.Reflection.Emit namespace من كتاب .NET Framework Notes for Professionals
-
يُنصح بتعزيز المشروع الخاص بك بتقنية اختبار الوَحْدَات (unit testing)، حيث يُوفِّر ذلك العديد من المزايا منها: سهولة إضافة خاصيات جديدة مع ضمان استمرارية عمل الشيفرة القديمة بطريقة سليمة. توفير توثيق برمجي لخاصيات المشروع. كتابة شيفرة أفضل من خلال تعزيز اِستخدَام الواجهات. نصائح لكتابة اختبار الوحدات ينبغي أن يتكون اسم الاختبار من ثلاثة أجزاء: اسم التابع تحت الاختبار. وصف للموقف المُراد اختباره. وصف التصرف المُتوقع عند حدوث هذا الموقف. مثال لاسم سئ: public void Test_Single() { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add("0"); Assert.Equal(0, actual); } مثال لاسم جيد: public void Add_SingleNumber_ReturnsSameNumber() { // } يَتكون أي اختبار من ثلاث خطوات: خطوة الإِعداد (arrange): يتم خلالها الإِعداد للاختبار بتجهيز الكائنات والنُسخ المُزيَّفة (mocks) وتوابعها بحيث يقتصِر الاختبار على ما نُريد فَحْصه فقط بمَعزَل عن بقية التَبَعيّات (dependencies) وغيره مما لا يَشمَله الاختبار. خطوة التَّنْفيذ (act): يتم خلالها الاستدعاء الفعلّي للتابع تحت الاختبار. خطوة الفَحْص (assert): يتم خلالها إِجراء الفُحوصات المَطلوبة. انظر المثال التالي: public void Add_EmptyString_ReturnsZero() { // Arrange var stringCalculator = new StringCalculator(); // Act var actual = stringCalculator.Add(""); // Assert Assert.Equal(0, actual); } تجنب كتابة اختبارات تحتوي على شروط منطقية مثل if و for و while ..إلخ؛ لأن ذلك سيزيد من احتمالية وجود أخطاء برمجية بالاختبار، وهذا آخر ما تود أن يحدث. لابُدّ لاختبارات الوحدة أن تكون خالية تمامًا من الأخطاء حتى تكون محلًا للثقة. مثال سئ: [Fact] public void Add_MultipleNumbers_ReturnsCorrectResults() { var stringCalculator = new StringCalculator(); var expected = 0; var testCases = new[] { "0,0,0", "0,1,2", "1,2,3" }; foreach (var test in testCases) { Assert.Equal(expected, stringCalculator.Add(test)); expected += 3; } } مثال أفضل: [Theory] [InlineData("0,0,0", 0)] [InlineData("0,1,2", 3)] [InlineData("1,2,3", 6)] public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected) { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add(input); Assert.Equal(expected, actual); } لابُدّ أن يحتوي كل اختبار على جملة فحص وحيدة. إذا كنت تريد إجراء أكثر من فحص لموقف معين، يُمكنك إنشاء اختبار منفصل لكل جملة فحص، مما سيُعطيك تصور أوضح لسبب فشل الاختبار. مثال سئ: [Fact] public void Add_EdgeCases_ThrowsArgumentExceptions() { Assert.Throws<ArgumentException>(() => stringCalculator.Add(null)); Assert.Throws<ArgumentException>(() => stringCalculator.Add("a")); } مثال أفضل: [Theory] [InlineData(null)] [InlineData("a")] public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input) { var stringCalculator = new StringCalculator(); Action actual = () => stringCalculator.Add(input); Assert.Throws<ArgumentException>(actual); } افحص التوابع العامة (public) فقط، وتجنب فحص التوابع الخاصة (private) فهي بالنهاية من المُفترض أن تُنفَّذ أثناء تَّنْفيذ احدى التوابع العامة. علاوة على ذلك فإنها تُعدّ مجرد تفصيلة صغيرة ضمن تَّنفيذ معين (implementation). قد يُغيّر المُبرمج في الواقع من طريقة التنفيذ بدون أن يؤثر على الوظيفة الفعلية الواقعة تحت الاختبار. لذا لا تُقيد اختباراتك على تَّنْفيذ بعينه. عند الحاجة لتهيئة نفس الكائنات لجميع الاختبارات، استخدم توابع مُساعدة (helper methods) بدلًا من اِستخدَام التابعين setup و teardown. يلجأ الكثير من المُبرمجين إلى اضافة شيفرات التهيئة بهما نظرًا لأنهما يُستدعيان تلقائيا قبل بدء تنفيذ أي اختبار وبعد انتهاء تنفيذه على الترتيب، لكن يؤدي ذلك إلى صعوبة قراءة الاختبار كما قد يؤدي أحيانًا إلى تهيئة كائنات غير مطلوبة لبعض الاختبارات. مثال للطريقة غير السليمة: private readonly StringCalculator stringCalculator; public StringCalculatorTests() { stringCalculator = new StringCalculator(); } // more tests... [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var result = stringCalculator.Add("0,1"); Assert.Equal(1, result); } مثال للطريقة الأفضل: [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var stringCalculator = CreateDefaultStringCalculator(); var actual = stringCalculator.Add("0,1"); Assert.Equal(1, actual); } // more tests... private StringCalculator CreateDefaultStringCalculator() { return new StringCalculator(); } إضافة مشروع اختبار الوحدات إلى حل موجود مسبقًا انقر بزر الفأرة الأيمن على الحل (solution) واختر أَضِف مشروع جديد. اختر نموذج مشروع اختبار الوَحْدَات (Unit Test Project) من جزء الاختبارات. اختر اسم للمكتبة (assembly). مثلًا إذا كان اسم المشروع المُختبَر Foo، فرُبما تَستخدِم الاسم Foo.Tests. أَضِف مَرجِع (reference) المشروع المُختبَر ضِمْن مَراجِع مشروع اختبار الوَحْدَات. إضافة تابع بغرض الاختبار تَتطلَّب MSTest (بيئة العمل الافتراضية للفَحْص) زَخرفة (decoration) الأصناف بغرض الاختبار (test classes) باستخدام السمة TestClass، وكذلك أن تَكون التوابع بغرض الاختبار (test methods) مُزخرَفة باستخدَام السمة TestMethod وأن تَكون عَلنية (public). في المثال التالي، يَختبِر التابع Test1 كَوْن قيمة المُتغيّر result مُساوِية للقيمة 1. [TestClass] public class FizzBuzzFixture { [TestMethod] public void Test1() { //arrange var solver = new FizzBuzzSolver(); //act var result = solver.FizzBuzz(1); //assert Assert.AreEqual("1",result); } } تتوفَّر الكثير من التوابع لإجراء الفحوصات مثل التابع AreEqual و AreNotEqual و IsTrue وغيرها. مثال آخر: [TestClass] public class UnitTest1 { private const string Expected = "Hello World!"; [TestMethod] public void TestMethod1() { using (var sw = new StringWriter()) { Console.SetOut(sw); HelloWorldCore.Program.Main(); var result = sw.ToString().Trim(); Assert.AreEqual(Expected, result); } } } مثال آخر باستخدام بيئة عمل NUnit للفَحْص (لاحظ اختلاف السمات المُستخدَمة): using NUnit.Framework; using System.IO; using System; namespace HelloWorldTests { public class Tests { private const string Expected = "Hello World!"; [SetUp] public void Setup() { } [Test] public void TestMethod1() { using (var sw = new StringWriter()) { Console.SetOut(sw); HelloWorldCore.Program.Main(); var result = sw.ToString().Trim(); Assert.AreEqual(Expected, result); } } } } تشغيل اختبار الوحدات افتح نافذة Test Explorer من قائمة Test بفيجوال ستوديو، ثم اُنقر على زر "تشغيل الكل (Run All)" مما سيؤدي إلى بدء تَّنْفيذ جميع الاختبارات. بعد انتهاء تَّنْفيذ أي اختبار، ستجد علامة بجانب اسمه إما باللون الأخضر أو الأحمر. يُشير اللون الأخضر إلى نجاح الاختبار أما اللون الأحمر فيُشير إلى فشله. نتائج حية لاختبار الوحدات (Live Unit Testing) يُوفِّر فيجوال ستوديو بدايةً من الإصدار 2017 خاصية عَرْض نتائج اختبار الوَحْدَات بصورة حيّة. يُمكنك تفعيل هذه الخاصية من خلال فتح قائمة Test ثم النقر على زر "اختبار حيّ للوَحْدَات Live Unit Testing" ثم على زر "ابدأ Start" بالقائمة الفرعية. ستجد -في حالة وجود اختبار مُناظِر- علامة صح أو خطأ إلى جانب كل تابع بنافذة مُحرِّر الشيفرة (code editor) لتُشير إلى نجاح أو فشل الاختبارات الخاصة به، كما أنها تُحدَّث تلقائيًا بينما تقوم بتعديل الشيفرة. ترجمة -وبتصرف- للفصل Unit Testing من كتاب .NET Framework Notes for Professionals
-
تُخزَّن الكائنات المُنشئة باستخدام العَامِل new بقسم الكَوْمَة المُدار في الذاكرة (managed heap). يُوفِّر اطار عمل .Net كَانِس المُهملات (Garbage Collector)، والذي يُدير الذاكرة ويُنْهِي (finalize) الكائنات المهملة دون أيّ تَدَخُّل صريح من المُبرمج. تَستعرِض بعض الأمثلة التالية طريقة عمل كَانِس المُهملات وسُلوكه بشئ من التفصيل، ويُوضِح بعضها الآخر كيفية إعداد الأنواع بطريقة تَسمَح لكَانِس المُهملات بإدارتها بشكل سليم. مفهوم الكائنات الحية والكائنات الميتة كقاعدة عامة، يُعدّ الكائن حيًّا (live object) إذا ما زال بالإِمكان إعادة اِستخدَامه، ويُعدّ مَيتًا (dead object) إذا أصبح ذلك غير ممكن. ما يُحدِّد إِمكانية الاستخدَام مِن عدمه هو وجود مُتغيّر أو حَقْل واحد على الأقل يَحمِل مَرجِعًا (reference) إلى مكان الكائن بالذاكرة. في حالة خروج كل مُتغيّرات مَراجِع كائن ما -هذا إن وُجدت أساسًا في مرحلة ما أثناء التَّنفيذ- من النطاق (scope)، يُعدّ الكائن مَيتًا ويُنْهَى (finalize) عند إجراء عملية كَنْس. مثال 1 في الشيفرة التالية، نُعرِّف النوع FinalizableObject، والذي يَحوِي فقط باني النوع (constructor) ومُنْهِيه (finalizer): public class FinalizableObject { public FinalizableObject() { Console.WriteLine("Instance initialized"); } ~FinalizableObject() { Console.WriteLine("Instance finalized"); } } نُنشئ كائنًا من هذا النوع: new FinalizableObject(); مما يُنتِج الخْرج التالي بالرغم مِن أننا لم نَستخدِم الكائن بَعْد: <namespace>.FinalizableObject initialized لن يُنْهَى الكائن حتى يَنتهي عَمَل البرنامج، والذي يؤدي في الواقع إلى إنهاء جميع الكائنات وتحرير مساحتها من قسم الكَوْمَة بالذاكرة. ومع ذلك يُمكنك استدعاء التابع Collect لإجبار كَانِس المُهملات (Garbage Collector) على إجراء عملية كَنْس خلال لحظة معينة، كالتالي: new FinalizableObject(); GC.Collect(); مما يُنتِج الخْرج التالي: <namespace>.FinalizableObject initialized <namespace>.FinalizableObject finalized أُنْهيت جميع الكائنات الميتة غير المُستخدَمة (dead objects)، وحُرِّرت مساحتها من قسم الكَوْمَة بالذاكرة بمجرد اِستدعاء كَانِس المُهملات. مثال 2 بِفَرْض أن كلًا من النوعين FinalizableObject1 و FinalizableObject2 مُشتقَّين من النوع FinalizableObject المُعرَّف مُسبقًا ويَرِثا نفس سُلوك طباعة رسائل التهيئة والإنهاء، نُنشِئ الكائنات التالية: var obj1 = new FinalizableObject1(); var obj2 = new FinalizableObject2(); obj1 = null; // (1) GC.Collect(); // (2) يكون الخْرج كالتالي: <namespace>.FinalizableObject1 initialized <namespace>.FinalizableObject2 initialized <namespace>.FinalizableObject1 finalized (1) لمّا أُسْنِدت القيمة الفارغة null إلى المُتغيّر obj1 (كان يَحمِل مَرجِعا إلى الكائن من النوع FinalizableObject1)، أصبح مِن غير الممكن -بطبيعة الحال- الولوج لهذا الكائن مرة أُخرى وبالتالي عُدّ ميتًا. ولذلك عندما اُستدعِي كَانِس المُهملات في وقت لاحق من تنفيذ الشيفرة (2)، أنهاه وحرَّر مساحته بقسم الكَوْمَة بالذاكرة، ويَظهَر ذلك جَلّيًا من خلال طباعة عبارة المُنْهِي (finalizer). على النقيض، ما يزال هناك مَرجِع للكائن من النوع FinalizableObject2 وبالتالي عُدّ حيًّا ولم يُنْهَ. مثال 3 بِفَرْض أن النوع FinalizableObject يَحمِل خاصية عَلّنية (public) من نفس نوعه تُسمَّى OtherObject. ماذا سيحُدث لو كان هناك كائنين ميتين، يَحمِل كلًا منهما مَرجِعًا للآخر؟ كالتالي: var obj1 = new FinalizableObject1(); var obj2 = new FinalizableObject2(); obj1.OtherObject = obj2; obj2.OtherObject = obj1; obj1 = null; // لم يعد هناك مَرجِع للكائن من النوع FinalizableObject1 obj2 = null; // لم يعد هناك مَرجِع للكائن من النوع FinalizableObject2 // لكن كلا منهما ما يزال يحمل مرجعا للآخر GC.Collect() يكون الخْرج كالتالي: <namespace>.FinalizedObject1 initialized <namespace>.FinalizedObject2 initialized <namespace>.FinalizedObject1 finalized <namespace>.FinalizedObject2 finalized على الرغم من أن كِلاَ الكائنين يَحمِل مَرجِعا إلى الكائن الآخر، يَقوُم كانس المُهملات بإنهائهما ويُحرِّر مساحتهما من قسم الكَوْمَة بالذاكرة؛ وذلك لعدم وجود أي مَرجِع لهما ضِمْن كائن حيّ. المَراجِع الضعيفة (Weak References) المَراجِع الضعيفة WeakReference هي -كأيّ مَرجِع- تُشير إلى مكان كائن مُعين بالذاكرة. لا يُعوِّل كَانِس المُهملات على وجود المَراجِع الضعيفة (weak references) عند تقديره لحالة الكائن من حيث كَوْنه حيًا أو ميتًا. وبالتالي، لا يَمنع وجود مَرجِع ضعيف إلى كائن معين كَانِس المُهملات من إنهاء هذا الكائن وتَحرير مساحته بالذاكرة، ومن هنا كان عَدّها ضعيفة (weak). انظر المثال التالي: var weak = new WeakReference<FinalizableObject>(new FinalizableObject()); GC.Collect(); يُنتِج الخْرج التالي: <namespace>.FinalizableObject initialized <namespace>.FinalizableObject finalized حَذَفَ كَانِس المُهملات -عند اِسْتِدْعائه- الكائن من قسم الكَوْمَة بالذاكرة على الرغم من وجود مَرجِعًا إليه من النوع WeakReference داخل النطاق (scope). نَستنتِج من ذلك: أولًا: مِن غيْر الآمن أن تَفترِض أن المساحة المُخصَّصة بقسم الكَوْمَة لكائن مُشار إليه بمَرجِع ضعيف لا تزال صالحة. ثانيًا: عندما تحتاج إلى تحصيل (dereference) قيمة مَرجِع ضعيف، اِستخدِم التابع TryGetTarget أولًا والذي يُعيد قيمة منطقية تُحدِّد إذا ما حُرِّرت مساحته بالذاكرة أم لا، ثم اُكتب شيفرة لمُعالجة كِلاَ الحالتين. كالتالي: var target = new object(); var weak = new WeakReference<object>(target); target = null; // فحص ما إذا كان الكائن ما زال مُتاحًا if(weak.TryGetTarget(out target)) { // يُمكنك استخدام الكائن هنا } else { // لا ينبغي استخدام الكائن هنا } تُوفِّر جميع إصدارات إطار عمل .NET نسخة غير مُعمَّمة من النوع WeakReference والتي تَعمَل بنفس الطريقة. في المقابل، دُعِّمت النسخة المُعمَّمة منذ اصدار 4.5. انظر المثال التالي للنسخة الغيْر مُعمَّمة: var target = new object(); var weak = new WeakReference(target); target = null; if (weak.IsAlive) { target = weak.Target; // يُمكنك استخدام الكائن هنا } else { // لا ينبغي استخدام الكائن هنا } التابع Dispose والمنهيات (finalizers) إذا أردت التأكد من تحرير الموارد (resources) التي يَستخدِمها كائن مُعين بمجرد أن يَخْرُج من النطاق ويُصبح من غير الممكن اِستخدَامه -خاصة إن كانت الموارد مُستَنزِفة للذاكرة أو غير مُدارة مثل التَعامُل مع الملفات فقد يُبلَّغ عن اعتراض عند محاولة فتح ملف للقراءة ولا يُغلَق مِقبَض الملف (file handle) كما يَنبغي-، صَرِّح عن كَوْن نوع الكائن من الواجهة IDisposable والتي تحتوي على تابع وحيد هو Dispose لا يَستقبِل أي مُعامِلات: public interface IDisposable { Dispose(); } ثم نفِّذ تابعها Dispose داخل النوع المَذكور بحيث يكون هذا التابع مَسئولًا عن تحرير تلك الموارد. بخلاف المُنْهِيات (finalizers) التي تُستَدعَى دائمًا عند انتهاء فترة حياة الكائن، يَقع عاتِق اِستِدعاء التابع Dispose على المُبرمِج وهو ما ليس مَضْمُونًا. يُمكن للمُبرمِج اِستِدعاء التابع Dispose صراحةً، كالمثال التالي: private void SomeFunction() { // هيئ كائن يستهلك موارد مستنزفة للذاكرة var disposableObject = new ClassThatImplementsIDisposable(); // استدعي التابع Dispose disposableObject.Dispose(); // (1) } (1): يَخرُج المُتغيّر disposableObject من النطاق (scope). مع ذلك، لا يُمكننا الجزم بموعد إنهاء (finalize) الكائن لنفسه، ولذلك يَضمَن اِستِدعاء التابع Dispose تحرير موارده المُستَنزِفة للذاكرة. ربما تَستَدعِيها كذلك صراحةً داخل كُتلة finally، كالمثال التالي: StreamReader sr; string textFromFile; string filename = "SomeFile.txt"; try { sr = new StreamReader(filename); textFromFile = sr.ReadToEnd(); } finally { if (sr != null) sr.Dispose(); } أو من خلال اِستخدَام عبارة using والتي في الواقع يُفضَّل اِستخدَامها، وسيُحوِّلها المُصرِّف آليًا إلى نفس الشيفرة الموجودة بالأعلى، كالتالي: string textFromFile; string filename = "SomeFile.txt"; using (StreamReader sr = new Streamreader(filename)) { textFromFile = sr.ReadToEnd(); } يُعدّ التَعامُل مع الأنواع التي يَكون إطار العمل مسؤولًا عن تَنشِئة كائناتها مثال آخر. في هذه الحالة، عادة ما يُشتقّ النوع الجديد من نوع أساسي (base). مثلًا، عندما تُعْلِن عن نوع مُتَحكِم جديد (controller)، يَرِث من النوع الأساسي System.Web.Mvc.ControllerBase. فإذا كان النوع الأساسي يُنفِّذ الواجهة IDisposable، غالبًا ما يَعني ذلك أن اطار العمل سيَستَدعِي التابع Dispose بطريقة سليمة، ولكن هذا ليس مَضْمُونًا. لا يُعدّ التابع Dispose بديلًا للمُنْهِي (finalizer)، لكن ينبغي أن تَستخدِم كليهما بحسب الغرض: يُحرِّر المُنْهِي الموارد -على أيّ حال- لتَجَنُّب حُدوث أي تَسرُّب للذاكرة (memory leaks). يُحرِّر التابع Dispose الموارد بمجرد إنتهاء الحاجة إليها؛ لتخفيف الضغط على الذاكرة المُخصَّصة (memory allocation) عامةً. استخدام النوع SafeHandle لتغليف الموارد غير المُدارة عند كتابة مُغلِّف (wrapper) لموارد غير مُدارة، اِحرص على أن يُشتقّ المُغلِّف من النوع SafeHandle بدلًا من أن تُنفِّذ الواجهة IDisposable أو تُنشِئ مُنْهِي (finalizer) بنفسك، حيث يُنفِّذ هذا النوع بالفعل الواجهة من أجلك، ويُهيِئ المُنْهِيات (finalizers) تهيئة مناسبة، كما يَضمَن تَّنفيذ شيفرة التحرير. يجب أن يَكون النوع المُشتقّ من الصنف SafeHandle صغيرًا وبسيطًا لتقليل احتمالية تَسريب المِقبَض (handle). يتأكد النوع SafeHandle من تحرير أي موارد غيْر مُدارة حتى في حالة حدوث تَسريب لكائنات المُغلِّف. انظر المثال التالي: using System.Runtime.InteropServices; class MyHandle : SafeHandle { public override bool IsInvalid => handle == IntPtr.Zero; public MyHandle() : base(IntPtr.Zero, true) { } public MyHandle(int length) : this() { SetHandle(Marshal.AllocHGlobal(length)); } protected override bool ReleaseHandle() { Marshal.FreeHGlobal(handle); return true; } } تنبيه: لا يَتعدَّى المثال بالأعلى كَوْنِه مجرد محاولة لاستعراض طريقة اِشتقاق SafeHandle. وعليه وجب التنبيه أنه من العبث تَخصيص جزء من الذاكرة بهذه الطريقة. تحرير الكائنات وإنهائها بشكل سليم يَستهدِف كلًا من التابع Dispose والمُنْهِيات (finalizer) ظرفًا مختلفًا. ولذلك، من الضروري للأنواع التي تَستخدِم موارد مُستَنزِفة للذاكرة أن تُنفِّذ كلتا الطريقتين. وبالنتيجة، نحصل على نوع يُمكنه التعامُل المُلائم مع كِلاَ المَوقِفيّن المُحتملين: اِستدعاء المُنْهِي فقط. اِستدعاء التابع Dispose أولًا ثم اِستدعاء المُنْهِي فيما بعد. أحد الحلول هو كتابة شيفرة التنظيف (cleanup) بطريقة قابلة للتنفيذ أكثر من مرة بدون وجود اختلاف بالنتيجة. تَعتمِد تلك القابلية على طبيعة عملية التنظيف نفسها، على سبيل المثال: لا تُحدِث محاولة إغلاق اتصال بقاعدة بيانات (connection) تم إغلاقه مُسبقّا فرقًا. قد يؤدي تحديث عَداد إلى نتائج خاطئة عند اِستدعاء الشيفرة مرتين بدلًا من مرة واحدة. حل آخر هو التأكد من تَّنفيذ شيفرة التنظيف (cleanup) مرة واحدة فقط بغض النظر عن السياق الخارجي، وهو ما يُمكن عادةً تحقيقه باستخدام مُتغيّر راية (flag) مُخصَّص لهذا الغرض. كالتالي: public class DisposableFinalizable1: IDisposable { private bool disposed = false; ~DisposableFinalizable1() { Cleanup(); } public void Dispose() { Cleanup(); } private void Cleanup() { if(!disposed) { // ضمن الشيفرة الفعلية المسئولة عن تحرير الموارد هنا disposed = true; } } } كذلك يمكن تحقيقه باِستدعاء التابع SuppressFinalize() في حالة اِستدعاء التابع Dispose. يُوفِّر كانس المُهملات هذا التابع الذي يَسمَح بتخطي تَّنفيذ المُنْهِي (finalizer)، كالتالي: public class DisposableFinalizable2 : IDisposable { ~DisposableFinalizable2() { Cleanup(); } public void Dispose() { Cleanup(); GC.SuppressFinalize(this); } private void Cleanup() { // ضمن الشيفرة الفعلية المسئولة عن تحرير الموارد هنا } } التخزين المؤقت Caching يُوفِّر النوع MemoryCache بإطار عمل .NET العديد من التوابع لتخزين البيانات بالذاكرة (memory). إضافة عنصر باستخدام التابع Set يُستخدَم التابع Set لإضافة مُدخَل إلى الذاكرة المخبئية (cache)، حيث يَستقبَل مُعامل من النوع CacheItem يَحمِل كلًا من مفتاح وقيمة المُدخَل. private static bool SetToCache() { string key = "Cache_Key"; string value = "Cache_Value"; // احصل على مَرجِع لنُسخة النوع MemoryCache الافتراضية var cacheContainer = MemoryCache.Default; var policy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(DEFAULT_CACHE_EXPIRATION_MINUTES) }; var itemToCache = new CacheItem(key, value); cacheContainer.Set(itemToCache, policy); } لاحظ استخدام الصنف CacheItemPolicy لضبط توقيت انتهاء صلاحية المُدخَل ومَسحُه من الذاكرة المخبئية. جلب أو إضافة عنصر باستخدام التابع AddOrGetExisting يَجلِب التابع AddOrGetExisting قيمة المفتاح المُمرَّر إليه من الذاكرة المخبئية (cache) في حالة وجوده. أما إذا لم يُكن موجودًا، فإنه سيَستخدِم المُفوِّض valueFetchFactory المُمرَّر إليه لجَلْب قيمة يُخزِنها بالذاكرة كقيمة للمفتاح المحدَّد ثم يُعيدها. public static TValue GetExistingOrAdd<TValue>(string key, double minutesForExpiration, Func<TValue> valueFetchFactory) { try { // سيتم تقييم التهيئة المرجأة للنوع فقط في حالة عدم وجود العنصر المطلوب بالذاكرة المخبئية var newValue = new Lazy<TValue>(valueFetchFactory); CacheItemPolicy policy = new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutesForExpiration) }; // يعيد العنصر من الذاكرة المخبئية إذا كان موجودًا أو يُضيفه في حالة عدم وجوده var cachedItem = _cacheContainer.AddOrGetExisting(key, newValue, policy) as Lazy<TValue>; return (cachedItem ?? newValue).Value; } catch (Exception excep) { return default(TValue); } } ترجمة -وبتصرف- للفصول Memory management و Garbage Collection و System.Runtime.Caching.MemoryCache من كتاب .NET Framework Notes for Professionals
-
يُوفِّر إطار عمل .NET طرائق مُبسطة لتَدْويِل (Internationalization) وتَوْطِين (localization) مشروعك. التَدْويِل هو بناء مشروعك بطريقة تُسهِل من عملية تَهيئته للعَمَل بلغات مُختلفة دون إجراء تَغْييرات جوهرية عليه. يتم ذلك بفَصْل ملفات الترجمة عن ملفات شيفرة المشروع. في المقابل، فإن التَوْطِين هو عملية تهيئة المشروع ذاتها ليعَمَل مع لغة بعينها من خلال ترجمة النصوص إلى تلك اللغة. يُطلَق مُصطلح العَولمة (Globalization) ليشمل كُلًا من التَدْويِل والتَوْطِين. مثال بمشروع ASP.NET MVC أولًا: أَضِف حزمة I18N إلى مشروع الـ MVC الخاص بك. ثانيًا: أَضِف i18n.LocalizingModule لقسم <httpModules> أو لقسم <modules> بملف web.config، كالتالي: <!-- IIS 6 --> <httpModules> <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" /> </httpModules> <!-- IIS 7 --> <system.webServer> <modules> <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" /> </modules> </system.webServer> ثالثًا: أَضِف مجلد باسم locale للمجلد الرئيسي الخاص بموقعك، ثم انشِئ بداخله مجلد فرعي لكل لغة ترغب بدَعْمها، مثلًا /locale/ar/. رابعًا: انشِئ ملفًا نصيًا باسم messages.po بداخل كل مجلد من المجلدات الفرعية من الخطوة السابقة. خامسًا: يُمكِنك كتابة السطور التالية بملف messages.po، فقط بغرض الاختبار: #: Translation test msgid "Hello, world!" msgstr "أهلًا بالعالم!" سادسًا: أَضِف مُتحكِمًا (controller) إلى المشروع، يُعيد نصًا ما بغرض تجربة الترجمة. في المثال التالي، يُعيد المُتحكِم القيمة "[[[Hello, world!]]]". لاحظ أنه لابُد للنص الموجود داخل الأقواس الثلاثة أن يتطابق مع قيمة msgid ضِمْن ملف الترجمة .po using System.Web.Mvc; namespace I18nDemo.Controllers { public class DefaultController : Controller { public ActionResult Index() { return Content("[[[Hello, world!]]]"); } } } سابعًا: شغِّل المشروع، ثم اِفتَح الرابط المُقابِل للإجراء (action) الذي عَرَّفناه للتو، مثلًًا http://localhost:[yourportnumber]/default. ستُلاحِظ أنه قد تم تَغْيير الرابط تلقائيًا ليَعكِس اللغة الافتراضية الخاصة بك كالتالي http://localhost:[yourportnumber]/en/default -بفرض أن الانجليزية هي اللغة الافتراضية بمُتصفحك. ثامنًا: إذا استبدلت كلمة ar (أو اللغة التي أَعددت ملف ترجمة خاص بها) بكلمة en الموجودة بالرابط، فستظهر النُسخة المُترجمة من النص. تاسعًا: إذا غَيّرت إعدادات اللغة بمُتصفحك لتفضِيل لغة أُخرى، ثم فتحت الرابط /default مُجددًا دون تخصيص اللغة، ستجد أنه مثل المرة السابقة قد تم تَغْيير الرابط تلقائيًا ليَعكِس لغتك الافتراضية التي قمت بضَبطِها في الخطوة السابقة، وسيَظهر النص مُترجمًا بتلك اللغة. عاشرًا: أَضِف مُعالِجَات (handlers) بملف web.config لمنع مُستخدِمي موقعك من فَتح مجلد locale. <!-- IIS 6 --> <system.web> <httpHandlers> <add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/> </httpHandlers> </system.web> <!-- IIS 7 --> <system.webServer> <handlers> <remove name="BlockViewHandler"/> <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/> </handlers> </system.webServer> ترجمة -وبتصرف- للفصل Globalization من كتاب .NET Framework Notes for Professionals
-
عادةً ما تُخزَّن إعدادات البرنامج بملف xml. وفي المقابل يُوفِّر إطار عمل .NET أنواع مُجهَّزة لإسترجاع قيم تلك الإعدادت. مثلًا الملف app.config: <?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="keyName" value="anything, as a string"/> <add key="keyNames" value="123"/> <add key="keyNames" value="234"/> </appSettings> </configuration> في حالة كان لديك مُفتاحين يَحمِلان نفس الاسم بقسم appSettings بملف الإعدادات كالمثال السابق، تُسترجَع أخِر قيمة. في الإصدارات 1.0 و 1.1 من إطار عمل .NET كان النوع ConfigurationSettings هو الطريقة المُتَّبَعة لاسترجاع الإعدادات (settings)، لكنه أصبح مَهجُورًا (deprecated) في الإصدارات الحديثة (2.0 أو أحدث) حيث حَلَّت الأنواع ConfigurationManager و WebConfigurationManager مَحَلّه. استرجاع الاعدادات باستخدام النوع ConfigurationSettings لاحظ المثال التالي: using System; using System.Configuration; using System.Diagnostics; namespace ConsoleApplication1 { class Program { static void Main() { string keyValue = ConfigurationSettings.AppSettings["keyName"]; Debug.Assert("anything, as a string".Equals(keyValue)); string twoKeys = ConfigurationSettings.AppSettings["keyNames"]; Debug.Assert("234".Equals(twoKeys)); Console.ReadKey(); } } } استرجاع الإعدادات باستخدام النوع ConfigurationManager يُدعِّم النوع ConfigurationManager الخاصية AppSettings مما يَسمَح لك بالاستمرار باسترجاع قيم الإعدادات الموجودة بقسم appSettings بملف الإعدادات بنفس طريقة الاصدارات القديمة. using System; using System.Configuration; using System.Diagnostics; namespace ConsoleApplication1 { class Program { static void Main() { string keyValue = ConfigurationManager.AppSettings["keyName"]; Debug.Assert("anything, as a string".Equals(keyValue)); var twoKeys = ConfigurationManager.AppSettings["keyNames"]; Debug.Assert("234".Equals(twoKeys)); Console.ReadKey(); } } } الإعدادات صارمة النوع باستخدام فيجوال ستوديو بدلًا من استخدام قسم appSettings بملف الإعدادات، تُمكِنك بيئة التطوير المتكاملة فيجوال ستوديو (Visual Studio IDE) من إدارة كلًا من إعدادات البرنامج والمُستخدِم بسهولة مع المميزات الإضافية التالية: إعدادات صارمة النوع (strongly typed)، أي يُمكِنك تقييد قيمة الإعدادات بنوع معين بشرط أن يَكون بالإمكان سَلسَلة هذا النوع (serializable). سُهولة فَصْل كلًا من إعدادات البرنامج والمُستخدِم حيث تُخزَّن الأولى بملف إعدادات وحيد web.config في حالة المواقع الالكترونية وتطبيقات الويب، بينما تُخزَّن الثانية بملف user.config بمجلد بيانات المُستخدِمين، والذي يختلف مساره بحسب إصدار نظام التشغيل المُستخدَم. علاوة على ذلك، غُيّرت تسمية الملف app.config إلى assembly.exe.config مع استبدال اسم الملف التَّنْفيذي بكلمة assembly. إمكانية دَمج إعدادات البرنامج من عدة مكتبات أصناف (class libraries) إلى ملف إعدادات وحيد بدون حُدوث تَعارُض بالأسماء (name collisions) عن طريق تخصيص قسم (section) لكل مكتبة أصناف. تُوفِّر غالبية أنواع المشروعات نافذة الإعدادات (settings) بأداة تصميم خاصيات المشروع (Project Properties Designer). تُعدّ هذه النافذة نقطة البداية لإنشاء إعدادات مُخصَّصة لكُلًا من البرنامج والمُستخدِم. تكون هذه النافذة فارغة بشكل مبدئي مع رابط وحيد لإنشاء ملف إعدادات افتراضي. سيؤدي النقر على هذا الرابط إلى التَغْييرات التالية: ظهور شبكة تَحكُم (grid control) بنافذة الإعدادات، والتي ستُمكِنك من إضافة مُدخَلات (entries) الإعداد وتعديلها وحَذفِها. إنشاء ملف إعدادات (app.config أو web.config) في حالة عدم وجوده. إضافة عنصر Settings.settings أسفل المجلد الخاص بالخاصيات Properties بنافذة مُستكشِف الحل (Solution Explorer). سيفَتَح النقر عليه نافذة الإعدادات. إضافة ملف جديد Settings.Designer.__(.cs,.vb,etc.) أسفل مجلد الخاصيات Properties بمجلد المشروع. يحتوي هذا الملف على تَعرِيف الصنف Settings. لاحظ أن الشيفرة بهذا الملف مُولَّدة آليًا وبالتالي لا ينبغي تعديلها. مع ذلك لمّا كان هذا الصنف مُعرَّف باستخدَام المُحدِّد الجزئي (partial modifier)، تستطيع تَمديده (extend) وإضافة أعضاء (members) أُخرى له بملف مُنفصل. يُنفِّذ هذا الصنف نمط المتفرّدة (singleton) وتُستخدَم الخاصية Default المُعرَّفة بداخله للولوج للنُسخة المتفرّدة. مع كل مُدخَل إِعداد جديد تُضيفُه إلى نافذة الإعدادات، سيَقُوم فيجوال ستوديو بالتالي: تَخزِين المُدخَل الجديد بقسم (section) إعدادات مُخصَّص بملف الإعدادات. صُمم هذا القسم لكي تتم إدارته بواسطة الصنف Settings. إضافة عضو (member) جديد للصنف Settings من النوع المُختار بنافذة الإعدادات. يُعدّ هذا العضو مُمثِلًا للمُدخَل بلغة الـc#، ويُستخدَم لقراءة قيمة الإعداد وتعديلها. قراءة الإعدادات صارمة النوع المخزَّنة بملف الاعدادات بدءً بصنف Settings جديد وقسم إعدادات مُخصَّص: أَضِف مُدخَل إعداد جديد للبرنامج من النوع System.Timespan باسم ExampleTimeout، وأسْنِد إليه قيمة تُساوِي دقيقة واحدة، كالتالي: ثم اِحفَظ خاصيات المشروع، والذي يُخزِن بدوره مُدخَلات نافذة الإعدادات، كما يُعيد التوليد الآلي للصنف Settings، ويُحَدِّث ملف إعدادات المشروع. الآن تَستطيع اِسترجاع قيمة هذا المُدخَل بشيفرة C# من خلال الخاصية Default بالنوع Settings، كالتالي: using System; using System.Diagnostics; using ConsoleApplication1.Properties; namespace ConsoleApplication1 { class Program { static void Main() { TimeSpan exampleTimeout = Settings.Default.ExampleTimeout; Debug.Assert(TimeSpan.FromMinutes(1).Equals(exampleTimeout)); Console.ReadKey(); } } } يُمكِنك أيضًا الإطلاع على ملف إعدادات المشروع app.config -المُحدَّث تلقائيًا بواسطة فيجوال ستوديو- لفَحْص كيفية تخزين مُدخَلات إعدادات البرنامج، لتجد التالي: <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup,System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > <section name="ConsoleApplication1.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral,PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </sectionGroup> </configSections> <appSettings /> <applicationSettings> <ConsoleApplication1.Properties.Settings> <setting name="ExampleTimeout" serializeAs="String"> <value>00:01:00</value> </setting> </ConsoleApplication1.Properties.Settings> </applicationSettings> </configuration> اِستخدَم فيجوال ستوديو القسم applicationSettings وليس appSettings لإدارة مُدخَلات نافذة الإعدادات. يحتوي القسم الجديد على قسم فرعي مخصَّص وِفقًا لفضاء الاسم (namespace). يَحوِي هذا القسم الفرعي على عنصر setting مُنفَصِل لكل مُدخَل (entry). لا يُخزَّن نوع المُدخَل بملف الإعدادات وإنما يُفْرَض فقط من خلال الصنف Settings. يُمكِنك أيضًا الإطلاع على ملف الصنف Settings -المُحدَّث تلقائيًا بواسطة فيجوال ستوديو- لفَحْص كيفية اِستخدَام الصنف ConfigurationManager لقراءة القسم الفرعي المُخصَّص بالأعلى، لتجد التالي: ... [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("00:01:00")] public global::System.TimeSpan ExampleTimeout { get { return ((global::System.TimeSpan)(this["ExampleTimeout"])); } } ... لاحظ اِستخدَام السمة DefaultSettingValueAttribute لتخزِين القيمة المُحدَّدة للمُدخَل بنافذة الإعدادات بأداة تصميم خاصيات المشروع. تُستخدَم هذه القيمة الافتراضية في حالة لم يكن هناك عنصر مقابل لهذا المُدخَل بملف الإعدادات. ترجمة -وبتصرف- للفصل Settings من كتاب .NET Framework Notes for Professionals
-
استخدام تقنية ADO.NET تَستطيع تطبيقات إطار عمل .NET الاتصال بمصادر البيانات (data sources) المُختلفة، مثل خادم SQL، وأوراكل Oracle، وXML، عن طريق تقنية ADO.NET -من مايكروسوفت-، وبالتالي يُمكِنها إدخال وجَلْب وتعديل البيانات الموجودة بمصدر البيانات (data source) بمجرد الاتصال به وِفقًا للصلاحيات المتناسبة. تُوفِّر ADO.NET بِنْيَة معمارية بدون اتصال (connection-less)، وهو ما يُعدّ أسلوبًا آمنًا للتَعامُل مع قواعد البيانات؛ لأن -عن طريقه- لم يُعدّ من الضروري الإبقاء على الاتصال طوال الجلسة (session). الممارسات المثلى عند التعامل مع ADO.NET كقاعدة عامة، حَاوِل تَقصِير وقت الاتصال قدر المُسْتَطاع، واغلقه بمجرد انتهاء تَّنْفيذ الإجراء المطلوب إغلاقًا صريحًا، مما يَضمَن عودة الكائن المُستخدَم في الاتصال إلى مَجمع الاتصالات (connection pool)، ويُحسِن من أداء الاتصال الفعلّي بخادم قاعدة البيانات. لاحظ أن أكبر حجم لمَجمع الاتصالات (pool max size) هو 100 بشكل افتراضي. تجمُّع الاتصالات (connection pooling) بخادم SQL أحِط كل الكائنات المُستخدَمة في الاتصال بقاعدة البيانات (database connections) بكتلة using، مما يَضمَن إغلاقها (dispose) والتخلص منها حتى في حالة التبلِّيغ عن اعتراض. اِطلع على عبارة using (مرجع c#) لمزيد من المعلومات. اِسترجِع سَلاسِل اتصال قواعد البيانات (connection strings) بالاسم من ملف app.config أو web.config بناءً على نوع التطبيق: يَتطلَّب إدراج مَرجِع لمكتبة System.configuration. اِطلع على سلاسل الاتصال وملفات الإعداد لمزيد من المعلومات عن كيفية تنظيم ملف الإعداد. ضَمِّن أي قيمة دَخْل بمُعامِل (parameter)؛ لأنه: يُجنّبك هجمات الحقن (SQL Injection). يُجنّبك الأخطاء في حالة استخدام نص مُتلاعَب به (malformed)، مثل تَضْمِين علامة اقتباس أحادية بداخل النص، والتي تَستخدِمها قاعدة البيانات SQL كمِحرِف تهريب (escaping) أو لبدء سِلسِلة نصية جديدة. يَسمَح لمُوفِّر قاعدة البيانات (database provider) بإعادة اِستخدَام خطط تَّنْفيذ الاستعلام (query plans) -إن أمكن- مما يُعزز من الكفاءة. (غُير مُدَعَّم من جميع مُوفِّري قواعد البيانات، فقط بعضها). عند التَعامُل مع مُعامِلات قاعدة البيانات: يُعدّ عدم تَوافق نوع وحجم مُعامِلات قاعدة البيانات أحد أهم الأخطاء الشائعة والتي تؤدي إلى فَشَل عمليات الإضافة والتَحْديث والجَلْب. قُم بتسمية مُعامِلات قاعدة البيانات بمُسمَّيات ذات مغزى، بنفس الطريقة التي تُسمِي بها أيّ مُتغيّرات بالشيفرة. حدِّد نوع العمود (column) بقاعدة البيانات، مما يَضمَن عدم اِستخدَام أنواع المُعامِلات الخاطئة وتَجنُّب أيّ نتائج غيْر مُتوقَّعة. تَأكَد من صلاحية (validate) قيم المُعامِلات قبل تمريرها إلى الأوامر command (فكما تَعلَم: مُدخلات خاطئة - مُخرجات خاطئة garbage in, garbage out). اِستخدِم الأنواع الصحيحة عند إسناد القيم للمُعامِلات. مثلًا إذا كان لديك مُعامِل من النوع DateTime، لا تُسْنِد القيمة المطلوبة كسِلسِلة نصية من النوع string، ولكن اِسندها بحيث تَكون من النوع DateTime أي بعد تحليلها (parsing). حدِّد خاصية size للمُعامِلات من النوع string؛ فقد يُعاد اِستخدَام نفس خطة تَّنْفيذ الاستعلام (execution plan) إذا كانت المُعامِلات مُتَوافِقة في النوع والحجم. يُستخدم -1 للإشارة إلى MAX. لا تَستخدِم التابع AddWithValue؛ لأنه من السهل جدًا أن تَنسَى تَحديد نوع المُعامِل. اطلع على "هل يمكننا التوقف عن استخدام التابع AddWithValue؟" لمزيد من المعلومات. عند التَعامُل مع كائنات الاتصال: اِحرص على تأخير فتح الاتصال قدر المُسْتَطاع، واغلقه بأسرع ما يمكن، بحيث يقتصِر وقت الاتصال على تَّنْفيذ الإجراء المطلوب فقط. يُنصح بذلك عامةً عند التَعامُل مع أي مصدر خارجي. لا تُشارِك الكائنات المُستخدَمة في الاتصال بأي شكل (مثلًا: لا تَستخدِم نمط المفرّدة (singleton pattern) بهدف مشاركة نُسخة وحيدة من النوع SqlConnection)، ولكن انشِئ كائن جديد إذا اقتضت الضرورة وتَخلَص (dispose) منه بمجرد انتهائه من تَّنْفيذ المطلوب. وذلك للأسباب التالية: يَمتلك معظم مُوفِّري قواعد البيانات مَجمع اتصالات (connection pool)، مما يعني أنه ليس من المُكلِف تنشئة كائن اتصال جديد. يَلْغي أي احتمالية مُستقبلية لحُدوث أخطاء إذا بدأت الشيفرة في التَعامُل مع أكثر من خيط (thread). تنفيذ استعلامات SQL بصيغة أمر Command public void SaveNewEmployee(Employee newEmployee) { // (1) using(SqlConnection con = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["MyConnectionName"].ConnectionString)) { using(SqlCommand sc = new SqlCommand("INSERT INTO employee (FirstName, LastName,DateOfBirth /*etc*/) VALUES (@firstName, @lastName, @dateOfBirth /*etc*/)", con)) { // (2) sc.Parameters.Add(new SqlParameter("@firstName", SqlDbType.VarChar, 200){Value = newEmployee.FirstName ?? (object) System.DBNull.Value}); sc.Parameters.Add(new SqlParameter("@lastName", SqlDbType.VarChar, 200){Value = newEmployee.LastName ?? (object) System.DBNull.Value}); sc.Parameters.Add(new SqlParameter("@dateOfBirth", SqlDbType.Date){Value = newEmployee.DateOfBirth}); // (3) con.Open(); sc.ExecuteNonQuery(); } } } (1): تنشئة كائن الاتصال بقاعدة البيانات ضِمْن كتلة using (2): تم تَحديد النوع SqlDbType.VarChar والحجم 200 للمُعامِل firstName (3): إِرجاء الفَتح الفعلّي للاتصال قَدْرِ الإمكان ملحوظة 1: اطلع على التعداد SqlDbType ملحوظة 2: اطلع على التعداد MySqlDbType استخدام واجهات مشتركة لتجريد الأنواع الخاصة بالمورد نظرًا لوجود العديد من مصادر البيانات (data sources)، وبالتالي العديد من مُوفِّري قواعد البيانات (database providers)، يُفضَّل الاعتماد على واجهات مُشتَرَكة (common interfaces) لتجريد (abstract) العمليات المُشتَرَكة. var providerName = "System.Data.SqlClient"; var connectionString = "{your-connection-string}"; var factory = DbProviderFactories.GetFactory(providerName); using(var connection = factory.CreateConnection()) { //IDbConnection connection.ConnectionString = connectionString; connection.Open(); using(var command = connection.CreateCommand()) { //IDbCommand command.CommandText = "{query}"; using(var reader = command.ExecuteReader()) { //IDataReader while(reader.Read()) { ... } } } } ترجمة -وبتصرف- للفصل ADO.NET من كتاب .NET Framework Notes for Professionals
-
مُدير الحزم NuGet Package Manager هو إضافة (extension) إلى بيئة التطوير المتكاملة فيجوال ستوديو (Visual Studio IDE). تَحتَاج إلى تَثْبيته كي تتمكن من إدارة الحزم بمشروعك. يُمكن اِستخدَامه من خلال الطرفية أو من خلال واجهة مُستخدِم رسومية (GUI). يُمكنك الإطلاع على المزيد من خلال التوثيق الرسمي: تَثْبيت عميل NuGet وتَحْديثه. تثبيت مدير الحزم NuGet Package Manager تستطيع تَثْبيتُه عن طريق اختيار الإضافات والتحديثات Extensions and Updates بقائمة الأدوات Tools بـفيجوال ستوديو. كالتالي: سيُثبِّت ذلك كلًا من : الواجهة الرسومية: تستطيع الولوج إليها من خلال اختيار Manage NuGet Packages... من القائمة المَعْرُوضة بعد النقر بزر الفأرة الأيمن على مجلد المشروع (أو مجلد مَراجِعه References). أداة الطرفية Package Manager Console: تستطيع الوُلوج إليها من خلال قائمة الأدوات Tools -> مُدير الحزم NuGet Package Manager -> طرفية مُدير الحزم Package Manager Console. لاحظ أن مُدير الحزم مُضمَّن بجميع إصدارات فيجوال ستوديو بدءً من الإصدار 2012. إدارة الحزم باستخدام الواجهة الرسومية (UI) انقر بزر الفأرة الأيمن على مُجلد المشروع (أو مُجلد مَراجِعه)، ثم اختر Manage NuGet Packages... من القائمة. ستُفتَح نافذة مُدير الحزم كالتالي: إدارة الحزم باستخدام الطرفية (Console) انقر على قائمة الأدوات Tools -> مُدير الحزم NuGet Package Manager -> طرفية مُدير الحزم Package Manager Console. ستُفتَح الطرفية بفيجوال ستوديو. اطلع على التوثيق الرسمي. تَثْبيت الحزم تَستطيع اِستخدَام العديد من الأوامر من خلال الطرفية مثل الأمر Install-Package، المسئول عن تَثْبيت حزمة بمشروع، كالتالي: PM> Install-Package Elmah لاحظ أنه في حالة عدم تحديد المشروع المُراد تَثْبيت الحزمة إليه، فإنها تُثبَّت تلقائيًا بالمشروع المُختَار حاليًا كمشروع افتراضي. في المقابل، تستطيع تَخصيص المشروع المطلوب تَثْبيت الحزمة إليه، كالتالي: PM> Install-Package Elmah -ProjectName MyFirstWebsite تستطيع أيضًا تخصيص إصدار مُعين من الحزمة، كالتالي: PM> Install-Package EntityFramework -Version 6.1.2 تحديث الحزم اِستخدِم الأمر Update-Package لتَحْدِيث حزمة مُعينة، كالتالي: PM> Update-Package EntityFramework لاحظ أنه في حالة عدم تحديد المشروع المُراد تَحْدِيث حزمته، فإن أمر التَحْدِيث سيُنفَّذ على جميع المشروعات، وهو بذلك يختلف عن الوَضْع الافتراضي للأمر Install-Package الذي يُنفِّذ الأمر على المشروع الحالي فقط. في المقابل، تستطيع تخصيص اسم المشروع صراحةً، كالتالي: PM> Update-Package EntityFramework -ProjectName MyFirstWebsite إلغاء تثبيت الحزم PM> Uninstall-Package EntityFramework بالمثل، يُمكنك تخصيص مشروع معين، كالتالي: PM> Uninstall-Package -ProjectName MyProjectB EntityFramework بالمثل، يُمكنك تخصيص إصدار معين، كالتالي: PM> uninstall-Package EntityFramework -Version 6.1.2 إضافة مصدر حزم مثل MyGet و Klondike nuget sources add -name feedname -source http://sourcefeedurl ترجمة -وبتصرف- للفصل Dependency Injection من كتاب .NET Framework Notes for Professionals
-
يعني حَقْن التَبَعيّات (Dependency Injection) بالأساس عملية كتابة الأنواع بطريقة تَمنَع تلك الأنواع من التَحكُم بتَبعيّاتها، هي فقط تُعلِّن عن اعتمادها على تَبَعيّات مُعينة، وفي المقابل تُوفَّر لها تلك التبعيّات فيما يُعرَف بعملية الحَقْن (Injection). في المثال التالي، تَقتصِر مسؤولية النوع Greeter على عرض رسالة تحيّة. للقيام بذلك، يَعتمِد النوع على تَبَعيّتان (dependencies)، تَمنَحُه الأولى نص رسالة التحية المطلوب إخراجها، بينما تُوفِّر التَبَعيّة الأُخرى الوسيلة المُستخدَمة لإخراج التحية. تَصِف الواجهتين IGreetingProvider و IGreetingWriter هاتين التَبَعيّتين على الترتيب، وتُحقَن (inject) التَبَعيّتين بالنوع. public class Greeter { private readonly IGreetingProvider _greetingProvider; private readonly IGreetingWriter _greetingWriter; public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter) { _greetingProvider = greetingProvider; _greetingWriter = greetingWriter; } public void Greet() { var greeting = _greetingProvider.GetGreeting(); _greetingWriter.WriteGreeting(greeting); } } public interface IGreetingProvider { string GetGreeting(); } public interface IGreetingWriter { void WriteGreeting(string greeting); } على الرغم من اعتماد النوع Greeting على كُلًا من الواجهتين IGreetingProvider و IGreetingWriter، فهو في نفس الوقت غيْر مسؤول عن تَنشئة نُسخ من أيّ منهما، وإنما يُفْترَض أن يَستقبِلهما من خلال باني الكائنات (constructor) خاصته. وبالتالي لابد لأي شيفرة تُنشِئ نسخة من هذا النوع أن تُوفِّر هاتين التَبَعيّتين فيما يُعرَف باسم حَقْن (injecting) التَبعيّات. في المثال السابق، يُمكن تسميتها أيضًا باسم حَقْن باني الكائنات (constructor injection)؛ نظرًا لأن التَبعيّات تُوفَّر من خلال باني الكائنات. بعضًا من الأعراف (conventions) الأكثر شيوعًا: يُخزِّن باني الكائنات (constructor) الخاص بنوع ما تَبعيّاته (dependencies) بعد استقبالها في حُقول خاصة (private fields). تُصبِح تلك التَبعيّات مُتوفِّرة لكل التوابع غير الساكنة (non-static) بهذا النوع بُمجرد تنشئة نُسخة منه. تَكون الحُقول الخاصة (private fields) للقراءة فقط، ولا يُمكِن تَغْيير قيمتها بعد ضَبْطِها بواسطة باني الكائنات (constructor)، مما يَعكِس أنه ليس من المَنوط -بل وليس بالإمكان- تَغْيير قيم تلك الحُقول خارج باني الكائنات، كما يَضمَن تَوفُّر التَبَعيّات طوال فترة حياة (lifetime) الكائن. تَكون التَبَعيّات عبارة عن واجهات. ليس هذا ضروريًا وإن كان شائعًا؛ لأنه يُسهِّل من استبدال تَّنْفيذ (implementation) تبعيّة معينة بتَّنْفيذ آخر. بالإضافة إلى أنه يَسمَح باستخدَام نُسخ مزيفة (mocks) من الواجهة لأغراض اختبار الوَحْدَات (unit testing). لماذا يسهل حقن التبعيات من اختبار الوحدات؟ في المثال بالأعلى، يَعتمِد النوع Greeter على تَبَعيّتان من الواجهتين IGreetingProvider و IGreetingWriter. قد يُعيد التَّنْفيذ (implementation) الفعلّي للواجهة IGreetingProvider السِلسِلة النصية من خلال اِستدعاء API أو من خلال قاعدة بيانات. بينما قد يَعرِض التَّنْفيذ الفعلّي للواجهة IGreetingWriter النص على الطرفية (console). لمّا كان النوع Greeter يَستخدِم حَقْن التَبَعيّة (dependency Injection) لتوفير تَبعيّاته، فمن السهل كتابة اختبار وِحْدَة (unit test) يَحقِن نُسخ مزيفة (mocks) من تلك الواجهات، كالتالي: public class TestGreetingProvider : IGreetingProvider { public const string TestGreeting = "Hello!"; public string GetGreeting() { return TestGreeting; } } public class TestGreetingWriter : List<string>, IGreetingWriter { public void WriteGreeting(string greeting) { Add(greeting); } } [TestClass] public class GreeterTests { [TestMethod] public void Greeter_WritesGreeting() { var greetingProvider = new TestGreetingProvider(); var greetingWriter = new TestGreetingWriter(); var greeter = new Greeter(greetingProvider, greetingWriter); greeter.Greet(); Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting); } } ملحوظة: عادةً ما يُستخدَم اطار عمل مثل Moq لتَنشِئة النُسخ المزيفة (mocks)، لكن في المثال بالأعلى، تم كتابة التَّنْفيذات (implementations) المُزيَّفة للتبسيط. يَتحقق اختبار الوِحْدَة (unit test) بالأعلى مما إذا كان النوع Greeter يَتسلَم نصوص الرسائل ثم يُخرِجها للطباعة بشكل صحيح، مما يعني أن الطريقة التي تَعمَل بها التَبعيّات IGreetingProvider و IGreetingWriter ليست ذات صلة هنا؛ فكل ما يُختبَر هو فقط طريقة تَفاعُل (interact) هذا النوع مع تَبَعيّاته. تَسمَح كتابة النوع بأسلوب حَقْن التَبَعيّة (dependency injection) بحَقْن تَبَعيّات مُزيَّفة دون تعقيد، وهو ما يُسهِل من كتابة اختبار الوَحْدَات (unit testing). لماذا نحتاج حاوي الخدمات (IoC Containers)؟ كما ذكرنا بالأعلى فحَقْن التَبَعيّات هو أسلوب كتابة للأنواع، وهو بذلك يختلف عن اِستخدَام إطار عمل حَقْن التبعيّات (يُعرَف عادة باسم حَاوِي الخدمات DI container / IoC container) مثل Windsor وAutofac وSimpleInjector وNinject وUnity وغيرها. تُسهِل تلك الحَاوِيات (containers) من عملية حَقْن التبعيّات. فلنفترِض أن لديك عددًا من الأنواع التي تَستخدِم حَقْن التبعيّات. فمثلًا يَعتمِد واحد منها على واجهات متعددة، وبدورها تَعتمَد بعض الأنواع المُنفِّذة لهذه الواجهات على واجهات أخرى وهكذا. لنقل أيضًا أن بعضًا منها يَعتمِد على قيم بعينها. كذلك يُنفِّذ البعض الآخر الواجهة IDisposable مما يعني ضرورة التخلص منها بعد انتهاء الغرض من وجودها. بهذه الطريقة أصبح كل نوع مَكتوبًا كتابة جيدة وبطريقة تُسهِل من اختباره، ولكن في المقابل، أصبحت عملية إنشاء نُسخة من نوع معين أمرًا بالغ التعقيد. في المثال التالي، نُنشِئ نُسخة من نوع CustomerService والتي تَمتلك تَبعيّات وتلك التَبعيّات بدورها تَمتلك تَبعيّات أُخرى: public CustomerData GetCustomerData(string customerNumber) { var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"]; var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"]; var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString; using(var logWriter = new LogWriter(logFilePath )) { using(var customerApiClient = new CustomerApiClient(customerApiEndpoint)) { var customerService = new CustomerService( new SqlAuthorizationRepository(authorizationConnectionString, logWriter), new CustomerDataRepository(customerApiClient, logWriter), logWriter ); // All this just to create an instance of CustomerService! return customerService.GetCustomerData(string customerNumber); } } } رُبما تتساءل لماذا لا نُضمِّن شيفرة البناء بالأعلى بداخل دالة منفصلة، بحيث يَقتصِر دَورِها على إعادة كائن من النوع CustomerService؟ دَعنا نَتفِق أنه ليس من المُفترَض لأي نوع معرفة ما إذا كانت تَبعيّاته تُنفِّذ الواجهة IDisposable أم لا، بل لا يُفترَض لتلك الأنواع التخلص من تلك التَبعيّات في حالة كانت كذلك. وإنما جُل التَعامُل مع تلك التَبعيّات هو اِستخدَامها. ولذلك، إذا كان لدينا دالة GetCustomerService تُعيد كائنًا مُكتمِل البناء من النوع CustomerService، فإننا نَفتَح الباب أمام وجود كائنات يُحتمَل امتلاكها لتَبعيّات تُنفِّذ الواجهة IDisposable دون وجود طريقة للولوج الى تلك التَبعيّات للتخلص منها. إلى جانب مشكلة صعوبة التخلص من الكائنات المُنفِّذة للواجهة IDisposable، مَن قد يرغب باستدعاء سِلسِلة من بواني الكائنات المُتداخِلة بهذه الطريقة لأيّ سبب كان؟ لاحظ أن المثال بالأعلى يُعدّ مجرد مثال قصير وبسيط. يمكن للأمر أن يزداد سوءً. ينبغي التأكيد على أن هذا لا يعني بالضرورة أن الأنواع مَكتوبة بطريقة خاطئة، فقد تَكون مكتوبة بشكل مثالي تمامًا كل على حِدى، لكن تَجمِيعهم سويًا هو ما قد يُمثِل تحديًا. هنا يأتي دور حَاوِيات حَقْن التَبعيّات حيث تَسمَح تلك الحَاوِيات بتخصيص النوع أو القيمة التي يجب اِستخدَامها لاِستيفاء تَبعيّة معينة. تُعرَف هذه العملية باسم تَسجِيل التبعيّات (registering dependencies) أو ضَبْط الحَاوِي (configuring the container). فمثلًا: var container = new WindsorContainer(); container.Register( Component.For<CustomerService>(), Component.For<ILogWriter, LogWriter>() .DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")), Component.For<IAuthorizationRepository, SqlAuthorizationRepository>() .DependsOn(Dependency.OnValue( connectionString, ConfigurationManager .ConnectionStrings["authorization"].ConnectionString )), Component.For<ICustomerDataProvider, CustomerApiClient>() .DependsOn(Dependency.OnAppSettingsValue( "apiEndpoint", "customerApi:customerApiEndpoint" )) ); جُل ما تَقوم به الشيفرة بالأعلى هو تَبلّيغ الحَاوِي بالآتي: لاستيفاء طلب تَبعيّة من الواجهة ILogWriter، اِنشِئ نُسخة من النوع LogWriter، واستخدِم القيمة الثابتة logWriter:logFilePath الموجودة بملف الإعدادات AppSettings لتوفير السِلسِلة النصية المطلوبة. لاستيفاء طلب تَبعيّة من الواجهة IAuthorizationRepository، اِنشِئ نُسخة من النوع SqlAuthorizationRepository، واستخدِم القيمة المذكورة بالأعلى من قسم ConnectionStrings لتوفير نص سِلسِلة الاتصال بقاعدة البيانات (connection string). لاستيفاء طلب تَبعيّة من الواجهة ICustomerDataProvider، اِنشِئ نُسخة من النوع CustomerApiClient، واستخدِم القيمة الفلانية الموجودة بملف الإعدادات AppSettings لتوفير السِلسِلة النصية المطلوبة. الآن، عندما نحتاج تَبعيّة معينة، -وهو ما يُعرَف باسم استيفاء (resolving) التبعيّة-، فمن المُمارسات السيئة أن تَستَوفِي التَبَعيّة مُباشرة من الحَاوِي، كالتالي: var customerService = container.Resolve<CustomerService>(); var data = customerService.GetCustomerData(customerNumber); container.Release(customerService); يَعتمد النوع CustomerService على التَبعيّتان IAuthorizationRepository وICustomerDataProvider. ولمّا كان الحَاوِي يَعلم الأنواع التي ينبغي اِنشاؤها لاستيفاء تلك التَبعيّات، بالإضافة إلى مَعرِفته بالأنواع التي ينبغي إنشاؤها لاستيفاء تَبعيّات تلك التَبعيّات، فإنه يُنشِئ جميع الأنواع المطلوبة إلى النقطة التي يُصبِح فيها قادرًا على إعادة نُسخة من النوع الأساسي المَطلوب CustomerService. إذا وَجَد الحَاوِي نفسه مُضطرًا لاستيفاء تَبعيّة لم يتم تسجيلها مثل IDoesSomethingElse، فإنه سيُبلِّغ عن اعتراض واضح يُعلِمنا بعدم تَوفُّر المعلومات المطلوبة لاستيفاء تلك الواجهة. قد تختلف حَاوِيات حَقْن التَبعيّات عن بعضها قليلًا، لكنها توفر لنا بشكل عام قدرًا معينًا من التَحكُم بطريقة إنشاء الأنواع. فمثلًا، هل نرغب بإنشاء نُسخة وحيدة من واجهة معينة ونُوفِّرها لأي نوع يطلبها، أم نرغب بإنشاء نُسخة جديدة من تلك الواجهة في كل مرة تُطلَّب فيها تلك التَبعيّة؟ تُوفر غالبية الحَاوِيات طريقة ما لتخصيص ذلك. ماذا عن الأنواع المُنفِّذة للواجهة IDisposable؟ في الواقع، استدعينا container.Release(customerService) لهذا الغرض. تقوم غالبية الحَاوِيات بالتخلُص (Dispose) من نُسخ التَبعيّات التي تحتاج لذلك. قد يبدو تسجيل التَبعيّات -كما رأينا بالأعلى- عملًا كثيرًا للقيام به. لكنه حقًا سيُؤْتي ثماره خاصة عند العمل على تطبيق يََحتاج لأنواع كثيرة تَعتمِد بدورها على تَبعيّات أُخرى كثيرة، وأنه لو اضطررنا لكتابة نفس تلك الأنواع بدون اِستخدَام حَقْن التبعيّات، فسيُصبِح من الصعب جدًا التَعامُل مع هذا التطبيق واختباره. بهذا نكون ألقينا نظرة خاطفة على أسباب اِستخدَام حَاوِيات حَقْن التبعيّات. مع ذلك فإن ضَبْط تطبيق معين بحيث يَستخدِم احدى تلك الحَاوِيات ليس مجرد موضوع واحد، وإنما عدة مواضيع؛ حيث ستختلف التَعلِيمات والأمثلة من حَاوِي لآخر، ولكنه إجراء غير مُعقَّد على الإطلاق. ترجمة -وبتصرف- للفصل Dependency Injection من كتاب .NET Framework Notes for Professionals
-
تُسهِل بيئة عمل .NET من البرمجة مُتعددة الأنوية (multi-core programming) من خلال توفير مَكتبة تَوازِي المَهامّ Task Parallel Library، حيث تَسمَح لك المكتبة بكتابة شيفرة -بالإضافة إلى كَوْنها مَقْرُوءة- فهي تُؤقلم نفسها مع العدد المُتاح من الأنوية (cores)، مما يَضمَن الترقية التلقائية للشيفرة وتحسين أدائها مع ترقية البيئة (environment). توازي البيانات (Data parallelism) تَوازِي البيانات (Data parallelism) هو التَّنفيذ المُتواقِت (concurrent) لنفس ذات العملية على عدة عناصر مُخزَّنة إِمّا بتَجمِيعة أو بمصفوفة. يُوفِّر إطار العمل .NET كلًا من البنائين البرمجيين (constructs) أو بشكل أدق التابعين Parallel.For و Parallel.Foreach لتَّنْفيذ حلقة (loop) مُتواقِتة من خلال تجزئة البيانات إلى مجموعات تُعالَج بنفس ذات الوقت. تُعدّ كلًا منهما النُسخة المُتواقِتة من الحلقة (loop) باستخدام for و foreach على الترتيب. مثلًا انظر المثال التالي: // النسخة المتتالية foreach (var item in sourcecollection){ Process(item); } // النسخة المتواقتة Parallel.Foreach(sourcecollection, item => Process(item)); صُمم التابع Parallel.ForEach بطريقة تَسمَح له -إن أمكن- باِستخدَام أكثر مِن نواة (core) أثناء التَّنْفيذ مما يُحسِن من أداء الشيفرة. استخدام التابع Parallel.For يُنفِّذ البناء البرمجي أو التابع Parallel.For حلقة (loop) مُتواقِتة. تتوافر عدة بصمات من هذا التابع تَستقبِل جميعها القيمة الأولية والنهائية للحلقة كأول مُعامِلين. تختلف البصمات بعد ذلك في عدد ونوع المُعامِلات لكن بالأخير لابُدّ لهن جميعًا من اِستقبَال مُفوَّض مَتن الحلقة من النوع Delegate، والذي يَحوِي العبارات البرمجية المطلوب تَّنْفيذها. على سبيل المثال، تَستقبِل البصمة، في المثال التالي، مُفوَّض مَتن الحلقة كمُعامِل رابع. تُمرَّر قيمة الحلقة الحالية (counter) لهذا المُفوَّض كأول مُعامِل له. ملحوظة: تتوفَّر بصمات مُبسطة من التابع Parallel.For تَقتصِر على المُعامِلات الثلاثة المذكورة بالأعلى والتي تُمثِل الأساس الجوهري لأيّ حلقة (loop) سواء كانت مُتواقِتة أم لا. يُنشِئ التابع Parallel.For عدة خيوط (threads) تُنفَّذ بنفس ذات الوقت. عادةً ما يحتاج كل خيط (thread) منها لمُتغيّر محلي (local) خاص يَتوَافر طوال فترة حياة (lifetime) الخيط. تَسمَح البصمة في المثال التالي بذلك، حيث يُستخدَم مُعامِلها الثالث لتهيئة القيمة المبدئية لذلك المُتغيّر. يُمرَّر هذا المتغير كمُعامِل ثالث إلى مُفوَّض مَتن الحلقة، بحيث يَستطيع التعديل من قيمته وإعادته لتَستقبِله الحلقة التالية ضِمْن نفس الخيط. تستمر العملية حتى ينتهي تَّنْفيذ جميع الحلقات ضِمْن نفس الخيط، وعندها تُمرَّر القيمة النهائية للمُتغيّر إلى مُفوَّض أخير (Delegate) من النوع Action، والذي يُنفَّذ مرة واحدة فقط قبل انتهاء فترة حياة الخيط. هذا المفوَّض الأخير هو المُعامِل الخامس للبصمة في المثال التالي. في المثال التالي، يُحسَب مجموع الأعداد من 1 إلى 10000 بصورة مُتواقِتة. بعد انتهاء كل خيط من تَّنْفيذ حلقاته، تُضاف القيمة النهائية للمُتغيّر المحلي الخاص بالخيط localSum إلى القيمة الكلية الفعلّية total باِستخدَام التابع Interlocked.Add وذلك لضَمان الأمن الخيطي (thread-safety). using System.Threading; int Foo() { int total = 0; Parallel.For(1, 10001, () => 0, // القيمة المبدئية, (num, state, localSum) => num + localSum, localSum => Interlocked.Add(ref total, localSum)); return total; } استخدام التابع Parallel.ForEach كالمثال السابق، يُنفِّذ التابع Parallel.ForEach حلقة (loop) مُتواقِتة، لكنه يُجريها على تَجمِيعَة بحيث تُقسَّم عناصر التَجمِيعَة وتُوزَّع على الخيوط المُنشئة. using System.Threading; int Foo() { int total = 0; var numbers = Enumerable.Range(1, 10000).ToList(); Parallel.ForEach(numbers, () => 0, // القيمة المبدئية (num, state, localSum) => num + localSum, localSum => Interlocked.Add(ref total, localSum)); return total; } تَتوفَّر بصمات أبسط من التابع Parallel.ForEach، كالمثال التالي: Parallel.Foreach(sourcecollection, item => Process(item)); يتوفَّر التابع Parallel.ForEach بلغة الفيجوال بيسك كالتالي: For Each row As DataRow In FooDataTable.Rows Me.RowsToProcess.Add(row) Next Dim myOptions As ParallelOptions = New ParallelOptions() myOptions.MaxDegreeOfParallelism = environment.processorcount Parallel.ForEach(RowsToProcess, myOptions, Sub(currentRow, state) ProcessRowParallel(currentRow, state) End Sub) توازي المهام (Task parallelism) تَوازِي المَهامّ (Task parallelism) هو التَّنْفيذ المُتواقِت (concurrent) لعمليات مُنفصلة. تَعتمِد مكتبة تَوازِي المَهامّ (Task Parallel Library) بشكل رئيسي على مفهوم المَهامّ (tasks)، والتي يُمكن مُوازنتِها بالخيوط (threads) ولكن على مُستوى أعلى من التجريد (abstraction). تَكون المَهامّ إمّا من النوع Task إذا كانت لا تُعيد قيمة أو من النوع Task<T> إذا كانت تُعيد قيمة من النوع T. استخدام التابع Parallel.Invoke يُعدّ التابع Parallel.Invoke أحد أبسط وسائل تَحقيق تَوازِي المَهامّ، حيث لا يَتطلَّب أي تَعامُل مباشر مع النوع Task. فقط مَرِّر إليه أيّ عدد من العمليات، كلًا منها من النوع Delegate<Action>، وسيُحوِّلها إلى عدة مَهامّ (tasks) ثم يُنفِّذها بصورة مُتواقِتة، كالمثال التالي: var actions = Enumerable.Range(1, 10).Select( n => new Action( () => { Console.WriteLine("I'm task " + n); if((n & 1) == 0) throw new Exception("Exception from task " + n); } )).ToArray(); try { Parallel.Invoke(actions); } catch(AggregateException ex) { foreach(var inner in ex.InnerExceptions) Console.WriteLine("Task failed: " + inner.Message); } لمزيد من التَحكُم ستحتاج إلى التَعامُل مع النوع Task مباشرة. تنشئة مُهِمّة Task باستخدام باني كائنات النوع Task يُمكنك إنشاء نُسخة من النوع Task مباشرة باِستخدَام باني الكائنات. فقط مَرِّر مُعامِلًا من النوع Delegate إليه يَحوِي العبارات البرمجية المُراد تَّنْفيذها خلال المُهِمّة. تحتاج لإستدعاء التابع Start اِستدعاءً صريحًا لبدء تَّنْفيذ المُهِمّة. كالتالي: var task = new Task( () => { Console.WriteLine("Task code starting..."); Thread.Sleep(2000); Console.WriteLine("...task code ending!"); }); Console.WriteLine("Starting task..."); task.Start(); task.Wait(); Console.WriteLine("Task completed!"); تنشئة مُهِمّة Task باستخدام التابع Task.Run بالمثل يَستقبِل التابع Task.Run مُعامِلًا من النوع Delegate يَحوِي العبارات البرمجية المُراد تَّنْفيذها خلال المُهِمّة، وينشئ مُهِمّة Task ويُنفِّذها في آن واحد. لذا، أنت لست في حاجة لاِستدعاء التابع Start. إذا كان الـ Delegate المُمرَّر من النوع Action، يُنشَئ مُتغيّر من النوع Task. كالتالي: Console.WriteLine("Starting task..."); var task = Task.Run( () => { Console.WriteLine("Task code starting..."); Thread.Sleep(2000); Console.WriteLine("...task code ending!"); }); task.Wait(); Console.WriteLine("Task completed!"); تنشئة مُهِمّة Task<T> باستخدام التابع Task.Run إذا كان الـ Delegate المُمرَّر من النوع Func<T>، تُنشَئ مُهمِة (task) من النوع Task<T>، بحيث تكون القيمة العائدة من المُهِمّة من النوع T. كالتالي: Task<int> t = Task.Run( () => { int sum = 0; for(int i = 0; i < 500; i++) sum += i; return sum; }); Console.WriteLine(t.Result); // 124750 كما رأينا بالمثال السابق، تُتيِح الخاصية Result بالنوع Task الوُلوج للقيمة العائدة من المُهِمّة. إذا كانت المُهِمّة تُنفَّذ بشكل غيْر مُتزامِن (asynchronous)، فإن اِستخدَام await لانتظار انتهائها من العَمَل يُعيد القيمة الفعلّية والتي تكون من النوع T، كالتالي: public async Task DoSomeWork() { WebClient client = new WebClient(); // نظرًا لاستخدام await تسند نتيجة المهمة إلى المتغير string response = await client.DownloadStringTaskAsync("http://somedomain.com"); } إلغاء مهمة باستخدام CancellationToken يُمكن إلغاء مُهِمّة أثناء تَّنْفيذها من خلال مُفتاح الإلغاء CancellationToken. تُنشِئ المُهِمّة الأساسية مُفتاح إلغاء (cancellation token) وتُمرِّره لإحدى مُهِمّاتها الفرعية أثناء تَنشِئتها من خلال المُعامِل state بباني كائنات النوع Task. وفي حالة أَرَادت إلغائها، يُمكِنها اِستدعاء التابع cancellationTokenSource.Cancel. في المقابل، ينبغي للمُهِمّة الفرعية أن تَستدعِي التابع ThrowIfCancellationRequested دَوريًّا لضَمان الاستجابة لطلب الإلغاء. كالمثال التالي: var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; var task = new Task( (state) => { int i = 1; var myCancellationToken = (CancellationToken)state; while(true) { Console.Write("{0} ", i++); Thread.Sleep(1000); myCancellationToken.ThrowIfCancellationRequested(); } }, cancellationToken: cancellationToken, state: cancellationToken); Console.WriteLine("Counting to infinity. Press any key to cancel!"); task.Start(); Console.ReadKey(); cancellationTokenSource.Cancel(); try { task.Wait(); } catch(AggregateException ex) { ex.Handle(inner => inner is OperationCanceledException); } Console.WriteLine($"{Environment.NewLine}You have cancelled! Task status is: {task.Status}"); //Canceled تستطيع المُهِمّة الفرعية أيضًا فحص وجود طلب الإلغاء باستخدام التابع IsCancellationRequested، والذي يُعيد قيمة منطقية. يُمكِنها بعد ذلك التبلّيغ عن اعتراض من النوع OperationCanceledException، كالمثال التالي: //New task delegate int i = 1; var myCancellationToken = (CancellationToken)state; while(!myCancellationToken.IsCancellationRequested) { Console.Write("{0} ", i++); Thread.Sleep(1000); } Console.WriteLine($"{Environment.NewLine}Ouch, I have been cancelled!!"); throw new OperationCanceledException(myCancellationToken); لاحظ أنه بالإضافة إلى تمرير مفتاح الإلغاء (cancellation token) للمُعامِل state بباني كائنات النوع Task، فقد مُرِّر مفتاح الإلغاء أيضًا للمُعامِل cancellationToken. يُعدّ ذلك ضروريًا لضَمان انتقال المُهِمّة إلى الحالة Canceled بدلًا من Faulted عند اِستدعاء التابع ThrowIfCancellationRequested. مُرِّر أيضًا مفتاح الإلغاء لمُعامِل باني كائنات النوع OperationCanceledException تمريرًا صريحًا لنفس السبب. ضمان منطقية السياق التنفيذي باستخدام النوع AsyncLocal يُمكن للمُهِمّة الأساسية (parent task) تمرير بعض البيانات إلى مَهامّها الفرعية (children tasks) باستخدام النوع AsyncLocal الذي يَسمَح بتَوفُّر نُسخة محلية (local) من تلك البيانات لكل مُهِمّة فرعية بشكل لا يُؤثر على البيانات الأصلية ضِمْن المُهِمّة الأساسية (parent task) مما يَضمَن منطقية السياق التَّنْفيذي. في المثال التالي: (1): لا يُؤثِر تَغيير قيمة المُتغيّر على بقية المَهامّ الفرعية؛ نظرًا لكَوْنه محلي (local). (2): تَدَفَّقت القيمة من التابع main إلى المُهِمّة task1 ومِنْ ثَمَّ إلى هذه المُهِمّة الفرعية دون أن تَتأثَر بأي تَغيّير قد يقع ضِمْن مُهمات اخرى. (3): بالمثل، تَدَفَّقت القيمة من التابع main إلى المُهِمّة task1 ومِنْ ثَمَّ إلى هذه المُهِمّة الفرعية، لكن في هذه الحالة حَدَث تعديل ضمن المُهِمّة task1 والتي تُعدّ ضِمْن سياقها التَّنْفيذي ولذلك تَأثَرت بالتَغيّير. void Main() { AsyncLocal<string> user = new AsyncLocal<string>(); user.Value = "initial user"; Task.Run(() => user.Value = "user from another task"); // (1) var task1 = Task.Run( () => { Console.WriteLine(user.Value); // "initial user" Task.Run( () => { // "initial user" Console.WriteLine(user.Value); // (2) }).Wait(); user.Value = "user from task1"; Task.Run( () => { // "user from task1" Console.WriteLine(user.Value); // (3) }).Wait(); }); task1.Wait(); // "initial user" Console.WriteLine(user.Value); } ملحوظة: المُمارسة المُثلى تكون بقَصر اَستخدَام النوع AsyncLocal على المُتغيّرات من نوع القيمة (value types) أو من النوع غيْر المُتغير (immutable)؛ لأن الخاصية AsynLocal.Value تَحمِل نُسخة من المُتغيّر، بالتالي إذا كان المُتغيّر من النوع المَرجِعي (reference type)، فإنها ستَحمِل مَرجِعًا إلى المُتغيّر الأصلي. يعني ذلك أن أي تَغْيير على قيمة AsynLocal.Value ضِمْن مُهِمّة فرعية معينة -في حالة النوع المَرجِعي- سينعكِس مباشرة على المُتغيّر الأصلى وتباعًا على قيمته ضِمْن المَهامّ الفرعية الأُخرى. التعامل مع تجميعة مهام يُوفِّر كلا من النوعين Task و Task<T> العديد من التوابع للتَعامُل مع تَجمِيعَة مَهامّ. ينتظر التابعين WaitAll و WhenAll انتهاء تَّنْفيذ جميع المَهامّ ضمن التَجمِيعَة. في المُقابل، ينتظر التابعين WhenAny و WaitAny انتهاء تَّنْفيذ مُهِمّة واحدة فقط. يَعمَل التابعين WaitAll و WaitAny بصورة مُتزامِنة (synchronous) مما يَتسبب بحُدوث تَعطِّيل (blocking). يُعدّ كلًا من التابعين WhenAll و WhenAny النسخة اللا مُتزامِنة وغيْر المُعَطِّلة (non-blocking/asynchronous) منهما على الترتيب ويُعيدان قيمة من النوع Task يُمكنك انتظار انتهائها من العَمَل باستخدَام await. استخدام التابع Task.WaitAll var tasks = Enumerable.Range(1, 5).Select( n => new Task<int>( () => { Console.WriteLine("I'm task " + n); return n; } )).ToArray(); foreach(var task in tasks) task.Start(); Task.WaitAll(tasks); foreach(var task in tasks) Console.WriteLine(task.Result); استخدام التابع WaitAny var allTasks = Enumerable.Range(1, 5).Select( n => new Task<int>(() => n) ).ToArray(); var pendingTasks = allTasks.ToArray(); foreach(var task in allTasks) task.Start(); while(pendingTasks.Length > 0) { var finishedTask = pendingTasks[Task.WaitAny(pendingTasks)]; Console.WriteLine("Task {0} finished", finishedTask.Result); pendingTasks = pendingTasks.Except(new[] {finishedTask}).ToArray(); } Task.WaitAll(allTasks); ملحوظة: يُعدّ استدعاء WaitAll ضروريًا نظرًا لأن WaitAny لا تُبلِّغ عن الاعتراضات. استخدام التابع WhenAll var random = new Random(); IEnumerable<Task<int>> tasks = Enumerable.Range(1, 5).Select( n => Task.Run( () => { Console.WriteLine("I'm task " + n); return n; })); Task<int[]> task = Task.WhenAll(tasks); int[] results = await task; Console.WriteLine(string.Join(",", results.Select(n => n.ToString()))); // Output: 1,2,3,4,5 استخدام التابع WhenAny var random = new Random(); IEnumerable<Task<int>> tasks = Enumerable.Range(1, 5).Select( n => Task.Run(async() => { Console.WriteLine("I'm task " + n); await Task.Delay(random.Next(10,1000)); return n; })); Task<Task<int>> whenAnyTask = Task.WhenAny(tasks); Task<int> completedTask = await whenAnyTask; Console.WriteLine("The winner is: task " + await completedTask); await Task.WhenAll(tasks); Console.WriteLine("All tasks finished!"); معالجة الاعتراضات (Exception Handling) استخدام التابع WaitAll يُبلِّغ التابع WaitAll عن اعتراض من النوع AggregateException إذا بَلَّغت مُهِمّة واحدة أو أكثر عن اعتراض. وبالتالي، تستطيع تَضْمِين العبارة البرمجية Task.WaitAll داخل كتلة try..catch، لتتمكن من مُعالجة الاعتراضات، كالمثال التالي: var task1 = Task.Run( () => { Console.WriteLine("Task 1 code starting..."); throw new Exception("Oh no, exception from task 1!!"); }); var task2 = Task.Run( () => { Console.WriteLine("Task 2 code starting..."); throw new Exception("Oh no, exception from task 2!!"); }); Console.WriteLine("Starting tasks..."); try { Task.WaitAll(task1, task2); } catch(AggregateException ex) { Console.WriteLine("Task(s) failed!"); foreach(var inner in ex.InnerExceptions) Console.WriteLine(inner.Message); } Console.WriteLine("Task 1 status is: " + task1.Status); //Faulted Console.WriteLine("Task 2 status is: " + task2.Status); //Faulted بدون استخدام Wait var task1 = Task.Run( () => { Console.WriteLine("Task 1 code starting..."); throw new Exception("Oh no, exception from task 1!!"); }); var task2 = Task.Run( () => { Console.WriteLine("Task 2 code starting..."); throw new Exception("Oh no, exception from task 2!!"); }); var tasks = new[] {task1, task2}; Console.WriteLine("Starting tasks..."); while(tasks.All(task => !task.IsCompleted)); foreach(var task in tasks) { if(task.IsFaulted) Console.WriteLine("Task failed: " + task.Exception.InnerExceptions.First().Message); } Console.WriteLine("Task 1 status is: " + task1.Status); //Faulted Console.WriteLine("Task 2 status is: " + task2.Status); //Faulted تدفق البيانات (Dataflow) تُسهِّل مكتبة تَوازِي المَهامّ لتدفق البيانات (TPL Dataflow Library) من البرمجة وفق نموذج تَدَفُّق البيانات (dataflow programming model). تُوفِّر المكتبة العديد من أنواع كتل تَدَفُّق البيانات (dataflow blocks) والتي يُعدّ كُلا منها هيكل بياني يُستخدَم إمّا كمصدر للبيانات (source) أو كمقصِد (target) أو كليهما (propagator). يُمكن أيضًا تقسيم أنواع كتل التَدَفُّق إلى كتل تَخزين مُؤقت (buffering) تَحمِل البيانات للمستهلكين، وكتل تَّنْفيذية (execution) تُنفِّذ مُفوَّض يُمرَّر إليها، وكتل تجميعية (grouping). بالإضافة إلى ذلك، تُوفِّر المكتبة العديد من التوابع لإرسال الرسائل واستقبالها من وإلى الكتل المختلفة، البعض منها مُتزامِن (synchronous) والبعض الآخر ليس كذلك. استخدام النوع BufferBlock لتنشئة متزامنة لنمط المُنتِج والمُستهلِك (producer-consumer pattern) تُصنَف الكتل من النوع BufferBlock ضِمْن كتل التخزين المؤقت (buffering)، وتَعمَل كمصدر ومَقصِد للبيانات (propagator). يُستخدَم التابعين Post و Receive لكتابة الرسائل (messages) إلى كتل التَدَفُّق واستقبالها منها على الترتيب بشكل مُتزامِن (synchronous). تُعرِّف الشيفرة التالية صنف المُنتِج: public class Producer { private static Random random = new Random((int)DateTime.UtcNow.Ticks); // انتج القيمة المُرسلة إلى كتلة التدفق public double Produce() { var value = random.NextDouble(); Console.WriteLine($"Producing value: {value}"); return value; } } تُعرِّف الشيفرة التالية صنف المُستهلك: public class Consumer { //consume the value that will be received from buffer block public void Consume (double value) => Console.WriteLine($"Consuming value: {value}"); } لاحظ كيف ضُمْنت كلًا من شيفرة المُنتِج (producer) والمُستهلِك (consumer) داخل مُهِمّتين مُنفصلتين بحيث يُصبِح من الممكن تَّنْفيذهما بصورة مُتواقِتة (concurrently)، كالتالي: class Program { private static BufferBlock<double> buffer = new BufferBlock<double>(); static void Main (string[] args) { // انشئ مهمة ترسل قيمة قيمة من المُنتج إلى كتلة التدفق كل ثانية var producerTask = Task.Run( async () => { var producer = new Producer(); while(true) { buffer.Post(producer.Produce()); await Task.Delay(1000); } }); // انشئ مهمة تستقبل القيم من كتلة التدفق var consumerTask = Task.Run( () => { var consumer = new Consumer(); while(true) { consumer.Consume(buffer.Receive()); } }); Task.WaitAll(new[] { producerTask, consumerTask }); } } استخدام النوع BufferBlock لتنشئة لا مُتزامِنة لنمط المُنتِج والمُستهلِك يُستخدَم التابعين SendAsync و ReceiveAsync لكتابة الرسائل (messages) إلى كتل التَدَفُّق واستقبالها منها على الترتيب بشكل لا مُتزامِن (asynchronous). var bufferBlock = new BufferBlock<int>( new DataflowBlockOptions { BoundedCapacity = 1000 }); var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; var producerTask = Task.Run( async () => { var random = new Random(); while (!cancellationToken.IsCancellationRequested) { var value = random.Next(); await bufferBlock.SendAsync(value, cancellationToken); } }); var consumerTask = Task.Run( async () => { while (await bufferBlock.OutputAvailableAsync()) { var value = bufferBlock.Receive(); Console.WriteLine(value); } }); await Task.WhenAll(producerTask, consumerTask); استخدام النوع BlockingCollection لتنشئة حلقة لنمط المنتج والمستهلك لا يَقَع النوع BlockingCollection ضِمْن مكتبة تَوازِي المَهامّ لتَدَفُّق البيانات (TPL Dataflow Library)، فهو مجرد تجميعة آمنة خيطيًا (thread-safe)، ويُوفِّر تَّنْفيذًا (implementation) لنمط المُنتِج والمُستهلِك (producer/consumer pattern)، لكنه لا يَتمتَع ببقية الميزات الأُخرى التي تُوفِّرها كتل التَدَفُّق. يُمكنك اِستخدَامه لحل المشاكل البسيطة التي لا تَتطلَّب خط أنابيب (pipeline) معقَّد. انظر المثال التالي: var consumerTask = Task.Run(() => { foreach(var item in collection.GetConsumingEnumerable()) { Console.WriteLine("Consumed: " + item); Thread.Sleep(random.Next(10,1000)); } Console.WriteLine("Consumer completed!"); }); var collection = new BlockingCollection<int>(5); var random = new Random(); var producerTask = Task.Run(() => { for(int item=1; item<=10; item++) { collection.Add(item); Console.WriteLine("Produced: " + item); Thread.Sleep(random.Next(10,1000)); } collection.CompleteAdding(); Console.WriteLine("Producer completed!"); }); ضُمنت كلًا من شيفرة المُنتِج (producer) والمُستهِلك (consumer) داخل مُهِمّتين منفصلتين بحيث يُصبِح من المُمكن تَّنْفيذهما بصورة مُتواقِتة (concurrently). Task.WaitAll(producerTask, consumerTask); Console.WriteLine("Everything completed!"); من الجدير بالذكر أنه في حالة عدم استدعاء التابع CompleteAdding، تستطيع مُتابعة إضافة عناصر للتَجمِيعَة حتى لو أصبح المُستهِلك قيْد التَّنْفيذ. اِستدعِي التابع collection.CompleteAdding() فقط عندما تكون متأكدًا أنك لم تَعدْ في حاجة لإضافة المزيد من العناصر. يُمكن اِستخدَام هذا النوع لتَّنْفيذ نمط عدة مُنتجِين لمُستهِلك وحيد، والذي يعنيّ وجود عدة مصادر للبيانات، تكون مسئولة عن تغذية التَجمِيعَة -من نفس النوع BlockingCollection- بالعناصر، وفي المقابل يَسَحَب مُستهلِك وحيد العناصر ويقوم بمُعالَجتِها بطريقة ما. لاحظ أنه في حالة عدم استدعاء التابع CompleteAdding وكانت التجميعة فارغة، فان المُعدَّد العائد من collection.GetConsumingEnumerable() سيتَسَبَّب بحُدوث تَعطّيل (blocking) إلى أن يُضاف عنصر جديد للتجميعة أو إلى أن يُستدعَى التابع BlockingCollection.CompleteAdding(). كتابة رسائل إلى كتلة تدفق من النوع ActionBlock تُصنَف الكتل من النوع ActionBlock ضِمْن الكتل التَّنْفيذية (execution) حيث تُنفِّذ مُفوَّض Delegate من النوع Action كلما استقبَلت بيانات جديدة، وتَعمَل كمقصِد للبيانات (target) فقط. انظر المثال التالي: // انشئ كتلة تدفق بحدث لا متزامن var block = new ActionBlock<string>( async hostName => { IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(hostName); Console.WriteLine(ipAddresses[0]); }); // ارسل البيانات إلى كتلة التدفق للمُعالجة block.Post("google.com"); block.Post("reddit.com"); block.Post("stackoverflow.com"); // بلغ كتلة التدفق بالتوقف عن استقبال عناصر جديدة block.Complete(); // انتظر حتي تنتهي كتلة التدفق من معالجة جميع العناصر await block.Completion; توصيل كتل التدفق لتنشئة خط أنابيب البيانات (data pipelines) يُستخدَم التابع LinkTo لتوصيل كتلتي تَدَفُّق (dataflow blocks) بحيث تَعمَل الأولى كمصدر للبيانات (source) والاخرى كمقصِد (target). يعني ذلك أنه بمجرد استقبَال الأولى لرسالة جديدة ومُعالَجتِها، فإنها ستُبلِّغ الكتل المتصلة بها بوجود رسالة جديدة. يُمكنك إنشاء خط أنابيب (pipeline) عن طريق توصيل عدة كتل تَدَفُّق ببعضها. كالمثال التالي: var httpClient = new HttpClient(); // أنشئ كتلة تدفق لاستقبال رابط وإعادة محتوياته كسِلسِلة نصية var downloaderBlock = new TransformBlock<string, string>( async uri => await httpClient.GetStringAsync(uri)); // انشئ كتلة تدفق لاستقبال سلسلة نصية وطباعتها على الطرفية var printerBlock = new ActionBlock<string>( contents => Console.WriteLine(contents)); // اجعل كتلة جلب محتويات الروابط مكتملة بمجرد اكتمال كتلة تدفق الطباعة var dataflowLinkOptions = new DataflowLinkOptions {PropagateCompletion = true}; // وصل كتلتي التدفق لإنشاء خط أنابيب downloaderBlock.LinkTo(printerBlock, dataflowLinkOptions); // أرسل عدة روابط لكتلة التدفق الأولى والتي ستُمرر محتوياتها إلى كتلة التدفق الثانية downloaderBlock.Post("http://youtube.com"); downloaderBlock.Post("http://github.com"); downloaderBlock.Post("http://twitter.com"); downloaderBlock.Complete(); // Completion will propagate to printerBlock await printerBlock.Completion; // Only need to wait for the last block in the pipeline السياق التزامني (Synchronization Contexts) يُعدّ السياق التزامني تمثيلًا تجريديًا (abstract representation) للبيئة التي تُنفَّذ بها الشيفرة. يُستخدَم عادةً عندما يُريد خيط ما تَّنْفيذ شيفرة ضِمْن خيط آخر. في هذه الحالة، يُلتقَط سياق الخيط المستهدف SynchronizationContext، ويُمرَّر للخيط الفرعي، الذي يستطيع -عند الحاجة- اِستخدَام التابع SynchronizationContext.Post لإرسال مُفوَّض (delegate) إلى سياق الخيط المُلتقَط لضَمان تَّنْفيذ المفوَّض ضِمْن ذلك الخيط. يكون ذلك بالأخص ضروريًا عند التَعامُل مع مُكوِنات الواجهة (UI component)، والتي لا يمكن تعديل خاصياتها إلا من خلال الخيط المالك الذي انشأها. الولوج إلى مُكِونات الواجهة (UI components) من خيوط أخرى إذا أردت تغيير خاصية مُكِون واجهة أو نموذج تَحكُم (form control)، مثل صندوق نصي (textbox) أو عنوان (label)، من خيط (thread) آخر غير خيط الواجهة الرسومية المُنشِئ لهذا المُكِون. فلابُدّ من اِستخدَام طرائق مُعينة غيْر التعديل المُباشر، وإلا ستحصل على رسالة الخطأ التالية: فمثلًا، ستؤدي الشيفرة التالية إلى التبلِّيغ عن اعتراض يَحمِل الرسالة السابقة: private void button4_Click(object sender, EventArgs e) { Thread thread = new Thread(updatetextbox); thread.Start(); } private void updatetextbox() { textBox1.Text = "updated"; } استخدام السياق التزامُني SynchronizationContext يمكن لخيط بالخلفية (background) تنفيذ شيفرة بخيط الواجهة (UI thread) لتَحْديث إحدى مُكوِنات الواجهة (UI component) من خلال استقبال السياق التزامني لخيط الواجهة من النوع SynchronizationContext، كالمثال التالي: void Button_Click(object sender, EventArgs args) { SynchronizationContext context = SynchronizationContext.Current; Task.Run( () => { for(int i = 0; i < 10; i++) { Thread.Sleep(500); context.Post(ShowProgress, "Work complete on item " + i); } } } void UpdateCallback(object state) { // يُستدعى هذا التابع من خلال خيط الواجهة فقط ولذلك تُحدث بنجاح this.MyTextBox.Text = state as string; } في المثال بالأعلى، إذا حَاول الخيط الفرعي تَحْديث خاصية مُكوِن الواجهة MyTextBox.Text داخل الحلقة for مباشرة، سيُبلَّغ عن خطأ خيطي (threading). أما إذا أرسل الحَدَث UpdateCallback إلى السياق التزامني SynchronizationContext الخاص بخيط الواجهة (UI thread)، ستتم عملية تَحْديث الصندوق النصي ضِمْن نفس خيط مُكوِنات الواجهة بنجاح. عمليًا، ينبغي أن تَستخدَم الواجهة System.IProgress لتَحْديث شريط التقدم (progress)؛ حيث يَلتقِط التَّنْفيذ الافتراضي لهذه الواجهة System.Progress سياق المُنشِئ التزامني (synchronisation context) تلقائيًا. استخدام التابعين Control.Invoke أو Control.BeginInvoke حل آخر هو أن تَستخدِم أحد التابعين Control.Invoke أو Control.BeginInvoke لتَغْيير مُحتوى مُكِون الواجهة -صندوق نصي مثلًا- من خيط آخر غير الخيط المالك لنموذج التحكم (form control). علاوة على ذلك، يُمكنك الاستعانة بالخاصية Control.InvokeRequired لفحص ما إذا كان استدعاء نموذج التحكم ضروريًا أم لا، كالتالي: private void updatetextbox() { if (textBox1.InvokeRequired) textBox1.BeginInvoke((Action)(() => textBox1.Text = "updated")); else textBox1.Text = "updated"; } إذا كنت ستَحتَاج لتَّنْفيذ ذلك بشكل مُتكرر، فربما من الأفضل تَضْمِين هذه الشيفرة داخل دالة مُوسِعَة (extension) للأنواع القابلة للاستدعاء (invokeable)، كالتالي: public static class Extensions { public static void BeginInvokeIfRequired(this ISynchronizeInvoke obj, Action action) { if (obj.InvokeRequired) obj.BeginInvoke(action, new object[0]); else action(); } } وبالتالي يُصبِح تَحْديث صندوق نصي من أي خيط أمرًا يسيرًا، كالتالي: private void updatetextbox() { textBox1.BeginInvokeIfRequired(() => textBox1.Text = "updated"); } الفرق بين التابعين Control.BeginInvoke و Control.Invoke هو أن الأول يُنفَّذ بشكل غير مُتزامِن (asynchronous) أما الثاني فيُنفَّذ بشكل مُتزامِن (synchronous). يعني ذلك أنه إذا اُستخدِم التابع الأول Control.BeginInvoke، فإن الشيفرة المكتوبة بعد استدعاء هذا التابع ستُنفَّذ بغض النظر عن انتهاء المفوَّض المُمرَّر إلى التابع من عَمَله أم لا. في المقابل، لن تُنفَّذ الشيفرة المكتوبة بعد التابع الثاني Control.Invoke إلا بعد انتهاء المفوَّض من العَمَل. يؤدي ذلك إلى حُدوث تَعطّيل بالخيط الأساسي ويَتسبَّب ببطئ الشيفرة إلى حد كبير خاصةً إذا لجأت إلى استخدام هذا التابع بكثرة. ربما أيضًا قد يَتسبَّب بحُدوث قفل ميت (deadlock) في حالة انتظار خيط الواجهة (GUI thread) انتهاء الخيط الفرعي من عمله أو انتظار تحريره لمَورِد معين. استخدام الواجهة IProgress لاحظ أن التابع Report مُنفَّذ صراحةً (explicit implementation) ضِمْن الواجهة IProgress، وليس موجودًا ضِمْن النوع المُنفِّذ System.Progress. لذا إمّا أن تَستدعِي التابع من خلال الواجهة أو من خلال النوع لكن بعد أن تُحوّله لنوع الواجهة (casting)، كالمثال التالي: var p1 = new Progress<int>(); p1.Report(1); // خطأ تصريف IProgress<int> p2 = new Progress<int>(); p2.Report(2); // تُصرف بنجاح var p3 = new Progress<int>(); ((IProgress<int>)p3).Report(3); // تُصرف بنجاح مثال بسيط لتحديث التقدم يُمكن لعملية معينة أن تَستخدِم الواجهة IProgress لتَحْديث شريط التقدم لعملية أخرى عن طريق استقبال كائن من الواجهة IProgress، واستدعاء تابعها Report كلما أرادت تَحْديث التقدم، كالمثال التالي: void Main() { IProgress<int> p = new Progress<int>( progress => { Console.WriteLine("Running Step: {0}", progress); }); LongJob(p); } public void LongJob(IProgress<int> progress) { var max = 10; for (int i = 0; i < max; i++) { progress.Report(i); } } الخْرج: Running Step: 0 Running Step: 3 Running Step: 4 Running Step: 5 Running Step: 6 Running Step: 7 Running Step: 8 Running Step: 9 Running Step: 2 Running Step: 1 يُنفَّذ التابع IProgress.Report() بشكل لا مُتزامِن (asynchronously)، لذلك ربما لا تُطبَع الأرقام بنفس ترتيب الاستدعاء. بالتالي، يُعدّ غير مُلائم عندما يكون التَحْديث وِفق ترتيب الاستدعاء ضروريًا. ترجمة -وبتصرف- للفصول 38 - 39 - 41 - 42 - 43 - 44 - 31 من كتاب .NET Framework Notes for Professionals
-
القراءة من والكتابة إلى المجارى القياسية يُمثِل النوع Console كلًا من مَجْاري الخْرج والدخْل والخْطأ القياسية ببرامج الطرفية. الكتابة إلى مجرى الخرج القياسي Stdout يَستقبِل التابع السِلسِلة النصية Hello World ويُرسلها إلى مَجْرى الخْرج القياسي، كالتالي: using System; class Program { static void Main() { Console.WriteLine("Hello World"); } } تَتوفَّر بصمات أُخرى من التابع Console.WriteLine. يَستقبِل إحداها مُعامِل من النوع Object، ويُرسله أيضًا إلى مَجْرى الخْرج القياسي، لكن بعد اِستدعاء التابع ToString من خلاله. الكتابة إلى مجرى الخطأ القياسي StdErr var sourceFileName = "NonExistingFile"; try { System.IO.File.Copy(sourceFileName, "DestinationFile"); } catch (Exception e) { var stdErr = Console.Error; stdErr.WriteLine($"Failed to copy '{sourceFileName}': {e.Message}"); } قراءة مجرى الخطأ القياسي الخاص بعملية فرعية (child process) var errors = new System.Text.StringBuilder(); var process = new Process { StartInfo = new ProcessStartInfo { RedirectStandardError = true, FileName = "xcopy.exe", Arguments = "\"NonExistingFile\" \"DestinationFile\"", UseShellExecute = false }, }; process.ErrorDataReceived += (s, e) => errors.AppendLine(e.Data); process.Start(); process.BeginErrorReadLine(); process.WaitForExit(); if (errors.Length > 0) // something went wrong System.Console.Error.WriteLine($"Child process error: \r\n {errors}"); العمليات على الملفات يُوفر النوع File -الموجود بفضاء الاسم System.IO- العديد من التوابع الساكنة (static methods) لإجراء العمليات المختلفة على الملفات، كـقراءة ملف أو حفظ بيانات بملف إِمّا بالكتابة (write) وإِمّا بالإضافة فيه (append) وغيره. فحص وجود ملف يَستقبِل التابع File.Exists مُعامِلًا من النوع String يحتوي على مسار ملف مُعين (قد يكون المسار نسبيًا relative أو مُطلقًا absolute/fully-qualified)، ثم يُعيد قيمة منطقية (bool) تُحدد ما إذا كان الملف موجودًا أم لا، كالتالي: using System; using System.IO; public class Program { public static void Main() { string filePath = "somePath"; if(File.Exists(filePath)) { Console.WriteLine("Exists"); } else { Console.WriteLine("Does not exist"); } } } كأيّ تابع يُعيد قيمة منطقية، يُمكن استخدامه مع المُعامِل الشرطي الثلاثي :? (ternary)، كالتالي: Console.WriteLine(File.Exists(pathToFile) ? "Exists" : "Does not exist"); قراءة ملف قراءة ملف باستخدام النوع File يُوفر النوع File ثلاثة توابع لقراءة البيانات من ملف معين. تَستقبِل التوابع الثلاثة مسار الملف المطلوب قراءته، ولكنها تختلف في طريقة القراءة. التابع File.ReadAllText: يقرأ محتويات الملف بالكامل إلى مُتغيِّر من النوع string. التابع File.ReadAllLines: يقرأ محتويات الملف إلى مصفوفة من السَلاسِل النصية string[]، بحيث يُقابل كل عنصر منها سطرًا واحدًا من الملف. التابع File.ReadAllBytes: يُعامِل الملف بِعدّه مجموعة بايتات Bytes ويُعيد مصفوفة بايتات byte[]. لاحظ الأمثلة التالية: string fileText = File.ReadAllText(file); string[] fileLines = File.ReadAllLines(file); byte[] fileBytes = File.ReadAllBytes(file); قراءة ملف باستخدام النوع StreamReader string fullOrRelativePath = "testfile.txt"; string fileData; using (var reader = new StreamReader(fullOrRelativePath)) { fileData = reader.ReadToEnd(); } كتابة ملف تختلف كتابة البيانات (writing) بملف عن إضافة بيانات إليه (appending) به. ففي الأولى، تُكتَب البيانات على محتويات الملف الحالية (overwrite)، أما في الثانية، تُضاف البيانات إلى نهاية الملف مما يعني الحفاظ على محتوياته الحالية. ملحوظة: تُنشِئ جميع التوابع المذكورة بالأسفل الملف بصورة آلية -إذا لم يَكُنْ موجودًا- قبل محاولة إلحاق/كتابة البيانات به. كتابة ملف باستخدام النوع File يُوفر النوع File ثلاثة توابع لكتابة البيانات (writing) بملف. التابع File.WriteAllText: يَستقبِل سِلسِلة نصية string ويكتبها بالملف. التابع File.WriteAllLines: يَستقبِل مصفوفة من السَلاسِل النصية string[] ويَكتِب كل عنصر بها إلى سطر منفصل بالملف. التابع File.WriteAllBytes: يَسمَح بكتابة مصفوفة بايتات byte[] بملف. لاحظ الأمثلة التالية بلغة C#: File.WriteAllText(file, "here is some data\nin this file."); File.WriteAllLines(file, new string[2] { "here is some data", "in this file" }); File.WriteAllBytes(file, new byte[2] { 0, 255 }); أو بلغة الفيجوال بيسك VB، كالتالي: using System.IO; using System.Text; string filename = "c:\path\to\file.txt"; File.writeAllText(filename, "Text to write\n"); كتابة ملف باستخدام النوع StreamWriter يُمكنك أيضًا الكتابة إلى ملف باستخدام كائن من النوع StreamWriter. عادةً ما يُنشَئ كائن المَجْرَى (stream) ضِمْن كتلة using، مما يَضمَن استدعاء التابع Dispose الذي يُفرّغ المَجْرَى ويُغلقه بمجرد الخروج من الكتلة. using System.Text; using System.IO; string filename = "c:\path\to\file.txt"; using (StreamWriter writer = new StreamWriter(filename)) { writer.WriteLine("Text to Write\n"); } بالمثل بلغة الفيجوال بيسك VB، يُمكنك الكتابة إلى الملفات باستخدام كائن من النوع StreamWriter، كالتالي: Dim filename As String = "c:\path\to\file.txt" If System.IO.File.Exists(filename) Then Dim writer As New System.IO.StreamWriter(filename) writer.Write("Text to write" & vbCrLf) 'Add a newline writer.close() End If إضافة بيانات إلى ملف يُوفر النوع File ثلاثة توابع لإضافة بيانات إلى ملف (appending). التابع File.AppendAllText: يَستقبِل سِلسِلة نصية string ويُلحِقها بنهاية الملف. التابع File.AppendAllLines: يَستقبِل مصفوفة من السَلاسِل النصية string[] ويُلحِق كل عنصر بها إلى الملف بـسطر منفصل. التابع File.AppendText: يَفتَح مَجْرَى (stream) من النوع StreamWriter. يُلحِق هذا المَجْرَى أي بيانات تُكتَب إليه بنهاية الملف. لاحظ الأمثلة التالية: File.AppendAllText(file, "Here is some data that is\nappended to the file."); File.AppendAllLines(file, new string[2] { "Here is some data that is", "appended to the file." }); using (StreamWriter stream = File.AppendText(file)) { stream.WriteLine("Here is some data that is"); stream.Write("appended to the file."); } حَذْف ملف يَحذِف التابع File.Delete الملف في حالة تَوفُّر الصلاحيات المطلوبة، كالمثال التالي: File.Delete(path); ومع ذلك، قد يَحدُث خطأ أثناء تنفيذ التَعلِيمة البرمجية بالأعلى، ربما لأحد الأسباب التالية: في حالة عدم تَوفُّر الصلاحيات المطلوبة، ويُبلَّغ عن اعتراض من نوع UnauthorizedAccessException. إذا كان الملف قَيْد الاستخدام أثناء محاولة الحذف، ويُبلَّغ عن اعتراض من نوع IOException. في حالة حُدوث خطأ مُنخفِض المستوى (low level) أو كان الملف مُعدّ للقراءة فقط، ويُبلَّغ عن اعتراض من نوع IOException. إذا لم يَكن الملف مَوجودًا، ويُبلَّغ عن اعتراض من نوع IOException. عادةً ما يتم التحايل على المشكلة الأخيرة باستخدام الشيفرة التالية: if (File.Exists(path)) File.Delete(path); مع ذلك فإن هذه الطريقة غير مَضْمُونَة؛ لأنها ليست عملية ذرية (atomic) حيث تَتِم على خُطوتين، فمن الممكن أن يُحذَف الملف -بواسطة عملية أخرى- بَعْد اجتياز عملية الفحص بنجاح وقبل محاولة إجراء الحَذْف الفعلي. لذلك فإن الطريقة الأصح للتعامُل مع عمليات الخْرج والدخْل (I/O) لابد وأن تتضمَّن معالجة للاعتراضات (exception handling)، والتي تعني اتخاذ مسار آخر من الأحداث في حالة فَشَل العملية، كالتالي: if (File.Exists(path)) { try { File.Delete(path); } catch (IOException exception) { if (!File.Exists(path)) return; // قام شخص آخر بحذف الملف } catch (UnauthorizedAccessException exception) { // لا تتوفر الصلاحيات المطلوبة } } لاحظ أنه أحيانًا ما تكون أخطاء الخْرج والدخْل (I/O) مؤقتة (مثلًا قد يكون الملف قَيْد الاستخدام)، وفي حالة استخدام الاتصال الشبكي، قد تُحَلّ المشكلة آليًا دون الحاجة لأيّ إجراء. لذا فمن الشائع إعادة محاولة إجراء عمليات الدخْل والخْرج (I/O) عدة مرات مع الانتظار لمُهلة قصيرة بين كل محاولة وأخرى، كالمثال التالي: public static void Delete(string path) { if (!File.Exists(path)) return; for (int i=1; ; ++i) { try { File.Delete(path); return; } catch (IOException e) { if (!File.Exists(path)) return; if (i == NumberOfAttempts) throw; Thread.Sleep(DelayBetweenEachAttempt); } // (*) } } private const int NumberOfAttempts = 3; private const int DelayBetweenEachAttempt = 1000; // ms إذا كنت تَستخدِّم نظام التشغيل Windows وكان ملف ما مفتوحًا بوَضْع FileShare.Delete، ثم حاولت حَذْف ذلك الملف باستخدام التابع File.Delete، فستُعدّ عملية الحَذْف مقبولة، ولكن في الواقع لن يُحذَف الملف إلا بَعْدَما يُغلق. (*): // ربما تقوم بنفس الشئ مع الاعتراض UnauthorizedAccessException مع أنه ليس من المحتمل حل مثل هذه المشكلة خلال ثوان. File.WriteAllLines( path, File.ReadAllLines(path).Where(x => !String.IsNullOrWhiteSpace(x))); نَقْل ملف من مسار إلى آخر يَستقبِل التابع File.Move مُعامِلين هما ملف المصدر (source) يُحدد الملف المُراد نَقْله، وملف المَقصِد (destination) يُحدد المسار المطلوب نَقْل ملف المصدر إليه. كالمثال التالي: File.Move(@"C:\TemporaryFile.txt", @"C:\TemporaryFiles\TemporaryFile.txt"); مع ذلك، قد يَحدُث خطأ أثناء تنفيذ التعليمة البرمجية بالأعلى. على سبيل المثال: ماذا لو كان مُستخدِم البرنامج لا يَملِك قُرْص بعنوان C؟ أو كان يَملِكه، ولكنه غَيّر عنوانه إلى B أو M؟ ماذا لو كان ملف المصدر (source) قد نُقل بدون عِلمك؟ أو لم يكن موجودًا من الأساس؟ يُمكن التحايل على تلك المشاكل بالتأكد من وجود ملف المصدر أولًا قبل محاولة نَقْله، كالتالي: string source = @"C:\TemporaryFile.txt", destination = @"C:\TemporaryFiles\TemporaryFile.txt"; if(File.Exists("C:\TemporaryFile.txt")) { File.Move(source, destination); } بهذه الطريقة سـنتأكد من وجود ملف المصدر (source) في تلك اللحظة، مما يعني إمكانية نَقْله إلى مكان آخر. لاحظ أنه أحيانًا، قد لا تكون هذه الطريقة كافية. أما في حالة لم يَكن الملف موجودًا، قُم بعملية الفحص مُجددًا، وإذا فَشَلت، يُمكنك مُعالجة الاعتراض أو تَبلِّيغ المُستخْدِم بذلك. ملحوظة: يُعدّ الاعتراض FileNotFoundException واحدًا فقط ضِمْن عِدّة اعتراضات قد تواجهك. يَعرِض الجدول التالي الاعتراضات المُحتملة: | نوع الاعتراض | الوصف | | :-------------------------: | :----------------------------------------------------------: | | IOException | إذا كان ملف المقصد موجود بالفعل أو كان ملف المصدر (source) غير موجود | | ArgumentNullException | إذا كانت قيمة ملف المصدر (source) أو ملف المقصد (destination) فارغة | | ArgumentException | إذا كانت قيمة ملف المصدر (source) أو ملف المقصد (destination) فارغة أو تحتوي محارِف غير صالحة | | UnauthorizedAccessException | إذا لم تتَوفَّر الصلاحيات المطلوبة لإجراء العملية | | PathTooLongException | إذا كان ملف المصدر (source) أو ملف المقصد (destination) أو المسارات المحددة تتعدّى الطول المسموح به. | | DirectoryNotFoundException | إذا كان المجلد المُحدد غير موجود | | NotSupportedException | إذا كان مسار ملف المصدر (source) أو ملف المقصد (destination) أو أسماء تلك الملفات بصيغة غير صالحة | تَعدِّيد ملفات أقدم من مدة محددة يُعرِّف المقطع البرمجي التالي دالة مُساعدة (helpers) تحمل الاسم EnumerateAllFilesOlderThan. تُعدِّد هذه الدالة الملفات الأقدم من عُمر معين بالاعتماد على التابع Directory.EnumerateFiles. تُعدّ هذه الدالة مفيدة لحذْف ملفات التسجيل (log files) القديمة أو البيانات المُخزَّنة مؤقتًا (cached). static IEnumerable<string> EnumerateAllFilesOlderThan( TimeSpan maximumAge, string path, string searchPattern = "*.*", SearchOption options = SearchOption.TopDirectoryOnly) { DateTime oldestWriteTime = DateTime.Now - maximumAge; return Directory.EnumerateFiles(path, searchPattern, options) .Where(x => Directory.GetLastWriteTime(x) < oldestWriteTime); } يُمكن استدعائها كالتالي: var oldFiles = EnumerateAllFilesOlderThan(TimeSpan.FromDays(7), @"c:\log", "*.log"); تَستخْدِم الدالة المُساعِدّة بالأعلى التابع Directory.EnumerateFiles() بدلًا من التابع Directory.GetFiles(). لذا لن تحتاج أن تنتظر جَلْب جميع الملفات قبل البدء بمعالجتها. تَفحَّص الدالة المُساعِدّة توقيت آخِر كتابة (last write time)، ولكن يمكنك أيضًا الاعتماد على وقت الإنشاء (creation time) أو وقت آخر وصول (last access time) وقد يكون ذلك مُفيدًا لحذْف ملفات التخزين المؤقت غير المُستخدَمة. انتبه فقد يكون وقت الوصول غيْر مُفعّل. حذف سطور من ملف نصي لا يُعدّ التعديل على الملفات النصية أمرًا يسيرًا؛ لأنه لابُدّ من قراءة محتواها أولًا. إذا كان حجم الملف صغيرًا، فإن أسهل طريقة هي قراءة الملف بالكامل إلى الذاكرة (memory)، وإجراء التعديلات، ثم إعادة كتابة النص المُعدّل إلى الملف. في المثال التالي، قُرِأت كل سطور الملف، وحُذْف الفارغ منها، ثم كُتبت بملف بنفس المسار. File.WriteAllLines(path, File.ReadAllLines(path).Where(x => !String.IsNullOrWhiteSpace(x))); أما إذا كان الملف كبيرًا لتقرأه بالكامل إلى الذاكرة، فيُفضَّل استخدام التابع File.ReadLines الذي يُعيد معدَّد من السَلاسِل النصية IEnumerable<string>. بهذه الطريقة، يُمكنك البدء بتعدِّيد التَجمِيعَة ومُعالجتها بدون استرجاعها بالكامل، بِعَكْس التابع File.ReadAllLines الذي لابُدّ عند استخدامه من الانتظار حتي تُقرأ محتويات الملف بالكامل إلى مصفوفة string[]، وبعدها يُمكن البدء بعملية المُعالجة. لكن انتبه، في هذه الحالة، سيختلف مسار كلًا من مَلفّي الدخْل والخْرج. File.WriteAllLines(outputPath, File.ReadLines(inputPath).Where(x => !String.IsNullOrWhiteSpace(x))); تغيير ترميز ملف نصي تُخزَّن النصوص مرمَّزة (enocoded). أحيانًا قد ترغب بتَغْيير الترميز المُستخدَم، وعندها يُمكنك تمرير مُعامِل إضافي من النوع Encoding للتابع WriteAllText؛ لتخصيص الترميز المطلوب، كالمثال التالي: public static void ConvertEncoding(string path, Encoding from, Encoding to) { File.WriteAllText(path, File.ReadAllText(path, from), to); } ملحوظة: افترضنا أن الملف صغيرًا كفاية ليُقرأ بالكامل إلى الذاكرة بغرض التبسيط. قد يحتوي الملف على BOM. يُمكنك الاطلاع على "لا يأخذ التابع Encoding.UTF8.GetString بالحسبان Preamble/BOM" لفهم أعمق لكيفية التعامل معها. التعامل مع الملفات بامتداد Zip عَرْض قائمة محتويات ملف بامتداد Zip تقوم الشيفرة التالية بعَرْض قائمة بأسماء الملفات الموجودة بملف أرشيفي: using (FileStream fs = new FileStream("archive.zip", FileMode.Open)) using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read)) { for (int i = 0; i < archive.Entries.Count; i++) { Console.WriteLine($"{i}: {archive.Entries[i]}"); } } لاحظ أن أسماء الملفات تكون نسبية (relative) وفقًا للملف الأرشيفي. استخراج (Extracting) محتويات ملف بامتداد Zip يَستخرِج التابع ExtractToDirectory الملفات الموجودة ضِمْن ملف أرشيفي كالتالي: using (FileStream fs = new FileStream("archive.zip", FileMode.Open)) using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read)) { archive.ExtractToDirectory( AppDomain.CurrentDomain.BaseDirectory); } سيُبلَّغ عن اعتراض من النوع System.IO.IOException إذا كان المجلد المُستخرَج إليه يحتوي على ملف يحمل نفس الاسم. يُمكنك اِستِخراج ملفات بعينها. فمثلًا، يُستخدَم التابع GetEntry لاختيار ملف معين عن طريق اسمه تمهيدًا لاستخراجه باستخدام التابع ExtractToFile. يُمكنك أيضًا الولوج للخاصية Entries والبحث فيها عن ملف يُحقق شرطًا معينًا، كالتالي: using (FileStream fs = new FileStream("archive.zip", FileMode.Open)) using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read)) { // اختر ملف بالمجلد الرئيسي archive.GetEntry("test.txt") .ExtractToFile("test_extracted_getentries.txt", true); // أضف مسارًا إذا كنت تريد استهداف ملف بـمجلد فرعي archive.GetEntry("sub/subtest.txt") .ExtractToFile("test_sub.txt", true); archive.Entries .FirstOrDefault(f => f.Name == "test.txt")? .ExtractToFile("test_extracted_linq.txt", true); } إذا اخترت اسم ملف غير موجود، سُيبلَّغ عن اعتراض من النوع System.ArgumentNullException، كالتالي: using (FileStream fs = new FileStream("archive.zip", FileMode.Open)) using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read)) { archive.GetEntry("nonexistingfile.txt") .ExtractToFile("fail.txt", true); } تحديث ملف بامتداد Zip لتحديث ملف بامتداد zip، يجب فتح الملف باستخدام الوَضْع ZipArchiveMode.Update، حتى تستطيع إضافة ملفات جديدة إليه. يُستخدَم التابع CreateEntryFromFile لإضافة ملف للمجلد الرئيسي أو لمجلد فرعي، كالتالي: using (FileStream fs = new FileStream("archive.zip", FileMode.Open)) using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Update)) { // أضف ملف إلى الأرشيف archive.CreateEntryFromFile("test.txt", "test.txt"); // أضيف ملف إلى مجلد فرعي بالملف الأرشيفي archive.CreateEntryFromFile("test.txt", "symbols/test.txt"); } يُمكن أيضًا الكتابة مباشرة بملف داخل الأرشيف باستخدام التابع CreateEntry، كالتالي: var entry = archive.CreateEntry("createentry.txt"); using(var writer = new StreamWriter(entry.Open())) { writer.WriteLine("Test line"); } القراءة من والكتابة إلى المَنافِذ التَسلسُليّة (Serial Ports) يُوفِّر اطار عمل .NET النوع SerialPort بفضاء الاسم System.IO.Ports للاتصالات التَسلسُليّة (serial communication). عرض قائمة بأسماء المَنافِذ المتاحة يُعيدّ التابع SerialPort.GetPortNames() قائمة بأسماء المَنافِذ المتاحة، كالتالي: using System.IO.Ports; string[] ports = SerialPort.GetPortNames(); for (int i = 0; i < ports.Length; i++) { Console.WriteLine(ports[i]); } تنشئة كائن من النوع SerialPort يُمثِل كائن من النوع SerialPort مَنفَذ تَسلسُليّ مُعين، ويُستخدَم لإرسال الرسائل النصية واستقبالها عبْر ذلك المَنفَذ. تُنشِئ الشيفرة التالية كائنات من النوع SerialPort باستخدَام بَوانِي الكائن: using System.IO.Ports; SerialPort port = new SerialPort(); SerialPort port = new SerialPort("COM 1"); ; SerialPort port = new SerialPort("COM 1", 9600); قراءة وكتابة البيانات من وإلى المَنافِذ التَسلسُليّة يُعدّ اِستخدَام التابعين SerialPort.Read و SerialPort.Write من أسهل الطرائق للقراءة من والكتابة إلى المَنافِذ التَسلسُليّة على الترتيب، كالتالي: int length = port.BytesToRead; byte[] buffer = new byte[length]; port.Read(buffer, 0, length); port.Write("here is some text"); byte[] data = new byte[1] { 255 }; port.Write(data, 0, data.Length); يُمكِن قراءة جميع البيانات المُتوفِّرة باستخدام التابع ReadExisting: string curData = port.ReadExisting(); أو قراءة السطر التالي باستخدام التابع ReadLine: string line = port.ReadLine(); مثال آخر: var serialPort = new SerialPort("COM1", 9600, Parity.Even, 8, StopBits.One); serialPort.Open(); serialPort.WriteLine("Test data"); string response = serialPort.ReadLine(); Console.WriteLine(response); serialPort.Close(); يُمكن أيضًا استخدام التابع SerialPort.BaseStream لتنشئة مَجْرى من النوع System.IO.Stream، واِستخدَامه لكتابة البيانات إلى المَنفَذ. خدمة صَدَى نصي مُتزامِنة (synchronous) يُستخدَم التابع ReadLine لقراءة سطر من مَنفَذ من النوع SerialPort بشكل مُتزامِن (synchronous) مما يعني التَسبُّب بتعطيل (blocking) في حالة عدم توفُّر سطر للقراءة. تَستعرِض الشيفرة التالية خِدمة صدى نصي (text echo) بسيطة بحيث تَبقَى الشيفرة في وَضْع اِستماع للمَنْفَذ إلى حين وصول سطر جديد. عندها يُقرأ السطر ويُعاد إرساله عبر نفس المَنْفَذ إلا في حالة كان يَحوِي السِلسِلة النصية quit. using System.IO.Ports; namespace TextEchoService { class Program { static void Main(string[] args) { var serialPort = new SerialPort("COM1", 9600, Parity.Even, 8, StopBits.One); serialPort.Open(); string message = ""; while (message != "quit") { message = serialPort.ReadLine(); serialPort.WriteLine(message); } serialPort.Close(); } } } قراءة غير متزامنة (asynchronous) يُوفِّر النوع SerialPort مجموعة من الأحداث (events) مما يَسمَح بكتابة شيفرة غير مُتزامِنة (asynchronous). مثلا، يُثار الحَدَث DataReceived عند استقبال بيانات عبر المَنفَذ (port). وبالتالي، يُمكن لمُعالِج حَدَث (event handler) التسجيل (subscribe) بالحَدَث المذكور وقراءة البيانات المُستلَمة بطريقة غير مُتزامِنة، كالمثال التالي: void SetupAsyncRead(SerialPort serialPort) { serialPort.DataReceived += (sender, e) => { byte[] buffer = new byte[4096]; switch (e.EventType) { case SerialData.Chars: var port = (SerialPort)sender; int bytesToRead = port.BytesToRead; if (bytesToRead > buffer.Length) Array.Resize(ref buffer, bytesToRead); int bytesRead = port.Read(buffer, 0, bytesToRead); // يمكنك معالجة البيانات المقروءة هنا break; case SerialData.Eof: // انهي العملية هنا break; } }; } مُستقبِل رسائل غير مُتزامِن (asynchronous) تَعرِض الشيفرة التالية مُستقبِل رسائل مَبني على المثال السابق: using System; using System.Collections.Generic; using System.IO.Ports; using System.Text; using System.Threading; namespace AsyncReceiver { class Program { const byte STX = 0x02; const byte ETX = 0x03; const byte ACK = 0x06; const byte NAK = 0x15; static ManualResetEvent terminateService = new ManualResetEvent(false); static readonly object eventLock = new object(); static List<byte> unprocessedBuffer = null; static void Main(string[] args) { try { var serialPort = new SerialPort("COM11", 9600, Parity.Even, 8, StopBits.One); serialPort.DataReceived += DataReceivedHandler; serialPort.ErrorReceived += ErrorReceivedHandler; serialPort.Open(); terminateService.WaitOne(); serialPort.Close(); } catch (Exception e) { Console.WriteLine("Exception occurred: {0}", e.Message); } Console.ReadKey(); } static void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { lock (eventLock) { byte[] buffer = new byte[4096]; switch (e.EventType) { case SerialData.Chars: var port = (SerialPort)sender; int bytesToRead = port.BytesToRead; if (bytesToRead > buffer.Length) Array.Resize(ref buffer, bytesToRead); int bytesRead = port.Read(buffer, 0, bytesToRead); ProcessBuffer(buffer, bytesRead); break; case SerialData.Eof: terminateService.Set(); break; } } } static void ErrorReceivedHandler(object sender, SerialErrorReceivedEventArgs e) { lock (eventLock) if (e.EventType == SerialError.TXFull) { Console.WriteLine("Error: TXFull. Can't handle this!"); terminateService.Set(); } else { Console.WriteLine("Error: {0}. Resetting everything", e.EventType); var port = (SerialPort)sender; port.DiscardInBuffer(); port.DiscardOutBuffer(); unprocessedBuffer = null; port.Write(new byte[] { NAK }, 0, 1); } } static void ProcessBuffer(byte[] buffer, int length) { List<byte> message = unprocessedBuffer; for (int i = 0; i < length; i++) if (buffer[i] == ETX) { if (message != null) { Console.WriteLine("MessageReceived: {0}", Encoding.ASCII.GetString(message.ToArray())); message = null; } } else if (buffer[i] == STX) message = null; else if (message != null) message.Add(buffer[i]); unprocessedBuffer = message; } } } ينتظر البرنامج بالأعلى الرسائل المُضمَّنة بين بايتات STX و ETX، ثم يُرسِل النص الفعلّي إلى مَجْرى الخْرج، أي شئ آخر غيْر ذلك يتم إهماله. يُوقَف البرنامج في حالة حدوث طفحان بالمَخزَن المؤقت (buffer overflow) باستخدام التابع Set. أما في حالة حدوث أي أخطاء أُخرى، يُفرَّغ مَخزَني الدخْل والخْرج المؤقتين باستخدام التابعين DiscardInBuffer و DiscardOutBuffer، ثم تُنتظَر أي رسائل أُخرى. تُوضِح الشيفرة بالأعلى النقاط التالية: قراءة مَنفَذ تَسلسُليّ بشكل غير مُتزامِن عن طريق التسجيل بالحَدَث SerialPort.DataReceived. مُعالجة أخطاء مَنفَذ تَسلسُليّ عن طريق التسجيل بالحَدَث SerialPort.ErrorReceived. تَّنفيذ لبروتوكول مَبنى على الرسائل غير النصية. قراءة جزئية للرسائل: قد يحدث ذلك؛ ربما لأن الحَدَث SerialPort.DataReceived قد أُثير قبل استلام المَنْفَذ للرسالة بأكملها (وفقًا لـETX)، أو ربما لأن التابع SerialPort.Read(..., ..., port.BytesToRead) لم يقرأ الرسالة بأكملها بل قرأ فقط جزءً منها، مما يترتب عليه عدم تَوفُّر كامل الرسالة بمَخزَن الدخْل المؤقت (input buffer). في هذه الحالة، يُنَحَّى الجزء المُستلَم غيْر المُعالَج (unprocessed) جانبًا لحين استلام بقية الرسالة. التَعامُل مع وصول أكثر من رسالة بدَفْعَة واحدة: قد يُثار الحَدَث SerialPort.DataReceived فقط بعدما يَستلِم عدة رسائل من الطرف الآخر. ترجمة -وبتصرف- للفصول 16-17-18-19-57-52 من كتاب .NET Framework Notes for Professionals
-
الاستعلامات التكميلية اللغوية (Language Integrated Query - LINQ) هي تَعبيرات برمجية (expressions)، تَجلْب بيانات مُعينة من مَصدر بيانات (data source). تُوفِر استعلامات LINQ نَموذج مُتجانِس لتسهيل التعامل مع البيانات من مُختَلَف أنواع وصِيغْ مصادر البيانات (data sources). عند استخدامك لاستعلامات LINQ، فأنت دومًا تتعامل مع كائنات (objects)، وتَستَخدِم نفس الأنماط البرمجية الأساسية لجَلْب وتَغيير البيانات سواء كانت مُخزَّنة بملفات XML، أو بقواعد البيانات SQL، أو تقنية ADO.NET أو تَجمِيعَات .NET، أو أيّ صيغْْة أخرى مُتاح لها مُوفِّر (provider). يُمكن استخدام LINQ بلغتي C# و VB. التابع SelectMany (ربط مسطح flat map) لكل عنصر دخْل، يُعِيد التابع Enumerable.Select عنصر خْرج وحيد مُناظِر. في المقابل، يُسمَح للتابع Enumerable.SelectMany بإعادة أيّ عدد مُناظِر من عناصر الخْرج. بالتالي، قد يكون عدد عناصر مُتتالِية الخْرج مِن التابع SelectMany غَيْر مُساو لعدد عناصر مُتتالية الدخْل. تُمرَّر دالة مُجردة (Lambda expression) لكِلا التابعين، وينبغي لتلك الدالة إعادة عدد من القيم بما يتوافق مع ما ذُكِر بالأعلى. مثال: class Invoice { public int Id { get; set; } } class Customer { public Invoice[] Invoices {get;set;} } var customers = new[] { new Customer { Invoices = new[] { new Invoice {Id=1}, new Invoice {Id=2}, } }, new Customer { Invoices = new[] { new Invoice {Id=3}, new Invoice {Id=4}, } }, new Customer { Invoices = new[] { new Invoice {Id=5}, new Invoice {Id=6}, } } }; var allInvoicesFromAllCustomers = customers.SelectMany(c => c.Invoices); Console.WriteLine( string.Join(",", allInvoicesFromAllCustomers.Select(i => i.Id).ToArray())); الخْرج: 1,2,3,4,5,6 مِثال حي يُمكن للاستعلام المُعتمِد على الصيغة (syntax-based query) تَنفيذ مُهمة التابع Enumerable.SelectMany باستخدام عِبارتي from متتاليتين كالتالي: var allInvoicesFromAllCustomers = from customer in customers from invoice in customer.Invoices select invoice; التابع Where (تَرشِيح filter) يُعيد التابع Where مُُعدَّد من الواجهة IEnumerable، والذي يَحوِي جميع العناصر التي تُحقِق شرط الدالة المُجردة (lambda expression) المُمرَّرة. مِثال: var personNames = new[] { "Foo", "Bar", "Fizz", "Buzz" }; var namesStartingWithF = personNames.Where(p => p.StartsWith("F")); Console.WriteLine(string.Join(",", namesStartingWithF)); الخْرج: Foo,Fizz مِثال حي التابع Any يُعيد التابع Any القيمة المنطقية true إذا حَوَت التَجمِيعَة أيّة عناصر تُحقِق شرط الدالة المُجردة (lambda expression) المُمرَّرة. انظر المثال التالي: var numbers = new[] {1,2,3,4,5}; var isNotEmpty = numbers.Any(); Console.WriteLine(isNotEmpty); //True var anyNumberIsOne = numbers.Any(n => n == 1); Console.WriteLine(anyNumberIsOne); //True var anyNumberIsSix = numbers.Any(n => n == 6); Console.WriteLine(anyNumberIsSix); //False var anyNumberIsOdd = numbers.Any(n => (n & 1) == 1); Console.WriteLine(anyNumberIsOdd); //True var anyNumberIsNegative = numbers.Any(n => n < 0); Console.WriteLine(anyNumberIsNegative); //False التابع GroupJoin class Developer { public int Id { get; set; } public string Name { get; set; } } class Project { public int DeveloperId { get; set; } public string Name { get; set; } } var developers = new[] { new Developer { Id = 1, Name = "Foobuzz" }, new Developer { Id = 2, Name = "Barfizz" } }; var projects = new[] { new Project { DeveloperId = 1, Name = "Hello World 3D" }, new Project { DeveloperId = 1, Name = "Super Fizzbuzz Maker" }, new Project { DeveloperId = 2, Name = "Citizen Kane - The action game" }, new Project { DeveloperId = 2, Name = "Pro Pong 2016" } }; var grouped = developers.GroupJoin( inner: projects, outerKeySelector: dev => dev.Id, innerKeySelector: proj => proj.DeveloperId, resultSelector: (dev, projs) => new { DeveloperName = dev.Name, ProjectNames = projs.Select(p => p.Name).ToArray()}); foreach(var item in grouped) { Console.WriteLine( "{0}'s projects: {1}", item.DeveloperName, string.Join(", ", item.ProjectNames)); } //Foobuzz's projects: Hello World 3D, Super Fizzbuzz Maker //Barfizz's projects: Citizen Kane - The action game, Pro Pong 2016 التابع Except var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var evenNumbersBetweenSixAndFourteen = new[] { 6, 8, 10, 12 }; var result = numbers.Except(evenNumbersBetweenSixAndFourteen); Console.WriteLine(string.Join(",", result)); //1, 2, 3, 4, 5, 7, 9 التابع Zip بِدءًا من الإصدار 4.0 أو أحدث. var tens = new[] {10,20,30,40,50}; var units = new[] {1,2,3,4,5}; var sums = tens.Zip(units, (first, second) => first + second); Console.WriteLine(string.Join(",", sums)); //11,22,33,44,55 التابع Aggregate (طوي fold) يَستقبِل التابع Aggregate المُعامِل seed، الذي يَحوِي قيمة التهيئة للمُراكِم (accumulator). في حالة تمرير سلسلة نصية كالمثال التالي، سيُنشَّئ كائن جديد مع كل خطوة أي بِعَدَد عناصر المُعدَّد. var elements = new[] {1,2,3,4,5}; var commaSeparatedElements = elements.Aggregate( seed: "", func: (aggregate, element) => $"{aggregate}{element},"); Console.WriteLine(commaSeparatedElements); //1,2,3,4,5, لاستخدام نفس الكائن بجميع الخطوات، مَرِّر كائنًا من النوع StringBuilder للمُعامِل seed كالتالي: var commaSeparatedElements2 = elements.Aggregate( seed: new StringBuilder(), func: (seed, element) => seed.Append($"{element},")); Console.WriteLine(commaSeparatedElements2.ToString()); //1,2,3,4,5, يَتحكَم المُعامِل resultSelector بصيغة الخْرج: var commaSeparatedElements3 = elements.Aggregate( seed: new StringBuilder(), func: (seed, element) => seed.Append($"{element},"), resultSelector: (seed) => seed.ToString()); Console.WriteLine(commaSeparatedElements3); //1,2,3,4,5, إذا لم تُمَرَّر قيمة للمُعامِل seed، تؤول قيمته بشكل افتراضي لأول عنصر بالمُعدَّد. var seedAndElements = elements.Select(n=>n.ToString()); var commaSeparatedElements4 = seedAndElements.Aggregate( func: (aggregate, element) => $"{aggregate}{element},"); Console.WriteLine(commaSeparatedElements4); //12,3,4,5, التابع ToLookup var persons = new[] { new { Name="Fizz", Job="Developer"}, new { Name="Buzz", Job="Developer"}, new { Name="Foo", Job="Astronaut"}, new { Name="Bar", Job="Astronaut"}, }; var groupedByJob = persons.ToLookup(p => p.Job); foreach(var theGroup in groupedByJob) { Console.WriteLine( "{0} are {1}s", string.Join(",", theGroup.Select(g => g.Name).ToArray()), theGroup.Key); } //Fizz,Buzz are Developers //Foo,Bar are Astronauts التابع Intersect var numbers1to10 = new[] {1,2,3,4,5,6,7,8,9,10}; var numbers5to15 = new[] {5,6,7,8,9,10,11,12,13,14,15}; var numbers5to10 = numbers1to10.Intersect(numbers5to15); Console.WriteLine(string.Join(",", numbers5to10)); //5,6,7,8,9,10 التابع Concat var numbers1to5 = new[] {1, 2, 3, 4, 5}; var numbers4to8 = new[] {4, 5, 6, 7, 8}; var numbers1to8 = numbers1to5.Concat(numbers4to8); Console.WriteLine(string.Join(",", numbers1to8)); //1,2,3,4,5,4,5,6,7,8 يحتفظ التابع Concat بالعناصر المُكرّرة دون حَذفِها. اِستخدِم التابع Union إذا لم يكن ذلك ما ترغب به. التابع All var numbers = new[] {1,2,3,4,5}; var allNumbersAreOdd = numbers.All(n => (n & 1) == 1); Console.WriteLine(allNumbersAreOdd); //False var allNumbersArePositive = numbers.All(n => n > 0); Console.WriteLine(allNumbersArePositive); //True يُعيِد التابع All القيمة المنطقية false فقط عندما يجد عنصرًا بالمُعدَّد لا يُحقق شرط الدالة المُجردة (lambda expression). بالتالي، إذا كان المُعدَّد فارغًا، سيُعيد التابع القيمة المنطقية true دومًا بغض النظر عن شرط الدالة المجردة، كالمثال التالي: var numbers = new int[0]; var allNumbersArePositive = numbers.All(n => n > 0); Console.WriteLine(allNumbersArePositive); //True التابع Sum var numbers = new[] {1,2,3,4}; var sumOfAllNumbers = numbers.Sum(); Console.WriteLine(sumOfAllNumbers); //10 var cities = new[] { new {Population = 1000}, new {Population = 2500}, new {Population = 4000} }; var totalPopulation = cities.Sum(c => c.Population); Console.WriteLine(totalPopulation); //7500 التابع SequenceEqual var numbers = new[] {1,2,3,4,5}; var sameNumbers = new[] {1,2,3,4,5}; var sameNumbersInDifferentOrder = new[] {5,1,4,2,3}; var equalIfSameOrder = numbers.SequenceEqual(sameNumbers); Console.WriteLine(equalIfSameOrder); //True var equalIfDifferentOrder = numbers.SequenceEqual(sameNumbersInDifferentOrder); Console.WriteLine(equalIfDifferentOrder); //False التابع Min var numbers = new[] {1,2,3,4}; var minNumber = numbers.Min(); Console.WriteLine(minNumber); //1 var cities = new[] { new {Population = 1000}, new {Population = 2500}, new {Population = 4000} }; var minPopulation = cities.Min(c => c.Population); Console.WriteLine(minPopulation); //1000 التابع Distinct var numbers = new[] {1, 1, 2, 2, 3, 3, 4, 4, 5, 5}; var distinctNumbers = numbers.Distinct(); Console.WriteLine(string.Join(",", distinctNumbers)); //1,2,3,4,5 التابع Count IEnumerable<int> numbers = new[] {1,2,3,4,5,6,7,8,9,10}; var numbersCount = numbers.Count(); Console.WriteLine(numbersCount); //10 var evenNumbersCount = numbers.Count(n => (n & 1) == 0); Console.WriteLine(evenNumbersCount); //5 التابع Cast يَختلف التابع Cast عن بقية توابع النوع Enumerable من جِهة كَوْنه تابعًا مُوسِّعًا للواجهة IEnumerable لا نظيرتها المُعمَّمة IEnumerable<T>. بالتالي، قد يُستَخَدم لتحويل نُسخ من الواجهة غير المُعمَّمة لآخرى مُعمَّمة. فمثلًا، لا يجتاز المثال التالي عملية التَصرِّيف (compilation)؛ نظرًا لأن التابع First -كغالبية توابع النوع Enumerable- هو تابِع موسِّع للواجهة IEnumerable<T>، ولمّا كان النوع ArrayList لا يُنَفِذ تلك الوَاجِهة وإنما يُنَفِذ الواجهة غيْر المُعمَّمة IEnumerable، تَفشل عملية التَصرِّيف. var numbers = new ArrayList() {1,2,3,4,5}; Console.WriteLine(numbers.First()); في المُقابِل، يَعمل المثال التالي بشكل سليم: var numbers = new ArrayList() {1,2,3,4,5}; Console.WriteLine(numbers.Cast<int>().First()); //1 لا يُجرِي التابع Cast عملية التحويل بين اﻷنواع (Casting). فمثلًا، في حين تجتاز الشيفرة التالية عملية التَصرِّيف (compilation)، يُبلَّغ عن اعتراض من النوع InvalidCastException خلال زمن التشغيل (runtime). var numbers = new int[] {1,2,3,4,5}; decimal[] numbersAsDecimal = numbers.Cast<decimal>().ToArray(); إذا أردت إجراء عملية التحويل بين الأنواع (casting) على التَجميعَات بطريقة صحيحة، نفذ التالي: var numbers= new int[] {1,2,3,4,5}; decimal[] numbersAsDecimal = numbers.Select(n => (decimal)n).ToArray(); التابع Range يَستقبِل التابع Range مُعامِلين، هما قيمة أول رقم بالمُُعدَّد، ورقم يُعبِر عن عدد العناصر بالمُعَّدد النَاتِج -لا قيمة آخر رقم. // prints 1,2,3,4,5,6,7,8,9,10 Console.WriteLine(string.Join(",", Enumerable.Range(1, 10))); // prints 10,11,12,13,14 Console.WriteLine(string.Join(",", Enumerable.Range(10, 5))); التابع ThenBy لا يُستخَدَم التابع ThenBy إلا بعد استدعاء التابع OrderBy، مما يَسمَح بترتيب عناصر المُُعدَّد وفقًا لأكثر من معيار. var persons { new {Id = 1, Name = "Foo", Order = 1}, new {Id = 1, Name = "FooTwo", Order = 2}, new {Id = 2, Name = "Bar", Order = 2}, new {Id = 2, Name = "BarTwo", Order = 1}, new {Id = 3, Name = "Fizz", Order = 2}, new {Id = 3, Name = "FizzTwo", Order = 1}, }; var personsSortedByName = persons.OrderBy(p => p.Id).ThenBy(p => p.Order); Console.WriteLine(string.Join(",", personsSortedByName.Select(p => p.Name))); //This will display : //Foo,FooTwo,BarTwo,Bar,FizzTwo,Fizz التابع Repeat يُعيد التابع Enumerable.Repeat مُتتالية من عدة عناصر تَحمِل جميعها نفس القيمة. يُنتج المثال التالي مُتتالية مكونة من أربع عناصر، يَحوي كلَا منها السلسلة النصية “Hello”. var repeats = Enumerable.Repeat("Hello", 4); foreach (var item in repeats) { Console.WriteLine(item); } /* output: Hello Hello Hello Hello */ التابع Empty يُنشِئ التابع Empty مُعدَّدًا فارغًا من الواجهة IEnumerable<T>. يَستقبِل التابع نوع المُعدَّد كمُعامِل نوع (type parameter). فمثلًا، يُنشِئ المثال التالي مُُعدَّد من النوع int: IEnumerable<int> emptyList = Enumerable.Empty<int>(); يُخزن التابع Empty كل نوع تم انشائه من المتُعدَّدات الفارغة IEnumerable<T> تخزينًا مؤقتًا (caching) ويُعيِد استخدامه عند الحاجة لتنشئة نسخة جديدة من نفس النوع، وبالتالي: Enumerable.Empty<decimal>() == Enumerable.Empty<decimal>(); // This is True Enumerable.Empty<int>() == Enumerable.Empty<decimal>(); // This is False التابع Select (ربط map) var persons { new {Id = 1, Name = "Foo"}, new {Id = 2, Name = "Bar"}, new {Id = 3, Name = "Fizz"}, new {Id = 4, Name = "Buzz"}, }; var names = persons.Select(p => p.Name); Console.WriteLine(string.Join(",", names.ToArray())); //Foo,Bar,Fizz,Buzz تُطلِق لغات البرمجة الوَظِيفية (functional languages) اسم الرَبْط (map) على هذا النوع من الدوال. التابع OrderBy var persons { new {Id = 1, Name = "Foo"}, new {Id = 2, Name = "Bar"}, new {Id = 3, Name = "Fizz"}, new {Id = 4, Name = "Buzz"}, }; var personsSortedByName = persons.OrderBy(p => p.Name); Console.WriteLine(string.Join(",", personsSortedByName.Select(p => p.Id).ToArray())); //2,4,3,1 التابع OrderByDescending var persons { new {Id = 1, Name = "Foo"}, new {Id = 2, Name = "Bar"}, new {Id = 3, Name = "Fizz"}, new {Id = 4, Name = "Buzz"}, }; var personsSortedByNameDescending = persons.OrderByDescending(p => p.Name); Console.WriteLine(string.Join(",", personsSortedByNameDescending.Select(p => p.Id).ToArray())); //1,3,4,2 التابع Contains var numbers = new[] {1,2,3,4,5}; Console.WriteLine(numbers.Contains(3)); //True Console.WriteLine(numbers.Contains(34)); //False التابع First (بحث وجلب find) var numbers = new[] {1,2,3,4,5}; var firstNumber = numbers.First(); Console.WriteLine(firstNumber); //1 var firstEvenNumber = numbers.First(n => (n & 1) == 0); Console.WriteLine(firstEvenNumber); //2 عند عدم تواجد أي عناصر تُحقِق شرط الدالة المجردة المُمرَّرة للتابع، يُبلَّغ عن اعتراض من النوع InvalidOperationException مَصحُوبًا بالرسالة التالية "لا تحتوي المتتالية على أية عناصر مُتوافِقة". var firstNegativeNumber = numbers.First(n => n < 0); التابع Single var oneNumber = new[] {5}; var theOnlyNumber = oneNumber.Single(); Console.WriteLine(theOnlyNumber); //5 var numbers = new[] {1,2,3,4,5}; var theOnlyNumberSmallerThanTwo = numbers.Single(n => n < 2); Console.WriteLine(theOnlyNumberSmallerThanTwo); //1 عند تواجد أكثر من عنصر يُحقِق شرط الدالة المجردة أو عند عدم وجوده نهائيًا كاﻷمثلة التالية، يُبلِّغ التابع Single عن اعتراض من النوع InvalidOperationException. var theOnlyNumberInNumbers = numbers.Single(); var theOnlyNegativeNumber = numbers.Single(n => n < 0); التابع Last var numbers = new[] {1,2,3,4,5}; var lastNumber = numbers.Last(); Console.WriteLine(lastNumber); //5 var lastEvenNumber = numbers.Last(n => (n & 1) == 0); Console.WriteLine(lastEvenNumber); //4 في المثال التالي، يُبلَّغ عن اعتراض من النوع InvalidOperationException لعدَم وجود أيّ عنصر يُحقِق شرط الدالة المُجردَّة: var lastNegativeNumber = numbers.Last(n => n < 0); التابع LastOrDefault var numbers = new[] {1,2,3,4,5}; var lastNumber = numbers.LastOrDefault(); Console.WriteLine(lastNumber); //5 var lastEvenNumber = numbers.LastOrDefault(n => (n & 1) == 0); Console.WriteLine(lastEvenNumber); //4 var lastNegativeNumber = numbers.LastOrDefault(n => n < 0); Console.WriteLine(lastNegativeNumber); //0 var words = new[] { "one", "two", "three", "four", "five" }; var lastWord = words.LastOrDefault(); Console.WriteLine(lastWord); // five var lastLongWord = words.LastOrDefault(w => w.Length > 4); Console.WriteLine(lastLongWord); // three var lastMissingWord = words.LastOrDefault(w => w.Length > 5); Console.WriteLine(lastMissingWord); // null التابع SingleOrDefault var oneNumber = new[] {5}; var theOnlyNumber = oneNumber.SingleOrDefault(); Console.WriteLine(theOnlyNumber); //5 var numbers = new[] {1,2,3,4,5}; var theOnlyNumberSmallerThanTwo = numbers.SingleOrDefault(n => n < 2); Console.WriteLine(theOnlyNumberSmallerThanTwo); //1 var theOnlyNegativeNumber = numbers.SingleOrDefault(n => n < 0); Console.WriteLine(theOnlyNegativeNumber); //0 يَختلف عن التابع Single عند عدم تواجد أي عنصر يُحقِق شرط الدالة المجردة، ففي هذه الحالة، لا يُبلِّغ التابع SingleOrDefault عن اعتراض، وإنما يُعيد القيمة الافتراضية للنوع الذي يحتويه المُُعدَّد. أما في حالة تواجد أكثر من عنصر، فإنه -مثل التابع Single- يُبلِّغ عن اعتراض من النوع InvalidOperationException كالمثال التالي: var theOnlyNumberInNumbers = numbers.SingleOrDefault(); التابع FirstOrDefault var numbers = new[] {1,2,3,4,5}; var firstNumber = numbers.FirstOrDefault(); Console.WriteLine(firstNumber); //1 var firstEvenNumber = numbers.FirstOrDefault(n => (n & 1) == 0); Console.WriteLine(firstEvenNumber); //2 var firstNegativeNumber = numbers.FirstOrDefault(n => n < 0); Console.WriteLine(firstNegativeNumber); //0 var words = new[] { "one", "two", "three", "four", "five" }; var firstWord = words.FirstOrDefault(); Console.WriteLine(firstWord); // one var firstLongWord = words.FirstOrDefault(w => w.Length > 3); Console.WriteLine(firstLongWord); // three var firstMissingWord = words.FirstOrDefault(w => w.Length > 5); Console.WriteLine(firstMissingWord); // null التابع Skip يقوم التابع Skip بتَخطِي أول مجموعة من العناصر بالمُعدَّد. عدد تلك العناصر يُحدِّده مُعامل دَخْل يُمرَّر للتابع. عندما يَصِل التابع Skip إلى أول عنصر بعد تلك المجموعة، يبدأ بإعادة قيم العناصر عند تَعدِّيدها. var numbers = new[] {1,2,3,4,5}; var allNumbersExceptFirstTwo = numbers.Skip(2); Console.WriteLine(string.Join(",", allNumbersExceptFirstTwo.ToArray())); //3,4,5 التابع Take يَجِلْب التابع Take أول مجموعة عناصر من المُعدَّد، ويحدِّد عددها مُعامل دَخْل يُمرَّر للتابع. var numbers = new[] {1,2,3,4,5}; var threeFirstNumbers = numbers.Take(3); Console.WriteLine(string.Join(",", threeFirstNumbers.ToArray())); //1,2,3 التابع Reverse var numbers = new[] {1,2,3,4,5}; var reversed = numbers.Reverse(); Console.WriteLine(string.Join(",", reversed.ToArray())); //5,4,3,2,1 التابع OfType var mixed = new object[] {1,"Foo",2,"Bar",3,"Fizz",4,"Buzz"}; var numbers = mixed.OfType<int>(); Console.WriteLine(string.Join(",", numbers.ToArray())); //1,2,3,4 التابع Max var numbers = new[] {1,2,3,4}; var maxNumber = numbers.Max(); Console.WriteLine(maxNumber); //4 var cities = new[] { new {Population = 1000}, new {Population = 2500}, new {Population = 4000} }; var maxPopulation = cities.Max(c => c.Population); Console.WriteLine(maxPopulation); //4000 التابع Average var numbers = new[] {1,2,3,4}; var averageNumber = numbers.Average(); Console.WriteLine(averageNumber); // 2,5 يَحسِب التابع Average في المثال باﻷعلى قيمة مُتوسِط مُعدَّد من النوع العَدَدي. var cities = new[] { new {Population = 1000}, new {Population = 2000}, new {Population = 4000} }; var averagePopulation = cities.Average(c => c.Population); Console.WriteLine(averagePopulation); // 2333,33 يَحِسب التابع في المثال بالأعلى قيمة مُتوسِط المُعدَّد بالاعتماد على مُفوِّض (delegated function) يُمرَّر له. التابع GroupBy var persons = new[] { new { Name="Fizz", Job="Developer"}, new { Name="Buzz", Job="Developer"}, new { Name="Foo", Job="Astronaut"}, new { Name="Bar", Job="Astronaut"}, }; var groupedByJob = persons.GroupBy(p => p.Job); foreach(var theGroup in groupedByJob) { Console.WriteLine( "{0} are {1}s", string.Join(",", theGroup.Select(g => g.Name).ToArray()), theGroup.Key); } //Fizz,Buzz are Developers //Foo,Bar are Astronauts في المثال التالي، تُجَمَّع الفواتير وفقًا لقيمة الدولة، ويُنشَّئ لكلًا منها كائن جديد يحتوي على عدد السِجلات، والقيمة المدفوعة الكلية، ومتوسط القيمة المدفوعة: var a = db.Invoices.GroupBy(i => i.Country) .Select(g => new { Country = g.Key, Count = g.Count(), Total = g.Sum(i => i.Paid), Average = g.Average(i => i.Paid) }); إذا أردنا المدفوعات فقط بدون مجموعات: var a = db.Invoices.GroupBy(i => 1) .Select(g => new { Count = g.Count(), Total = g.Sum(i => i.Paid), Average = g.Average(i => i.Paid) }); إذا أردنا أكثر من تِعداد: var a = db.Invoices.GroupBy(g => 1) .Select(g => new { High = g.Count(i => i.Paid >= 1000), Low = g.Count(i => i.Paid < 1000), Sum = g.Sum(i => i.Paid) }); التابع ToDictionary يُحول التابع ToDictionary مُعامِل مُعدَّد من الوَاجِهة IEnumerable -يُدعى المصدر source- إلى قاموس (Dictionary)، بالاستعانة بمُعامِل دالة مُجردة تُمرَّر للتابع -تُدعى keySelector- لتحديد مفاتيح القاموس (keys). يُبلِّغ التابع عن اعتراض من النوع ArgumentException إذا كانت الدالة المُجردَّة keySelector غير مُتباينة (الدالة المُتباينة injective هي دالة تعيد قيمة فريدة (unique) لكل عنصر بمُعامل التَجمِيعَة source). تتوفر توابع التحميل الزائد (overloads) من التابع ToDictionary، والتي تَستقبِل دوال مجردة لتخصيص مفاتيح (keys) وقيم (value) القاموس. var persons = new[] { new { Name="Fizz", Id=1}, new { Name="Buzz", Id=2}, new { Name="Foo", Id=3}, new { Name="Bar", Id=4}, }; إذا ما خُصِّصَت دالة اختيار المفتاح فقط، سيُنشَّئ قاموس من النوع Dictionary<TKey,TVal>. بحيث يكون نوع المفتاح TKey من نفس نوع القيمة العائدة من الدالة KeySelector، بينما يكون نوع القيمة TVal من نفس نوع الكائن اﻷصلي ويحمل قيمته. var personsById = persons.ToDictionary(p => p.Id); // personsById is a Dictionary<int,object> Console.WriteLine(personsById[1].Name); //Fizz Console.WriteLine(personsById[2].Name); //Buzz أما إذا خُصّصت دالة اختيار القيمة ValueSelector أيضًا، فسيُنشَّئ قاموس من النوع Dictionary<TKey,TVal>. بحيث يكون نوع المفتاح TKey من نفس نوع القيمة العائدة من الدالة KeySelector، ويكون نوع القيمة TVal من نفس نوع القيمة العائدة من الدالة ValueSelector ويحمل قيمتها الفِعلية تِباعًا. var namesById = persons.ToDictionary(p => p.Id, p => p.Name); //namesById is a Dictionary<int,string> Console.WriteLine(namesById[3]); //Foo Console.WriteLine(namesById[4]); //Bar كما ذُكر باﻷعلى، يجب أن تكون المفاتيح -التي تعيدها دالة اختيار المفاتيح- فريدة. في المثال التالي، يُبلَّغ عن اعتراض لمُخالفة هذا الشرط: var persons = new[] { new { Name="Fizz", Id=1}, new { Name="Buzz", Id=2}, new { Name="Foo", Id=3}, new { Name="Bar", Id=4}, new { Name="Oops", Id=4} }; var willThrowException = persons.ToDictionary(p => p.Id) إذا لم يكن بالإمكان تخصيص مفتاح فريد لكل عنصر بالتَجمِيعَة، يُمكِنك استخدام التابع ToLookUp. ظاهريًا، يعمل التابع ToLookup بصورة مشابهة للتابع ToDictionary، بِخلاف إمكانية ربطه لكل مفتاح بتَجمِيعَة من القيم التي تَحمِل نفس المفتاح. التابع Union var numbers1to5 = new[] {1,2,3,4,5}; var numbers4to8 = new[] {4,5,6,7,8}; var numbers1to8 = numbers1to5.Union(numbers4to8); Console.WriteLine(string.Join(",", numbers1to8)); //1,2,3,4,5,6,7,8 يَحذِف التابع Union العناصر المُكرّرة. اِستخدِم التابع Concat إذا لم يكن ذلك ما ترغب به. التابع ToArray var numbers = new[] {1,2,3,4,5,6,7,8,9,10}; var someNumbers = numbers.Where(n => n < 6); Console.WriteLine(someNumbers.GetType().Name); //WhereArrayIterator`1 var someNumbersArray = someNumbers.ToArray(); Console.WriteLine(someNumbersArray.GetType().Name); //Int32[] التابع ToList var numbers = new[] {1,2,3,4,5,6,7,8,9,10}; var someNumbers = numbers.Where(n => n < 6); Console.WriteLine(someNumbers.GetType().Name); //WhereArrayIterator`1 var someNumbersList = someNumbers.ToList(); Console.WriteLine( someNumbersList.GetType().Name + " - " + someNumbersList.GetType().GetGenericArguments()[0].Name); //List`1 - Int32 التابع List.ForEach التابع ForEach مُعرف بالصنف List<T>، وليس بأيّ من الوَاجِهتين IQueryable<T> أو IEnumerable<T>. ومِنْ ثَمَّ إذا أردت استدعاء هذا التابع من خلال كائن من احدى هاتين الوَاجِهتين، فلديك خيارين: الخيار اﻷول: استدعاء التابع ToList أولًا عند استدعاء التابع ToList، سيحدث شيئًا من اثنين اعتمادًا على وَاجِهة الكائن. إذا كان الكائن من الوَاجِهة IEnumerable<T>، ستُجرَى عملية تِعِداد (enumeration) للمُعدَّد، مما يؤدي إلى نَسخ النتيجة إلى قائمة جديدة (List). أما إذا كان الكائن من الوَاجِهة IQueryable<T>، فستُجرَى عملية اتصال بقاعدة البيانات لتنفيذ عبارة استعلام (query) مُعيّنة. وأخيرًا، سيُستدعَى التابع ForEach لكل عنصر بالقائمة. مثلًا: IEnumerable<Customer> customers = new List<Customer>(); customers.ToList().ForEach(c => c.SendEmail()); تُعاني هذه الطريقة مِن استهلاك غير ضروري للذاكرة بسبب الاضطرار لإنشاء مؤقت لقائمة (List). الخيار الثاني: استخدام التابع المُوسِّع (Extension Method) أضِف التابع المُوسِّع التالي للواجهة IEnumerable<T> كالتالي: public static void ForEach<T>(this IEnumerable<T> enumeration, Action<T> action) { foreach(T item in enumeration) { action(item); } } ثم اِستَدعه مباشرة من خلال الوَاجِهة كالتالي: IEnumerable<Customer> customers = new List<Customer>(); customers.ForEach(c => c.SendEmail()); تنبيه: صُممت توابع الاستعلامات التكميلية اللغوية LINQ لتكون نَقيَّة (pure methods)، مما يَعنيّ عدم تَسَبّبهم بأيّ تأثيرات جانبية (side effects). يَنحرِف التابع ForEach عن بقية التوابع من هذه الناحية لأن هدفه اﻷوحد هو إحِداث تأثير جانبي. بدلًا من ذلك، قد تُنفِذ التِكرار (loop) باستخدام الكلمة المفتاحية foreach، وهذا، في الواقع، ما تَفعله الدالة المُوسِّعة باﻷعلى داخليًا. في حالة كنت تُريد إجراء التكرار على مُتغير من النوع List، يُمكنك استدعاء التابع ForEach مُباشِرة دون الحاجة إلى أي توابع مُوسِّعة، كالتالي: public class Customer { public void SendEmail() { // Sending email code here } } List<Customer> customers = new List<Customer>(); customers.Add(new Customer()); customers.Add(new Customer()); customers.ForEach(c => c.SendEmail()); التابع ElementAt يُبلِّغ التابع ElementAt عن اعتراض من النوع ArgumentOutOfRangeException عند تمرِّير فِهرَس (index) خارج مدى (Range) المُتعدَّد. var names = new[] {"Foo","Bar","Fizz","Buzz"}; var thirdName = names.ElementAt(2); Console.WriteLine(thirdName); //Fizz سيُبلَّغ عن اعتراض ArgumentOutOfRangeException في المثال التالي: var names = new[] {"Foo","Bar","Fizz","Buzz"}; var minusOnethName = names.ElementAt(-1); var fifthName = names.ElementAt(4); التابع ElementAtOrDefault var names = new[] {"Foo","Bar","Fizz","Buzz"}; var thirdName = names.ElementAtOrDefault(2); Console.WriteLine(thirdName); //Fizz var minusOnethName = names.ElementAtOrDefault(-1); Console.WriteLine(minusOnethName); //null var fifthName = names.ElementAtOrDefault(4); Console.WriteLine(fifthName); //null التابع SkipWhile var numbers = new[] {2,4,6,8,1,3,5,7}; var oddNumbers = numbers.SkipWhile(n => (n & 1) == 0); Console.WriteLine(string.Join(",", oddNumbers.ToArray())); //1,3,5,7 التابع TakeWhile var numbers = new[] {2,4,6,1,3,5,7,8}; var evenNumbers = numbers.TakeWhile(n => (n & 1) == 0); Console.WriteLine(string.Join(",", evenNumbers.ToArray())); //2,4,6 التابع DefaultIfEmpty var numbers = new[] {2,4,6,8,1,3,5,7}; var numbersOrDefault = numbers.DefaultIfEmpty(); Console.WriteLine(numbers.SequenceEqual(numbersOrDefault)); //True var noNumbers = new int[0]; var noNumbersOrDefault = noNumbers.DefaultIfEmpty(); Console.WriteLine(noNumbersOrDefault.Count()); //1 Console.WriteLine(noNumbersOrDefault.Single()); //0 var noNumbersOrExplicitDefault = noNumbers.DefaultIfEmpty(34); Console.WriteLine(noNumbersOrExplicitDefault.Count()); //1 Console.WriteLine(noNumbersOrExplicitDefault.Single()); //34 التابع Join class Developer { public int Id { get; set; } public string Name { get; set; } } class Project { public int DeveloperId { get; set; } public string Name { get; set; } } var developers = new[] { new Developer { Id = 1, Name = "Foobuzz" }, new Developer { Id = 2, Name = "Barfizz" } }; var projects = new[] { new Project { DeveloperId = 1, Name = "Hello World 3D" }, new Project { DeveloperId = 1, Name = "Super Fizzbuzz Maker" }, new Project { DeveloperId = 2, Name = "Citizen Kane - The action game" }, new Project { DeveloperId = 2, Name = "Pro Pong 2016" } }; var denormalized = developers.Join( inner: projects, outerKeySelector: dev => dev.Id, innerKeySelector: proj => proj.DeveloperId, resultSelector: (dev, proj) => new { ProjectName = proj.Name, DeveloperName = dev.Name}); foreach(var item in denormalized) { Console.WriteLine("{0} by {1}", item.ProjectName, item.DeveloperName); } //Hello World 3D by Foobuzz //Super Fizzbuzz Maker by Foobuzz //Citizen Kane - The action game by Barfizz //Pro Pong 2016 by Barfizz مثال آخر عن التابع Join مع تطبيق الضم الخارجي اليساري (left outer join): class Person { public string FirstName { get; set; } public string LastName { get; set; } } class Pet { public string Name { get; set; } public Person Owner { get; set; } } public static void Main(string[] args) { var magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" }; var terry = new Person { FirstName = "Terry", LastName = "Adams" }; var barley = new Pet { Name = "Barley", Owner = terry }; var people = new[] { magnus, terry }; var pets = new[] { barley }; var query = from person in people join pet in pets on person equals pet.Owner into gj from subpet in gj.DefaultIfEmpty() select new { person.FirstName, PetName = subpet?.Name ?? "-" // Use - if he has no pet }; foreach (var p in query) Console.WriteLine($"{p.FirstName}: {p.PetName}"); } ترجمة -وبتصرف- للفصل LINQ والفصل ForEach من كتاب .NET Framework Notes for Professionals
-
أنواع القيمة (Value types) ببساطة، تَحوِّي أنواع القيمة (value types) القِيمَة الفِعلِية. تُشتَقّ جميع أنواع القيمة -والتي تَتضَمَن غالبية اﻷصناف المبنية مُسبَقًا (built-in types)- مِن الصنف System.ValueType. ونظرًا لكَوْن هذا الصنف غيْر قابِل للتوريث بشكل مباشر، تُستخَدَم الكلمة المفتاحية صنف (Struct keyword) لإنشاء نوع قيمة مُخصّص (custom value types) كالمثال بالأسفل. عند إنشّاء نسخة (instance) جديدة من نوع القيمة، تُستَخَدَم ذاكِرة المَكْدَس (Stack Memory) لتخزينها، والتي تَتسِع بما يتوافق مع حجم النوع المُصرّح عنه. على سبيل المثال، يُخصَّص لكل نوع عَدَدِيّ int مِساحة 32 بت بِـذاكِرة المَكْدَس. ويُلْغَى تَخصّيص هذه المساحة عند خروج النُسخة من النِطاق (scope). مِن المُهم أن تُدرِك أنه عند إِسْناد متغير من نوع القيمة لآخر، تُنسَخ قيمته لا مَرجِعه للمُتغير الآخر، مما يَعّني أنه قد أصبح لدينا نسختين مُنفَصلتين، لا يؤثر تغيير قيمة احِدَاهُما على قيمة النسخة الآخرى. struct PersonAsValueType { public string Name; } class Program { static void Main() { PersonAsValueType personA; personA.Name = "Bob"; var personB = personA; personA.Name = "Linda"; // يُشير المُتغيّران إلى مواضع مختلفة بالذاكرة Console.WriteLine(object.ReferenceEquals(personA, personB)); // 'False' Console.WriteLine(personA.Name); // Outputs 'Linda' Console.WriteLine(personB.Name); // Outputs 'Bob' } } أنواع مرجعية (Reference types) تتكون الأنواع المَرجعِية (reference types) من قيمة (value) مُخزَّنة بمَكان ما بالذاكرة ومَرجِع (reference) يُشير إلى هذا المكان. يُمكِن القول أنها تَعمَل بصورة مشابهة للمُؤشِرات (pointers) بلغتي C/C++. تُعدّ جميع الأصناف (Classes) -حتى الساكن منها (static class)- من النوع المَرجعِي. يُوضح المثال باﻷسفل استخدام صنف (class) لإنشاء متغير من النوع المَرجِعي وإِسْناده لآخر. تُخزَّن جميع الأنواع المَرجعِية بقسم الكَوْمَة في الذاكرة (Memory Heap). فعِند إنشاء كائن جديد، تُخصَّص مِساحة من ذاكرة الكَوْمَة له ويُعَاد مَرجِع (reference) يُشير إلى مَوقِع تِلك المساحة. يَتولى كانِس المهملات (garbage collector) مُهِمّة إدارة ذاكرة الكَوْمَة، ولا يُسمَح لك بالتَدخُل أو التحكم بها بنفسك. بالإضافة إلى مساحة الذاكِرة المُخصَصة للكائن ذاته، تُخصّص مساحة أخرى إضافية لتخزين كلًا من المَرجِع ومعلومات إضافية تحتاجها بيئة التنفيذ المشتركة (CLR) بإطار عمل .NET بصورة مؤقتة. أحد أهم ما يُفرِّق بين نوعي القيمة والمَرجِع هو أنه عند إِسْناد متغير من نوع مَرجِعي لآخر، يُنسَخ المَرجِع إلى المُتغير الآخر لا القيمة التي يُشير إليها، مما يَعّني أنه قد اَصبح لدينا مَرجِعين يُشيران إلى نفس الكائن. بالتالي، سَيُؤثر أيّ تغيير في القِيم الفِعلية لهذا الكائن على قيمة كلا المَرجِعين. class PersonAsReferenceType { public string Name; } class Program { static void Main() { PersonAsReferenceType personA; personA = new PersonAsReferenceType { Name = "Bob" }; var personB = personA; personA.Name = "Linda"; // يُشير كلًا من المُتغيّران إلى نفس موضع الذاكرة Console.WriteLine(object.ReferenceEquals(personA, personB)); // 'True' Console.WriteLine(personA.Name); // 'Linda' Console.WriteLine(personB.Name); // 'Linda' } } الأنواع المخصصة (Custom Types) بالإضافة إلى اﻷصناف المبنية مُسبَقًا (built-in types)، يُمكن إنشاء أصناف مُخصّصة (Custom). الصنف Struct تَرِث الأصناف (structs) -المُعرَّفة باستخدام الكلمة المفتاحية Struct- النوع System.ValueType تلقائيًا، ولذلك تُعدّ من أنواع القيمة (value types)-كما ذَكرنا مُسبَقًا-، كما تُخزَّن بذاكِرة المَكْدَس (stack). على سبيل المثال: Struct MyStruct { public int x; public int y; } تُمرَّر المُتَغيّرات من نوع القيمة كمُعامِلات للدوال تمريرًا قيميًا (pass by value)، أي تُنسَخ قيمة المُتَغيّر وتُسْنَد لمُعامِل الدالة، وبالتالي إذا غَيرَّت الدالة قيمة المُعامِل فإن هذا التَغيير لا يَنعكِس على قيمة المُتَغيّر الأصلي خارج الدالة؛ لأنه مُجرد نُسخة. في الشيفرة التالية، تَستقبِل الدالة AddNumbers مُعامِلين x و y من النوع int وهو نوع قيمة. على الرغم من أن الدالة تُزيد قيمة المُعامِل x بمقدار 5، فإن قيمة المُتَغيّر a تظل كما هي؛ لأن المُعامِل x هو، بالواقع، مُجرد نسخة من قيمة المُتَغيّر a وليس المُتَغيّر ذاته. int a = 5; int b = 6; AddNumbers(a,b); public AddNumbers(int x, int y) { int z = x + y; // قيمة المتغير z تساوي 11 x = x + 5; // غُيرت قيمة x إلى القيمة 10 z = x + y; // قيمة المتغير z تساوي 16 } الصنف class تَرِث الأصناف (classes) -المُعرَّفة باستخدام الكلمة المفتاحية Class- النوع System.Object تلقائيًا، ولذلك تُعدّ من الأنواع المَرجِعية (reference types)، كما تُخزَّن بقسم الكَوْمَة في الذاكرة (heap). على سبيل المثال: public Class MyClass { public int a; public int b; } تُمرَّر المُتَغيّرات من نوع المَرجِع كمُعامِلات للدوال تمريرًا مَرجعيًا (pass by reference)، أي يُنسَخ مَرجِع المُتَغيّر ويُسند لمُعامِل الدالة. لمّا كانت الدالة تَملِك مُعاملًا يَحمِل مَرجِعًا يُشير إلى الكائن الأصلي ذاته، فإنها إذا غَيرَّت قيمة المُعامِل فإن هذا التَغيير سيَنعكِس على قيمة المُتَغيّر الأصلي خارج الدالة. في الشيفرة التالية، نَستخدِم نفس المثال السابق لكن ضُمِنِّت المُتَغيّرات من النوع int بداخل صنف class، بحيث تَستقبِل الدالة مُعامِلًا من ذلك الصنف. الآن، عندما تُغيّر الدالة قيمة المُتَغيّر sample.a، فإن قيمة المُتَغيّر instanceOfMyClass.a ستَتَغيَّر تلقائيًا؛ لأن مُعامِل الدالة، في الواقع، يَحمِل مَرجِعًا يُشير إلى نفس ذات الكائن المُمرَّر لها، وليس مُجرد نسخة منه. MyClass instanceOfMyClass = new MyClass(); instanceOfMyClass.a = 5; instanceOfMyClass.b = 6; AddNumbers(instanceOfMyClass); public AddNumbers(MyClass sample) { int z = sample.a + sample.b; // قيمة المتغير z تساوي 11 sample.a = sample.a + 5; // غُيرت قيمة a إلى القيمة 10 z = sample.a + sample.b; // قيمة المتغير z تساوي 16 } ترجمة -وبتصرف- للفصلين Custom Types و Stack and Heap من كتاب .NET Framework Notes for Professionals
-
مهيئ التجميعات (Collection Initializers) يُمكِن تَهيِّئة (Initialize) بعض أنواع التَجمِيعَات أثناء التصريح (declaration) عنها. على سبيل المثال، تُنشِئ التَعلِيمَة البرمجية التالية المُتغَير numbers وتُهيِّئه بمجموعة أعداد (integers): List<int> numbers = new List<int>(){10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1}; في الواقع، يُحوِل مُصرِّف (Compiler) c# تَعلِيمَة التَهيِّئة السابقة لسلسلة استدعاءات للتابع Add. بناءً على ذلك، لا يُمكِن استخدام هذه الصِيْغة (syntax) إلا مع التَجمِيعَات التي تُدعِم التابع Add. لاحظ أن صَنفيّ المَكْدَس Stack<T> والرَتَل Queue<T> لا يُدعِمَانِها. عِند التَعامُل مع التَجمِيعَات المُعقدة مِثل Dictionary<TKey, TValue>، المُكَوّنة مِن أزواج مفتاح/قيمة (key/value pairs)، يُحدَّد كل زوج كنوع مَجهول الاسم (anonymous type) بقائمة التَهيِّئة (initializer list)، كالآتي: Dictionary<int, string> employee = new Dictionary<int, string>() {{44, "John"}, {45, "Bob"}, {47, "James"}, {48, "Franklin"}}; العنصر الأول بكل زوج هو المفتاح (key) بينما الثاني هو القيمة (value). القائمة (List) قائمة من أنواع أولية (Primitive) List<int> numbers = new List<int>(){10, 9, 8, 7, 7, 6, 5, 10, 4, 3, 2, 1}; قائمة من أنواع مخصصة (Custom Types) وتهيئتها تُعرِّف الشيفرة التالية الصَنف Model، الذي يحتوي على خاصيتي Name و Selected من النوع nullable boolean. public class Model { public string Name { get; set; } public bool? Selected { get; set; } } توجد عدة طرائق لتَهيِّئة قائمة List من الصنف Model. إذا لم يُعرَّف بَانِي الكائن (Constructor) بالصنف كالشيفرة بالأعلى، تُنشَّئ نسخ جديدة من الصنف Model وتُهيَّئ كالتالي: var SelectedEmployees = { new Model() {Name = "Item1", Selected = true}, new Model() {Name = "Item2", Selected = false}, new Model() {Name = "Item3", Selected = false}, new Model() {Name = "Item4"} }; أما لو عُرِّف بَانِي الكائن (Constructor) بالصَنف كالتالي: public class Model { public Model(string name, bool? selected = false) { Name = name; selected = Selected; } public string Name { get; set; } public bool? Selected { get; set; } } يَسمَح ذلك بتَهيِّئة القائمة بشكل مختلف قليلًا، كالتالي: var SelectedEmployees = new List<Model> { new Model("Mark", true), new Model("Alexis"), new Model("") }; ماذا عن صنف إِحِدى خواصه هي صنف بذاته؟ مثلًا: public class Model { public string Name { get; set; } public bool? Selected { get; set; } } public class ExtendedModel : Model { public ExtendedModel() { BaseModel = new Model(); } public Model BaseModel { get; set; } public DateTime BirthDate { get; set; } } لتبسِيط المثال قليلًا، أُزيِل بَانِي الكائن (constructor) من الصنف Model. var SelectedWithBirthDate = new List<ExtendedModel> { new ExtendedModel() { BaseModel = new Model { Name = "Mark", Selected = true}, BirthDate = new DateTime(2015, 11, 23) }, new ExtendedModel() { BaseModel = new Model { Name = "Random"}, BirthDate = new DateTime(2015, 11, 23) } } بالمِثل، يُمكِن تطبيق ذلك على الأصناف Collection<ExtendedModel> أو ExtendedModel[] و object[] أو []. المكدس (Stack) تُدعِم .NET تَجمِيعَة المَكْدَس (Stack)، وهي هيكل بياني (data structure) لإدارة مجموعة من القيم اِعتمادًا على مفهوم “الداخل آخرًا، يخرج أولًا“ (LIFO). يُدخِل التابع Push(T item) العناصر للمَكْدَس، بينما يُخرِج التابع Pop() آخر عنصر تَمْ إدخاله للمَكْدَس، ويَحذِفه منه. تُوضِح الشيفرة التالية استخدام النسخة المُعمَّمة (Generic) من المَكْدَس لإدارة مجموعة سلاسل نصية. قم أولًا بإضافة فَضَاء الاسم (namespace): using System.Collections.Generic; ثم استخدمه: Stack<string> stack = new Stack<string>(); stack.Push("John"); stack.Push("Paul"); stack.Push("George"); stack.Push("Ringo"); string value; value = stack.Pop(); // return Ringo value = stack.Pop(); // return George value = stack.Pop(); // return Paul value = stack.Pop(); // return John يُوجد نسخة غير مُعمَّمة من المَكْدَس تَتَعَامَل مع الكائنات (objects). أضِف فَضَاء الاسم: using System.Collections; تُوضح الشيفرة التالية استخدام مَكْدَس غير مُعمَّم: Stack stack = new Stack(); stack.Push("Hello World"); // string stack.Push(5); // int stack.Push(1d); // double stack.Push(true); // bool stack.Push(new Product()); // Product object object value; value = stack.Pop(); // return Product (Product type) value = stack.Pop(); // return true (bool) value = stack.Pop(); // return 1d (double) value = stack.Pop(); // return 5 (int) value = stack.Pop(); // return Hello World (string) بخلاف التابع Pop()، يُخرِج التابع Peek() آخر عنصر تَمْ إدخاله للمَكْدَس دون حَذفه منه. Stack<int> stack = new Stack<int>(); stack.Push(10); stack.Push(20); var lastValueAdded = stack.Peek(); // 20 يُطبّق التِكرار (iterate) على عناصر المَكْدَس وفقًا لمفهوم “الداخل آخرًا، يخرج أولًا“ (LIFO) وبدون حذفها، كاﻵتي: Stack<int> stack = new Stack<int>(); stack.Push(10); stack.Push(20); stack.Push(30); stack.Push(40); stack.Push(50); foreach (int element in stack) { Console.WriteLine(element); } خَرْج الشيفرة باﻷعلى يكون كالتالي: 50 40 30 20 10 الرتل (Queue) تُدعِم .NET تَجمِيعَة الرَتَل (Queue)، وهي هيكل بياني (data structure) لإدارة مجموعة من القيم اِعتمادًا على مفهوم “الداخل أولًا، يخرج أولًا“ (FIFO). يُدخِل التابع Enqueue(T item) العناصر للرَتَل، بينما يُخرِج التابع Dequeue() أول عنصر من الرَتَل، ويَحذِفه منه. توضح الشيفرة التالية استخدام النسخة المُعمَّمة (Generic) من رَتَل لإدارة مجموعة سلاسل النصية. قم أولًا بإضافة فَضَاء الاسم (namespace): using System.Collections.Generic; ثم استخدمه: Queue<string> queue = new Queue<string>(); queue.Enqueue("John"); queue.Enqueue("Paul"); queue.Enqueue("George"); queue.Enqueue("Ringo"); string dequeueValue; dequeueValue = queue.Dequeue(); // return John dequeueValue = queue.Dequeue(); // return Paul dequeueValue = queue.Dequeue(); // return George dequeueValue = queue.Dequeue(); // return Ringo يُوجد نسخة غير مُعمَّمة من الرَتَل تَتَعَامَل مع الكائنات (objects). بالمِثل، أضف فضاء الاسم: using System.Collections; تُوضح الشيفرة التالية استخدام رَتَل غير مُعمَّم: Queue queue = new Queue(); queue.Enqueue("Hello World"); // string queue.Enqueue(5); // int queue.Enqueue(1d); // double queue.Enqueue(true); // bool queue.Enqueue(new Product()); // Product object object dequeueValue; dequeueValue = queue.Dequeue(); // return Hello World (string) dequeueValue = queue.Dequeue(); // return 5 (int) dequeueValue = queue.Dequeue(); // return 1d (double) dequeueValue = queue.Dequeue(); // return true (bool) dequeueValue = queue.Dequeue(); // return Product (Product type) بخلاف التابع Dequeue()، يُخرِج التابع Peek() أول عنصر بالرَتَل دون حَذفه منه. Queue<int> queue = new Queue<int>(); queue.Enqueue(10); queue.Enqueue(20); var lastValueAdded = queue.Peek(); // 10 يُطبّق التِكرار (iterate) على عناصر الرَتَل وفقًا لمفهوم “الداخل أولًا، يخرج أولًا“ (FIFO) وبدون حذفها، كاﻵتي: Queue<int> queue = new Queue<int>(); queue.Enqueue(10); queue.Enqueue(20); queue.Enqueue(30); queue.Enqueue(40); queue.Enqueue(50); foreach (int element in queue) { Console.WriteLine(i); } خَرْج الشيفرة باﻷعلى يكون كالتالي: 10 20 30 40 50 القاموس (Dictionary) تهيئة القاموس باستخدام مُهَيِّئ التَجمِيعَات (Collection Initializer): // Translates to `dict.Add(1, "First")` etc. var dict = new Dictionary<int, string>() { { 1, "First" }, { 2, "Second" }, { 3, "Third" } }; // Translates to `dict[1] = "First"` etc. // Works in C# 6.0. var dict = new Dictionary<int, string>() { [1] = "First", [2] = "Second", [3] = "Third" }; الإضافة للقاموس Dictionary<int, string> dict = new Dictionary<int, string>(); dict.Add(1, "First"); dict.Add(2, "Second"); // To safely add items (check to ensure item does not already exist - would throw) if(!dict.ContainsKey(3)) { dict.Add(3, "Third"); } يُمكِن للمُفهرِس (Indexer) الإضافة للقاموس أيضًا كبديل عن التابع Add. يَبدو المُفهرِس، من الداخل، كأيّ خَاصية (Property) تَملُك جَالِب (getter) وضَابِط (setter) خاص بها، بِخلاف كونهما يَستقبِلان مُعامِلات من أي نوع تُحدِّد بين قوْسين مَعقُوفَين []. لاحظ المثال التالي: Dictionary<int, string> dict = new Dictionary<int, string>(); dict[1] = "First"; dict[2] = "Second"; dict[3] = "Third"; عِند مُحاولة إضافة عنصر مفتاحه (Key) مَوجُود مُسبَقًا بالقاموس، يَستبدِل المُفهرِس القيمة الجديدة بالقيمة الموجودة، هذا بِخلاف التابع Add الذي يُبلِّغ عن اعتراض (Exception). لتَنشِئة قاموس آمن خيطيًا، استخدم الصنف ConcurrentDictionary<TKey, TValue>، كالآتي: var dict = new ConcurrentDictionary<int, string>(); dict.AddOrUpdate(1, "First", (oldKey, oldValue) => "First"); جلب قيمة من القاموس انظر لشيفرة التهيئة التالية: var dict = new Dictionary<int, string>() { { 1, "First" }, { 2, "Second" }, { 3, "Third" } }; قَبل مُحاولة استخدام مفتاح (key) لجَلْب قيمته (value) المُناظِرة من القاموس، يُمكِنك أولًا استخدام التابع ContainsKey لاختبار وُجُود المفتاح؛ وذلك لِتجَنُب التَبلِّيغ عن اعتراض من النوع KeyNotFoundException في حالة عَدَم وُجُود المفتاح. if (dict.ContainsKey(1)) Console.WriteLine(dict[1]); تُعانِي الشيفرة باﻷعلى، مع ذلك، مِن عَيْب البَحَث بالقاموس مَرتين (مرَّة لاختبار وجود المفتاح ومرَّة للجَلْب الفِعلِيّ للقيمة). إذا كان القاموس (Dictionary) كبيرًا، فسيُؤثِر ذلك على مستوى أداء الشيفرة. لحسن الحظ، يُمكِن للتابع TryGetValue إجراء العَمليتين (البحث والجَلْب) سَويًا، كالتالي: string value; if (dict.TryGetValue(1, out value)) Console.WriteLine(value); جعل القاموس Dictionary<string, T> لا يَتأثَر بحالة حروف المفاتيح (keys) var MyDict = new Dictionary<string,T>(StringComparison.InvariantCultureIgnoreCase) تعديد (Enumerating) القاموس يُمكِنك تَعديد قاموس بِطَرِيقة مِن ثَلَاث: الطريقة اﻷولى: استخدام أزواج KeyValue: Dictionary<int, string> dict = new Dictionary<int, string>(); foreach(KeyValuePair<int, string> kvp in dict) { Console.WriteLine("Key : " + kvp.Key.ToString() + ", Value : " + kvp.Value); } الطريقة الثانية: استخدام الخاصية Keys: Dictionary<int, string> dict = new Dictionary<int, string>(); foreach(int key in dict.Keys) { Console.WriteLine("Key : " + key.ToString() + ", Value : " + dict[key]); } الطريقة الثالثة: استخدام الخاصية Values: Dictionary<int, string> dict = new Dictionary<int, string>(); foreach(string s in dict.Values) { Console.WriteLine("Value : " + s); } تحويل الواجهة IEnumerable إلى قاموس إصدار .NET 3.5 أو أحدث يُنشئ التابع ToDictionary نُسخَة من النوع Dictionary,> من الواجهة IEnumerable كالتالي: using System; using System.Collections.Generic; using System.Linq; public class Fruits { public int Id { get; set; } public string Name { get; set; } } var fruits = new[] { new Fruits { Id = 8 , Name = "Apple" }, new Fruits { Id = 3 , Name = "Banana" }, new Fruits { Id = 7 , Name = "Mango" }, }; // Dictionary<int, string> key value var dictionary = fruits.ToDictionary(x => x.Id, x => x.Name); تحويل القاموس إلى قائِمة (List) تَنشِّئة قائمة من زوج مفتاح/قيمة KeyValuePair: Dictionary<int, int> dictionary = new Dictionary<int, int>(); List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(); list.AddRange(dictionary); تَنشِّئة قائمة من مفاتيح القاموس (keys): Dictionary<int, int> dictionary = new Dictionary<int, int>(); List<int> list = new List<int>(); list.AddRange(dictionary.Keys); تَنشِّئة قائمة من قيم القاموس (values): Dictionary<int, int> dictionary = new Dictionary<int, int>(); List<int> list = new List<int>(); list.AddRange(dictionary.Values); الحذف من القاموس انظر لشيفرة التهيئة التالية: var dict = new Dictionary<int, string>() { { 1, "First" }, { 2, "Second" }, { 3, "Third" } }; يَحذِف التابع Remove مفتاحًا مُعَيَنًا وقِيمته من القاموس كالآتي: bool wasRemoved = dict.Remove(2); يُعِيد التابع Remove قِيمَة مِن النوع boolean، تكون true إذا ما وَجَدَ التابع المفتاح المُحدَّد واستطاع حَذفُه من القاموس. أو تكون false إذا لم يَجِده (مما يعّنِي أنه لا يُبلِّغ عن اعتراض). من غَيْر الصحيح أن تُحِاول حَذْف مفتاح من القاموس بإِسنَاد القيمة الفارغة null إليه، كالآتي: // WRONG WAY TO REMOVE! dict[2] = null; سَتَستبدِل الشيفرة باﻷعلى القيمة الفارغة null بقيمة المفتاح السابقة فقط دُون الحَذفُ الفِعِلِّي للمفتاح. استخدم التابع Clear لحذف جميع المفاتيح والقيم من القاموس، كالآتي: dict.Clear(); بالرغم من أن التابع Clear يُحرِر جميع مَراجِع العناصر الموجودة بالقاموس، ويُعيد ضَبْط قيمة الخاصية Count للصفر، فإنه لا يُحرِر سَعَة المصفوفة الداخلية وتظل كما هي. فحص وجود مفتاح بالقاموس يُجري التابع ContainsKey(TKey) اختبار وُجُود مفتاح (key) بقاموس (Dictionary). يُمَرَّر المفتاح كمُعامِل من النوع TKey للتابع الذي يُعيد قيمة منطقية bool تُحدد ما إذا كان المفتاح موجودًا أم لا. var dictionary = new Dictionary<string, Customer>() { {"F1", new Customer() { FirstName = "Felipe", ...}}, {"C2", new Customer() { FirstName = "Carl", ... }}, {"J7", new Customer() { FirstName = "John", ... }}, {"M5", new Customer() { FirstName = "Mary", ... }}, }; لاختبار وجود المفتاح C2 بالقاموس: if (dictionary.ContainsKey("C2")) { // exists } تُدعِم النُسخة المُعمَّمة (Generic) من القاموس Dictionary,> هذا التابع ContainsKey أيضًا. القاموس المتزامن ConcurrentDictionary<TKey, TValue> يتوفَّر -منذ إصدار .NET 4.0 أو أحدث- القاموس المُتَزَامِن وهو تَجمِيعة آمِنة خيطيًا (thread-safe) مُكوَّنة مِن أزواج مفتاح/قيمة (key/value pairs)، بحيث يمكن لخُيُوط (threads) مُتعدِّدة أن تَلِج إليها وُلُوجًا مُتَزَامِنًا. إنشاء نسخة مِثِل تَنشِئة نسخة من Dictionary<TKey, TValue>، كالآتي: var dict = new ConcurrentDictionary<int, string>(); إِضافة أو تحدِيث ربما تُفَاجئ بِعَدَم وجود تابع Add، حيث يُجرِي التابع AddOrUpdate كِلَا العَمَليتين. في الواقع، تتوفر بصمتين (Method Overloading) من التابع AddOrUpdate. اﻷولى: AddOrUpdate(TKey key, TValue, Func<TKey, TValue, TValue> addValue) إذا لم يَكُن المفتاح موجودًا مُسبَقًا، يُضِيف هذا الشكل من التابع زوج مفتاح/قيمة جديد. أما إذا كان موجودًا، يُحدِّثه مُعامِل الدالة المُمَرَّر للتابع addValue. الثانية: AddOrUpdate(TKey key, Func<TKey, TValue> addValue, Func<TKey, TValue, TValue> updateValueFactory) تُحَدِث أو تُضيِف مُعامِلات الدوال، المُمَرَّرة لهذا الشكل من التابع، زوج مفتاح/قيمة (key/value pair) إذا كان المفتاح موجودًا مسبقًا أم لا على الترتيب. تُضُيِف أو تُحَدِث البصمة الأولى من التابع قيمة مُفتاح مُعين بغض النظر عن القيمة السابقة إن وُجِدّت، كالآتي: string addedValue = dict.AddOrUpdate(1, "First", (updateKey, valueOld) => "First"); كالمثال باﻷعلى، تُضُيِف البصمة الأول من التابع قيمة مفتاح معين، لكنها في حالة التحديث، تَعتمِد على القيمة السابقة للمفتاح كالآتي: string addedValue2 = dict.AddOrUpdate(1, "First", (updateKey, valueOld) => $"{valueOld} Updated"); يُمكِن للبصمة الثانية من التابع الإضافة أيضًا من خلال مُعامِل دَالة مُمَرَّر لها كالآتي: string addedValue3 = dict.AddOrUpdate(1, (key) => key == 1 ? "First" : "Not First", (updateKey, valueOld) => $"{valueOld} Updated"); جلب قيمة (value) مثل جَلْب قيمة من Dictionary<TKey, TValue>، كالآتي: string value = null; bool success = dict.TryGetValue(1, out value); جلب مع إضافة قيمة تتوفر بصمتين من التابع GetOrAdd لجلب قيمة مفتاح معين أو إضافته للقاموس بطريقة آمِنة خيطيًا. تَجلْب البصمة الأولى من التابع GetOrAdd قيمة المفتاح 2 أو تضيفها إن لم يكن المفتاح موجودًا بالقاموس كالآتي: string theValue = dict.GetOrAdd(2, "Second"); كالمثال السابق، تَجلْب البصمة الثانية من التابع قيمة المفتاح 2 إن وُجِدَّت، لكنها تُضِيفها باستخدام مُعامِل الدالة المُمَرَّر لها إن لم تَكُن موجودة، كالآتي: string theValue2 = dict.GetOrAdd(2, (key) => key == 2 ? "Second" : "Not Second." ); تعزيز القاموس المتزامن باستخدام الإرجاء (Lazy) لخفض الحوسبة المكررة المُشكِلة: يَبزُغ دور القاموس المُتَزَامِن ConcurrentDictionary عندما يكون المفتاح المطلوب مُخزنًا بالذاكرة المخبئيِّة (cache) فيُعَاد الكائن المناظر للمفتاح فوريًا عادةً بدون قِفل (lock free). لكن ماذا لو لم يكن المفتاح موجودًا بالذاكرة المخبئيِّة (cache misses) وكانت عملية تنشئة الكائن مُكلِفة بصورة تفوق كُلفة تبديل سياق الخيط (thread-context switch)؟ في هذه الحالة، إذا طَلَبَت عدة خيوط (threads) الوُلوج إلى نفس المفتاح، سيُنشّئ كلًا منها كائنًا. وبالنهاية، سيُضاف إحداها فقط إلى التَجمِيعَة بينما ستُهمل بقية الكائنات، مما يعني هَدْر أكثر من مورد بصورة غير ضرورية سواء كان مورد وحدة المعالجة المركزية (CPU) لتنشئة تلك الكائنات أو مورد الذاكرة لتخزينها بشكل مؤقت، بالاضافة إلى هَدْر بعض الموارد الأخرى. الحل: لتجاوز تلك المشكلة، يُمكِن الجمع بين القاموس المُتَزَامِن ConcurrentDictionary<TKey, TValue> والتهيئة المُرجأة باستخدام الصنف Lazy<TValue>. الفكرة ببساطة تعتمد على كَوْن تابع القاموس المُتَزَامِن GetOrAdd يُعيِد فقط القيم المُضافة فعليًا للتَجمِيعَة. لاحظ أنه، في هذه الحالة أيضًا، قد تُهْدَر الكائنات المُرجأة (Lazy objects)، لكن لا يُمكن عَدَّ ذلك مشكلة كبيرة، وبخاصة أن الكائنات المُرجأة ليست مُكلِفة بالموازنة مع الكائن الفِعلِي، وبالطبع أنت ذكي كفاية لألا تَطلُب خاصية Value الموجودة بتلك الكائنات، وإنما يُفترض بك أن تطلبها فقط للكائنات العائدة من التابع GetOrAdd مما يَضمن كوْنها مُضافة فِعليًا للتَجميعة. public static class ConcurrentDictionaryExtensions { public static TValue GetOrCreateLazy<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> d, TKey key, Func<TKey, TValue> factory) { return d.GetOrAdd( key, key1 => new Lazy<TValue>(() => factory(key1), LazyThreadSafetyMode.ExecutionAndPublication)).Value; } } قد يكْون التخزين المؤقت (caching) للكائنات من الصنف XmlSerializer بالتحديد مُكلِفًا، مع وجود كثير من التنازع (contention) أثناء بدء تشغيل (startup) البرنامج. علاوة على ذلك، إذا كانت المُسَلسِلَات مُخصّصة (custom serializers)، ستُسَرَّب الذاكرة (memory leak) أثناء بقية دورة حياة العمليّة (process lifecycle). الفائدة الوحيدة من استخدام القاموس المُتزامِن ConcurrentDictionary في هذه الحالة هو تجنب الأقفال (locks) أثناء بقية دورة حياة العمليّة (process lifecycle)، ولكن لن يكون وقت بدء تشغيل البرنامج واستهلاك الذاكرة مقبولًا. وهنا يَبزُغ دور القاموس المتزامن المُعَّزز بالإِرجَاء المُذكور سلفًا. private ConcurrentDictionary<Type, Lazy<XmlSerializer>> _serializers = new ConcurrentDictionary<Type, Lazy<XmlSerializer>>(); public XmlSerializer GetSerialier(Type t) { return _serializers.GetOrCreateLazy(t, BuildSerializer); } private XmlSerializer BuildSerializer(Type t) { throw new NotImplementedException("and this is a homework"); } تجمِيعات القراءة فقط ReadOnlyCollections تنشئة تجميعة قراءة فقط باستخدام بَانِي الكائن (Constructor) تُنشَّئ تجميعة قراءة فقط ReadOnlyCollection بتَمريِر كائن واجهة IList إلى بَانِي الكائن الخاص بالصنف ReadOnlyCollection، كالتالي: var groceryList = new List<string> { "Apple", "Banana" }; var readOnlyGroceryList = new ReadOnlyCollection<string>(groceryList); باستخدام استعلامات LINQ تُوفِر استعلامات LINQ تابع مُوسِّع AsReadOnly() لكائنات وَاجِهة IList، كالتالي: var readOnlyVersion = groceryList.AsReadOnly(); عادة ما تُريد الاحتفاظ بمَرجِع مَصْدَر التَجمِيعَة بِهَدف التعديل عليه خاصًا (private)، بينما تَسمَح بالولُوج العَلنِي (public) إلى تَجمِيعَة القراءة فقط ReadOnlyCollection. في حِين تستطيع تَنشِّئة نسخة القراءة فقط مِن قائمة ضِمنيّة (in-line list) كالمثال التالي، لن تكون قادرًا على تعديل التَجمِيعَة بعد إِنشَّائها. var readOnlyGroceryList = new List<string> {"Apple", "Banana"}.AsReadOnly(); عظيم! لكنك لن تكون قادرًا على تحديث القائمة لأنك لم تعد تمتلك مرجع للقائمة اﻷصلية. إذا كانت التَنشِّئة من قائمة ضِمنيّة مناسبة لغَرضَك، رُبما يُفترض بك استخدام هيكل بيانات آخر مثل التَجمِيعَة الثابتة ImmutableCollection. تحديث تجميعة قراءة فقط كما ذكرنا مُسبَقًا، لا يُمكنك تعديل تَجمِيعَة القراءة فقط ReadOnlyCollection مباشرة. بدلًا من ذلك، يُحَدَّث مَصْدَر التَجمِيعَة الذي بِدورِه يؤدي إلى تحديث تَجمِيعَة القراءة فقط. يُعد ذلك مِيزة رئيسية لتَجمِيعَات القراءة فقط ReadOnlyCollection. var groceryList = new List<string> { "Apple", "Banana" }; var readOnlyGroceryList = new ReadOnlyCollection<string>(groceryList); var itemCount = readOnlyGroceryList.Count; // تحتوي على عنصرين //readOnlyGroceryList.Add("Candy"); // خطأ تصريفي، لا يُمكن إضافة عناصر لكائن من الصنف ReadOnlyCollection groceryList.Add("Vitamins"); // لكن يمكن إضافتهم إلى التجميعة الأصلية itemCount = readOnlyGroceryList.Count; // الآن، تحتوي على ثلاثة عناصر var lastItem = readOnlyGroceryList.Last(); // أصبح العنصر اﻷخير بتجميعة القراءة فقط هو "Vitamins" مِثال حيّ عناصر تجمِيعات القراءة فقط ليست بالضرورة للقراءة فقط إذا كانت عناصر مَصْدَر التَجمِيعَة من نوع غَيْر ثابِت (mutable)، فعِندها يُمكن الولُوج إليها من خلال تَجمِيعَة القراءة فقط وتعديلها. public class Item { public string Name { get; set; } public decimal Price { get; set; } } public static void FillOrder() { // An order is generated var order = new List<Item> { new Item { Name = "Apple", Price = 0.50m }, new Item { Name = "Banana", Price = 0.75m }, new Item { Name = "Vitamins", Price = 5.50m } }; // The current sub total is $6.75 var subTotal = order.Sum(item => item.Price); // Let the customer preview their order var customerPreview = new ReadOnlyCollection<Item>(order); // لا يمكن إضافة أو حذف عناصر من تجميعة القراءة فقط لكن يمكن التعديل على قيمة متغير السعر customerPreview.Last().Price = 0.25m; // The sub total is now only $1.50! subTotal = order.Sum(item => item.Price); } مِثال حيّ ترجمة -وبتصرف- للفصول: Dictionaries Collections ReadOnlyCollections من كتاب .NET Framework Notes for Professionals
-
عدم قابلية السلاسل النصية للتغيير تُعدّ السلاسل النصية غير قابلة للتغيير immutable؛ بمعنى أنه لا يمكن التلاعب بسلسلة نصية بتغيير أحد المحارف فيها. عند إجراء أي عملية على السلسلة النصية، يُنشَئ نسخة جديدة منها بعد التعديل؛ مما يعني أنه عند استبدال حرف واحد بـسلسة نصية طويلة، ستُعيَّن مساحة بالذاكرة للقيمة الجديدة. // تعيين مساحة بالذاكرة string veryLongString = ... // حذف أول حرف من السلسلة النصية string newString = veryLongString.Remove(0,1); إذا أردت إجراء عدد كبير من العمليات على قيمة سلسلة نصية، استخدم الصنف StringBuilder المُعدّ خصيصًا للتعديل على السلاسل النصية بكفاءة. var sb = new StringBuilder(someInitialString); foreach(var str in manyManyStrings) { sb.Append(str); } var finalString = sb.ToString(); عد الحروف إذا كنت بحاجة إلى عَدّ الحروف بسلسلة نصية، فلا يُمكِنك ببساطة استخدام الخاصية Length؛ لأنها تَحسِب عدد العناصر بمصفوفة محارف. لا تُمثِّل هذه المصفوفة الحروف كما نعهدها، وإنما تُمثِّل العدد البتي للمحرف code-unit (ليس محارف اليونيكود Unicode code-points ولا الوحدات الكتابية graphemes). لذلك فإن الشيفرة الصحيحة تكون كالتالي: int length = text.EnumerateCharacters().Count(); يُمكِن لتحسين بسيط إعادة كتابة التابع المُوسِّع EnumerateCharacters؛ بحيث يكون مُعَدّ خصيصًا لحِساب عدد المحارف: public static class StringExtensions { public static int CountCharacters(this string text) { if (String.IsNullOrEmpty(text)) return 0; int count = 0; var enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) ++count; return count; } } عد الحروف غير المكررة على سبيل المثال، إذا استخدمت text.Distinct().Count()، سَتحصُل على نتائج خاطئة. الشيفرة الصحيحة كالتالي: int distinctCharactersCount = text.EnumerateCharacters().Count(); خطوة إضافية هي عَدّ مرات تكرار كل حرف على حدى. يُمكِنك ببساطة استخدام الشيفرة التالية إذا لم يكن عامل اﻷداء مهمًا: var frequencies = text.EnumerateCharacters() .GroupBy(x => x, StringComparer.CurrentCultureIgnoreCase) .Select(x => new { Character = x.Key, Count = x.Count() }; تحويل السلسلة النصية من وإلى ترميز آخر السلاسل النصية بالـ .NET هي بالأساس مصفوفة محارف System.Char (عبارة عن بتات الترميز UTF-16). إذا أردت حفظ النصوص أو التعامل معها بترميز آخر، يجب أن تتعامل مع مصفوفة بايتات System.Byte. تُحَوِّل أصناف مُشتَقَة من System.Text.Encoder و System.Text.Decoder النصوص من وإلى ترميز آخر (من مصفوفة بايتات إلى سلسلة نصية بترميز UTF-16 والعكس). من المُعتاد أن تُستَدعَى كلًا من أداة الترميز وفكه سويًا، لذلك تُتضَمَن داخل أصناف مُشتَقَة من System.Text.Encoding؛ بحيث تُحوِّل هذه الأصناف من وإلى الترميزات الشائعة (UTF-8 و UTF-16 ..إلخ) أمثلة تحويل سلسلة نصية إلى ترميز UTF-8: byte[] data = Encoding.UTF8.GetBytes("This is my text"); تحويل بيانات بترميز UTF-8 إلى سلسلة نصية: var text = Encoding.UTF8.GetString(data); تغيير ترميز ملف نصي: تقرأ الشيفرة التالية محتويات ملف نصي بترميز UTF-8، ثم تحفظها مجددًا، ولكن بترميز UTF-16. إذا كان حجم الملف كبيرًا، فإن هذه الشيفرة ليست الحل الأمثل؛ لأنها ستنقل جميع محتويات الملف إلى الذاكرة: var content = File.ReadAllText(path, Encoding.UTF8); File.WriteAllText(content, Encoding.UTF16); موازنة السلاسل النصية بالرغم من أن النوع String نوع مَرجِعِي باﻷساس، يُقارِن عامل المساواة == قيمة السلسلة النصية وليس مَرجِعها. ربما تعلم أن السلسلة النصية ليست سوى مصفوفة محارف. لكن في المقابل، إن كنت تظن أن اختبار مساواة السلاسل النصية ومقارنتها النسبية يتم على مستوى المحارف حرفًا بحرف، فأنت على خطأ. في الواقع، تختلف عمليات الاختبار بحسب اللغة؛ فمن الممكن أن تتكافئ أكثر من متتالية محارف اعتمادا على اللغة. عند اختبار مساواة سلسلتين نصيتين، فكر مليًا قبل الاعتماد على تساوي خاصية الطول Length للسلسلتين بِحُسبَانِها دارة قصيرة short circuiting. تُستخدم الدارة القصيرة مع التعبيرات المنطقية logical expressions لاختصار حِساب قيمة التعبير إن كان ذلك مُمكنًا. فمثلًا، تُعيد العملية AND القيمة true فقط إذا آل كِلا مُعامليها للقيمة true. وبالتالي، إذا آل المعامل اﻷول للقيمة false، يُمكِن للدارة القصيرة أن تُعيد القيمة false كقيمة إجمالية للتعبير المنطقي دون الحاجة إلى حساب قيمة المعامل الثاني؛ ﻷن قيمته لم تعد مُؤثرة. إذا أردت تغيير السلوك الافتراضي لعملية الموازنة، يُمكِنك استخدام التحميلات الزائدة (method overloading) للتابع String.Equal، والتي تَستَقبِل مُعامِلًا إضافيًا من النوع StringComparison. عد مرات تكرار حرف معين لا يُمكِنك ببساطة استخدام الشيفرة التالية لحساب عدد مرات تكرار حرف معين (إلا إذا كنت ترغب بعَدّ مرات تكرار العدد البتي للمحرف): int count = text.Count(x => x == ch); في الواقع، تحتاج لدالة أكثر تعقيدًا مثل الدالة التالية: public static int CountOccurrencesOf(this string text, string character) { return text.EnumerateCharacters() .Count(x => String.Equals(x, character, StringComparer.CurrentCulture)); } لاحظ أنه من الضروري أن تُقارَنّ السلاسل النصية وفقًا لقواعد لغة بعينها، بعكس مقارنة الحروف والتي يمكن أن تتباين بحسب اللغة. تقسيم السلسلة النصية إلى كتل متساوية لا يُمكِنك تقسيم السلسلة النصية تَقسِيمًا عشوائيًا؛ لأن أي محرف System.Char بداخل المصفوفة قد يكون غير صالح بمفرده؛ إِمَّا لكونه محرف دمج أو جزء من زوج بدل surrogate pair. لذلك لابد للشيفرة أن تأخذ هذا بالحسبان. لاحظ أن المقصود من كتل متساوية هو تساوي عدد الوحدات الكتابية graphemes وليس عدد البتات للمحرف code-units. public static IEnumerable<string> Split(this string value, int desiredLength) { var characters = StringInfo.GetTextElementEnumerator(value); while (characters.MoveNext()) yield return String.Concat(Take(characters, desiredLength)); } private static IEnumerable<string> Take(TextElementEnumerator enumerator, int count) { for (int i = 0; i < count; ++i) { yield return (string)enumerator.Current; if (!enumerator.MoveNext()) yield break; } } التابع الإفتراضي Object.ToString كل الأنواع الموجودة في NET. هي أنواع مشتقة من النوع Object في نهاية المطاف الذي يحوي التابع ToString، لذا ترثه تلك الأنواع المشتقة تلقائيًا ويمكن إعادة كتابة تنفيذ خاص به لنوع محدَّد والخروج عن التنفيذ الافتراضي الذي يعيد اسم النوع افتراضيًا إن لم يكن هنالك ما يحوّله. المثال التالي يُعرف الصنف Foo، ونظرًا ﻷنه لم يُعيد كتابة تنفيذ التابع ToString، تم استدعاء التنفيذ الافتراضي فيكون الخْرج هو اسم الصنف Foo: public class Foo { } var foo = new Foo(); Console.WriteLine(foo); // outputs Foo يُستَدعَى التابع ToString ضمنيًا عند ضم قيمة إلى سلسلة نصية: public class Foo { public override string ToString() { return "I am Foo"; } } var foo = new Foo(); Console.WriteLine("I am bar and "+foo); // I am bar and I am Foo تُسجِل أدوات تنقيح الأخطاء debugging tools الأحَدَاث الحاصلة في البرنامج logs، لذلك فإنها عادة ما تحتاج للإشارة إلى الكائنات المُتفاعلة ضمن الحَدَث. لذا تلجأ لاستخدام التابع ToString. إذا أردت، لسبب ما، أن تُخصِّص طريقة عرض المُنقح debugger للقيم بدون أن تُعيد تعريف التابع، استخدم السمة DebuggerDisplay، انظر MSDN: // [DebuggerDisplay("Person = FN {FirstName}, LN {LastName}")] [DebuggerDisplay("Person = FN {"+nameof(Person.FirstName)+"}, LN {"+nameof(Person.LastName)+"}")] public class Person { public string FirstName { get; set; } public string LastName { get; set;} // ... } التعبيرات النمطية (Regular Expressions) التابع Regex.IsMatch لاختبار تطابق نمط (pattern) يَفحص التابع Regex.IsMatch ما إذا كانت السِلسِلة النصية المُعطاة متطابقة مع نَمط معين، ويُعيد قيمة منطقية تُحدِّد ذلك، كالمثال التالي: public bool Check() { string input = "Hello World!"; string pattern = @"H.ll. W.rld!"; // true return Regex.IsMatch(input, pattern); } تَتوفَّر بَصمة أُخْرى من التابع Regex.IsMatch تَسمَح بتمرير بعض الخيارات للتحكم بعملية الفحص، كالتالي: public bool Check() { string input = "Hello World!"; string pattern = @"H.ll. W.rld!"; // true return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); } التابع Regex.Match لاستخراج أول تطابق (match) يبحث التابع Regex.Match عن نمط (pattern) معين بالسِلسِلة النمطية المُعطاة، ويُعيد كائن من النوع Match يُمثِل أول ظهور (occurrence) للنمط بالسِلسِلة. يُمكِن الولوج للخاصيتين Value و Index بالقيمة المُعادة. تَحوي الأولى القيمة الكاملة لأول ظهور للنمط بينما تَحوي الثانية مَوضِع (position) ذلك الظهور أي أين ظهرت القيمة المتطابقة. تَستطيع أيضًا الولوج للخاصية Groups من النوع GroupCollection. تَحمِل هذه الخاصية دومًا عنصر واحد على الأقل، سيحتوي هذا العنصر على القيمة الكاملة لأول ظهور للنمط إن وجد أو قيمة فارغة في حالة عدم وجوده. قد تتواجد عناصر اخرى ضِمْن التَجمِيعَة إذا كنت قد عَرَّفت أي مجموعات بالنمط المُمرَّر مثل Subject كالمثال التالي: string input = "Hello World!"; string pattern = @"H.ll. (?<Subject>W.rld)!"; Match match = Regex.Match(input, pattern); Console.WriteLine(match.Value); // Hello World! Console.WriteLine(match.Index); // 0 Console.WriteLine(match.Groups["Subject"].Value); // World التابع Regex.Matches لاستخراج جميع التطابقات (matches) يَعمَل بصورة مشابهة للتابع Regex.Match لكنه لا يَقتصِر على إعادة أول ظهور (occurrence) للنمط بالسِلسِلة النصية، وإنما يُعيد قيمة من النوع GroupCollection تحتوي على جميع التطابقات (matches). انظر المثال التالي: using System.Text.RegularExpressions; static void Main(string[] args) { string input = "Carrot Banana Apple Cherry Clementine Grape"; // Find words that start with uppercase 'C' string pattern = @"\bC\w*\b"; MatchCollection matches = Regex.Matches(input, pattern); foreach (Match m in matches) Console.WriteLine(m.Value); } الخْرج: Carrot Cherry Clementine التابع Regex.Replace للاستبدال وفقًا لنمط يَستبدِل التابع Regex.Replace أجزاء من سِلسِلة نصية مُعطاة وفقًا لنمط يُمرَّر إليه، ثم يُعيد السِلسِلة النصية بعد عملية الاستبدال، كالمثال التالي: public string Check() { string input = "Hello World!"; string pattern = @"W.rld"; // Hello Stack Overflow! return Regex.Replace(input, pattern, "Stack Overflow"); } يُمكن استخدام التابع Regex.Replace لحَذْف الحروف الأبْجَعَددية (alphanumeric) من سِلسِلة نصية عن طريق تمرير النمط [^a-zA-Z0-9] للتابع وطلب استبداله بسِلسِلة نصية فارغة، كالتالي: public string Remove() { string input = "Hello./!"; return Regex.Replace(input, "[^a-zA-Z0-9]", ""); } ترجمة -وبتصرف- للفصل DateTime parsing من كتاب .NET Framework Notes for Professionals
-
التابع ParseExact يَستقبِل التابع DateTime.ParseExact مُعامِلًا ثالثًا، والذي يُحدِّد مَحلّيّة (Culture/Locale) صِيْغة سلسلة التنسيق (Format String)، مما يُمكِنك من تمرير قيمة مَحلّيّة مُحدّدة. يُؤدي تمرير القيمة الفارغة (null) أو القيمة CultureInfo.CurrentCulture إلى استخدام مَحلّيّة النظام. var dateString = "2015-11-24"; var date = DateTime.ParseExact(dateString, "yyyy-MM-dd", null); Console.WriteLine(date); 11/24/2015 12:00:00 AM صِيْغ سلسلة التنسيق (Format Strings) لابّد أن تَتَّلائَم السلسلة النصية المُعطاة مع صِيْغة سلسلة التنسيق (Format String). var date = DateTime.ParseExact("24|201511", "dd|yyyyMM", null); Console.WriteLine(date); 11/24/2015 12:00:00 AM إذا احتوت صِيْغة سلسلة التنسيق (Format String) على أيّة حروف غير مُحدِّدة للتنسيق (Format Specifiers)، فإنها تُعامَل كـسلسلة نصية مُجردَّة. var date = DateTime.ParseExact("2015|11|24", "yyyy|MM|dd", null); Console.WriteLine(date); 11/24/2015 12:00:00 AM يُفرّق مُحدِّد التنسيق (Format Specifiers) بين حالات الحرف (Case). فمثلًا، في المثال التالي، حُلّلت قيم كلًا من الشهر والدقيقة إلى مَقاصِد خاطئة. var date = DateTime.ParseExact("2015-01-24 11:11:30", "yyyy-mm-dd hh:MM:ss", null); Console.WriteLine(date); 11/24/2015 11:01:30 AM لابّد لصِيْغ سلسلة التنسيق (Format Strings) المُكونّة من حرف وحيد أن تكون قِياسية. var date = DateTime.ParseExact("11/24/2015", "d", new CultureInfo("en-US")); var date = DateTime.ParseExact("2015-11-24T10:15:45", "s", null); var date = DateTime.ParseExact("2015-11-24 10:15:45Z", "u", null); الاعتراضات (Exceptions) يُبَلَّغ عن اعتراض من النوع ArgumentNullException عند تمرير قيمة فارغة (null). var date = DateTime.ParseExact(null, "yyyy-MM-dd", null); var date = DateTime.ParseExact("2015-11-24", null, null); يُبَلَّغ عن اعتراض من النوع FormatException، إمِا لِكَوْن صِيغة سلسلة التنسيق غير صالحة، أو لعدم تَلاؤمها مع السلسلة النصية المُعطاة. var date = DateTime.ParseExact("", "yyyy-MM-dd", null); var date = DateTime.ParseExact("2015-11-24", "", null); var date = DateTime.ParseExact("2015-0C-24", "yyyy-MM-dd", null); var date = DateTime.ParseExact("2015-11-24", "yyyy-QQ-dd", null); // Single-character format strings must be one of the standard formats var date = DateTime.ParseExact("2015-11-24", "q", null); // Format strings must match the input exactly* (see next section) var date = DateTime.ParseExact("2015-11-24", "d", null); // Expects 11/24/2015 or 24/11/2015 for most cultures معالجة أكثر من صيغة سلسلة تنسيق محتملة var date = DateTime.ParseExact("2015-11-24T10:15:45", new [] { "s", "t", "u", "yyyy-MM-dd" }, // Will succeed as long as input matches one of these CultureInfo.CurrentCulture, DateTimeStyles.None); معالجة الاختلافات المحلية لصيغ سلسلة التنسيق var dateString = "10/11/2015"; var date = DateTime.ParseExact(dateString, "d", new CultureInfo("en-US")); Console.WriteLine("Day: {0}; Month: {1}", date.Day, date.Month); Day: 11; Month: 10 date = DateTime.ParseExact(dateString, "d", new CultureInfo("en-GB")); Console.WriteLine("Day: {0}; Month: {1}", date.Day, date.Month); Day: 10; Month: 11 التابع TryParse تَستَقبِل البصمة TryParse(string, out DateTime) كلًا من السلسلة النصية المراد تحليلها كمُعامِل دخْل، بالإضافة إلى مُتغير آخر كمُعامِل خْرج. يُحِاول التابع تحليل السلسلة النصية المُعطاة إلى النوع DateTime، ثم يُعيد قيمة منطقية Boolean تُحدِّد إذا ما نجح أم لا. تُسند نتيجة التحليل إلى مُتغير الخْرج إذا ما نجحت العملية، بينما تُسنَد القيمة الافتراضية DateTime.MinValue للمُتغير في حالة الفشل. DateTime parsedValue; if (DateTime.TryParse("monkey", out parsedValue)) { Console.WriteLine("Apparently, 'monkey' is a date/time value. Who knew?"); } في المثال التالي، يُحاول التابع تَحليل السلسلة النصية المُعطاة وفقًا لكُلًا من صِيْغ التنسيق (Formats) الشائعة مِثْل ISO 8601، وإعدادات مَحلّيّة النظام. DateTime.TryParse("11/24/2015 14:28:42", out parsedValue); // true DateTime.TryParse("2015-11-24 14:28:42", out parsedValue); // true DateTime.TryParse("2015-11-24T14:28:42", out parsedValue); // true DateTime.TryParse("Sat, 24 Nov 2015 14:28:42", out parsedValue); // true لمّا كان هذا التابع لا يَستقبِل قيمة تُحدِّد مَحلّيّة (Culture/Locale) صيْغة التنسيق (Format)، لذا فإنه يَعتمِد على إعدادات مَحلّيّة النظام، مما قد يُؤدي إلى نتائج غير متوقعة. في الأمثلة التالية، لاحظ كيف تُؤثِر مَحلّيّة النظام على قيمة الخُرج. // System set to en-US culture bool result = DateTime.TryParse("24/11/2015", out parsedValue); Console.WriteLine(result); False // System set to en-GB culture bool result = DateTime.TryParse("11/24/2015", out parsedValue); Console.WriteLine(result); False // System set to en-GB culture bool result = DateTime.TryParse("10/11/2015", out parsedValue); Console.WriteLine(result); True في المثال اﻷخير، لاحظ أنه إذا كنت تَمكُث في الولايات المتحدة الأمريكية بينما إعدادات مَحلّيّة نِظامك مَضبْوطة على en-GB، فقد تُفاجئ بأن النتيجة هي 10 نوفمبر وليس 11 أكتوبر. تَستقبِل بصمة اخرى من نفس التابع TryParse(string, IFormatProvider, DateTimeStyles, out DateTime) مُعامِلًا من النوع IFormatProvider بِخِلاف تابعه الشقيق، وذلك لتحديد مَحلّيّة (Culture) صِيْغة التنسيق (Format)، والتي تَؤول إلى مَحلّيّة النظام في حالة تمرير قيمة فارغة null. يَسمَح هذا التابع أيضًا بتحديد صِيْغة التنسيق من خلال مُعامِل من النوع تِعداد (Enum) من الصنف DateTimeStyles. if (DateTime.TryParse(" monkey ", new CultureInfo("en-GB"), DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite, out parsedValue) { Console.WriteLine("Apparently, ' monkey ' is a date/time value. Who knew?"); } اعتراضات (Exceptions) في بعض الحالات المُحدَّدة، قد يُبَلِّغ هذا التابع عن إعتراضات (Exceptions)، تَتَّعلّق بمُعامِلات الدخْل التي أُضيفت خلال التحميل الزائد للتابع (Method Overloading). بالتحديد مُعامِل الواجهة IFormatProvider ومُعامِل التعداد من الصنف DateTimeStyles. الاعتراضات المُحتملة: اعتراض من النوع NotSupportedException: إذا كانت قيمة وسيط المُعامِل IFormatProvider مُحايدة. اعتراض من النوع ArgumentException: إمّا لعدم صلاحية قيمة وسيط المُعامِل DateTimeStyles أو لاحِتواءُه على قيم مُتعارِضة مثل AssumeUniversal وAssumeLocal. التابع TryParseExact يُعدّ هذا التابع تَجميِعة لكلًا من التابعين TryParse وParseExact؛ فهو يَسمَح - مثل التابع ParseExact - بتحديد صِيْغ سلسلة التنسيق (Format Strings) مُخصصة، كما يُعِيد - مثل التابع TryParse - قيمة منطقية Boolean تُحدد إذا ما حُللّت السلسلة النصية بنجاح أم لا، بدلًا من التبلّيغ عن إعتراض (Exception) في حالة الفشل. تحاول البصمة TryParseExact(string, string, IFormatProvider, DateTimeStyles, out DateTime) من التابع تحليل السلسلة النصية المُعطاة وِفقًا لصِيْغة سلسلة التنسيق (Format String) مُحددة. لابّد للسلسلة النصية أن تَتَلائَم مع الصِيْغة لِضمان تحليل ناجح. DateTime.TryParseExact("11242015", "MMddyyyy", null, DateTimeStyles.None, out parsedValue); // true تحاول البصمة TryParseExact(string, string[], IFormatProvider, DateTimeStyles, out DateTime) من التابع تحليل السلسلة النصية المُعطاة وِفقًا لمصفوفة من صِيْغ سلسلة التنسيق (Format Strings). لابّد للسلسلة النصية أن تَتَلائَم مع صِيْغة واحدة على الأقل لِضمان تحليل ناجح. DateTime.TryParseExact("11242015", new [] { "yyyy-MM-dd", "MMddyyyy" }, null, DateTimeStyles.None, out parsedValue); // true ترجمة -وبتصرف- للفصل DateTime parsing من كتاب .NET Framework Notes for Professionals