اذهب إلى المحتوى

رضوى العربي

الأعضاء
  • المساهمات

    114
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو رضوى العربي

  1. عادةً ما تُخزَّن إعدادات البرنامج بملف 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
  2. استخدام تقنية 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
  3. مُدير الحزم 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
  4. يعني حَقْن التَبَعيّات (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
  5. تُسهِل بيئة عمل ‎.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
  6. تُخزَّن الكائنات المُنشئة باستخدام العَامِل 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
  7. القراءة من والكتابة إلى المجارى القياسية يُمثِل النوع 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
  8. التقاط الاستثناءات 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
  9. الاستعلامات التكميلية اللغوية (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
  10. مهيئ التجميعات (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
  11. عدم قابلية السلاسل النصية للتغيير تُعدّ السلاسل النصية غير قابلة للتغيير 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
  12. التابع 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
  13. التصريف في الوقت المناسب (JIT compilation) يَعتمِد إطار عمل ‎.NET framework على التَصرِّيف في الوقت المناسب (Just-In-Time compilation)، والذي يَختلف عن كلًا من التَصرِّيف المُسبَق (Ahead-of-time compilation) والتفسير (Interpretation). باختصار، لا تُصرَّف الشيفرة المصدرية (source code) المكتوبة بأيّ من اللغات المُدعَّمة مثل C#‎ أو F#‎ أو Visual Basic إلى لغة الآلة (machine code) مُباشرة كما في التَصرِّيف المُسبَق (AOT compilation)، وإنما تُصرَّف أولًا إلى اللغة الوسيطة المشتركة (Common Intermediate Language (CIL، وهي لغة مُنخَفِضة المستوى وقريبة من لغة الآلة ولكنها غير مُرتبطة بمنصة (platform) معينة. تُنفَّذ شيفرة اللغة الوسيطة داخل بيئة تَّنفيذ افتراضية تُسمى بيئة التَّنفيذ المُشتركة Common Language Runtime (CLR) بإطار عمل ‎.NET framework. تَتضمَّن هذه البيئة مُصرِّف آني وقت التَّنفيذ JIT compiler، والذي يُصرِّف تلك اللغة الوسيطة أثناء زمن التَّنفيذ (runtime) إلى لغة الآلة الخاصة بالمنصة التي تُنفَّذ عليها الشيفرة. بيئة التنفيذ المشتركة (CLR) تُعدّ بيئة التَّنفيذ المشتركة Common Language Runtime (CLR)‎ جزءً أساسيًا من إطار عمل ‎.NET framework. وهي بالأساس بيئة تَّنفيذ افتراضية، وتَتضمَّن التالي: لغة بايتكود مَحمولة تُعرَف باسم اللغة المشتركة الوسيطة Common Intermediate Language (CIL or IL)‎. مُصرِّف آني وقت التَّنفيذ JIT compiler، يُحوِّل شيفرة البايت (bytecode) إلى لغة الآلة الفعلّية (machine code) عند الحاجة. كَانِس مُهملات (garbage collector) والذي يُوفِّر إدارة أوتوماتيكية للذاكرة. تَستخدِم عمليات فرعية (sub-processes) مُنفصلة تُعرَف باسم AppDomains لضمان عزل البرامج. تَستخدِم العديد من أساليب تَوفِّير الآمان مثل التَحقُّق من الشيفرة (verifiable code) ومستويات الثقة (trust levels). عادةً ما يُشار إلى الشيفرة التي تُنفَّذ داخل بيئة التَّنفيذ المشتركة (CLR) باسم الشيفرة المُدارة (managed code) بخلاف الشيفرة التي تُنفَّذ خارجها، ويُشار إليها بطبيعة الحال باسم الشيفرة غير المُدارة (unmanaged code). تَتوفَّر العديد من الطرائق لتسهيل التعامل بين الشيفرات من كلا النوعين. مثال "أهلًا بالعالم" على الرغم من شيوع استخدام كلًا من اللغات C#‎، و Visual Basic و F#‎ بإطار عمل ‎.NET، يُمكن أيضًا لأي لغة برمجية -متوافقة مع معايير البنية التحتية للغة الوسيطة (Common Language Infrastructure CLI)- أن تُصرَّف إلى اللغة الوسيطة المشتركة (CIL)، وتُنفَّذ ببيئة التَّنفيذ المُشتركة (CLR). لغة C#‎ using System; class Program { // هذه الدالة هي أول ما يُنفذ بالبرنامج static void Main() { // أرسل السلسلة النصية "أهلًا بالعالم" إلى الخرج القياسي Console.WriteLine("Hello World"); } } يوجد العديد من التحميلات الزائدة للتابع Console.WriteLine. في المثال باﻷعلى، السِلسِلة النصية Hello World هي مُعامِل للتابع. عند تنفيذ البرنامج، يُرسل التابع السِلسِلة النصية المُمررة إليه Hello World إلى مجرى الخْرج القياسي. تَستدعي بعض التحميلات الزائدة (overloads) الآخرى التابع ToString من خلال الوسيط، قبل أن تُرسل النتيجة إلى مجرى الخْرج القياسي. اطلع على توثيق إطار عمل .NET لمزيد من المعلومات. رابط مثال حي الشيفرة المكافئة باللغة الوسيطة IL (والتي ستُصرَّف باستخدام مُصرِّف آني وقت التَّنفيذ JIT): // Microsoft (R) .NET Framework IL Disassembler. Version 4.6.1055.0 // Copyright (c) Microsoft Corporation. All rights reserved. // Metadata version: v4.0.30319 .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 4:0:0:0 } .assembly HelloWorld { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // ....T..WrapNonEx 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows. // --- The following custom attribute is added automatically, do not uncomment ------- // .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 0A 48 65 6C 6C 6F 57 6F 72 6C 64 00 00 ) // ...HelloWorld.. .custom instance void [mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 0A 48 65 6C 6C 6F 57 6F 72 6C 64 00 00 ) // ...HelloWorld.. .custom instance void [mscorlib]System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 12 43 6F 70 79 72 69 67 68 74 20 C2 A9 20 // ...Copyright .. 20 32 30 31 37 00 00 ) // 2017.. .custom instance void [mscorlib]System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.InteropServices.ComVisibleAttribute::.ctor(bool) = ( 01 00 00 00 00 ) .custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = ( 01 00 24 33 30 38 62 33 64 38 36 2D 34 31 37 32 // ..$308b3d86-4172 2D 34 30 32 32 2D 61 66 63 63 2D 33 66 38 65 33 // -4022-afcc-3f8e3 32 33 33 63 35 62 30 00 00 ) // 233c5b0.. .custom instance void [mscorlib]System.Reflection.AssemblyFileVersionAttribute::.ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 ) // ...1.0.0.0.. .custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 1C 2E 4E 45 54 46 72 61 6D 65 77 6F 72 6B // ....NETFramework 2C 56 65 72 73 69 6F 6E 3D 76 34 2E 35 2E 32 01 // ,Version=v4.5.2. 00 54 0E 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 // .T..FrameworkDis 70 6C 61 79 4E 61 6D 65 14 2E 4E 45 54 20 46 72 // playName..NET Fr 61 6D 65 77 6F 72 6B 20 34 2E 35 2E 32 ) // amework 4.5.2 .hash algorithm 0x00008004 .ver 1:0:0:0 } .module HelloWorld.exe // MVID: {2A7E1D59-1272-4B47-85F6-D7E1ED057831} .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00020003 // ILONLY 32BITPREFERRED // Image base: 0x0000021C70230000 // =============== CLASS MEMBERS DECLARATION =================== .class private auto ansi beforefieldinit HelloWorld.Program extends [mscorlib]System.Object { .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldstr "Hello World" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class HelloWorld.Program Generated with MS ILDASM tool (IL disassembler) لغة F#‎ open System [<EntryPoint>] let main argv = printfn "Hello World" 0 رابط مثال حي لغة Visual Basic Imports System Module Program Public Sub Main() Console.WriteLine("Hello World") End Sub End Module رابط مثال حي لغة C++/CLI using namespace System; int main(array<String^>^ args) { Console::WriteLine("Hello World"); } اللغة الوسيطة IL .class public auto ansi beforefieldinit Program extends [mscorlib]System.Object { .method public hidebysig static void Main() cil managed { .maxstack 8 IL_0000: nop IL_0001: ldstr "Hello World" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } } لغة PowerShell Write-Host "Hello World" لغة Nemerle System.Console.WriteLine("Hello World"); لغة Python print "Hello World" import clr from System import Console Console.WriteLine("Hello World") لغة Oxygene namespace HelloWorld; interface type App = class public class method Main(args: array of String); end; implementation class method App.Main(args: array of String); begin Console.WriteLine('Hello World'); end; end. لغة Boo print "Hello World" 1.4: إصدارات إطار عمل ‎.NET إطار عمل ‎.NET الإصدار تاريخ الإصدار 1.0 13 فبراير 2002 1.1 24 أبريل 2003 2.0 07 نوفمبر 2005 3.0 06 نوفمبر 2006 3.5 19 نوفمبر 2007 3.5 SP1 11 أغسطس 2008 4.0 12 أبريل 2010 4.5 15 أغسطس 2012 4.5.1 17 أكتوبر 2013 4.5.2 05 مايو 2014 4.6 20 يوليو 2015 4.6.1 17 نوفمبر 2015 4.6.2 02 أغسطس 2016 4.7 05 أبريل 2017 4.7.1 17 أكتوبر 2017 إطار عمل ‎.NET Compact الإصدار تاريخ الإصدار 1.0 01 يناير 2000 2.0 01 أكتوبر 2005 3.5 19 نوفمبر 2007 3.7 01 يناير 2009 3.9 01 يونيو 2013 إطار عمل ‎.NET Micro الإصدار تاريخ الإصدار 4.2 04 أكتوبر 2011 4.3 04 ديسمبر 2012 4.4 20 أكتوبر 2015 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ترجمة -وبتصرف- للفصل Getting started with .NET Framework من كتاب ‎.NET Framework Notes for Professionals
×
×
  • أضف...