يمكن القراءة والكتابة من وإلى الملفات النصيّة في سي شارب بعدّة طرق. سنتناول أسلوبًا بسيطًا وذلك من خلال الصنفين StreamWriter و StreamReader.
يسمح الصنف StreamWriter بالكتابة فقط، أمّا الصنف StreamReader فهو يسمح بالقراءة فقط. كما يمكن استخدام كلا الصنفين في نفس البرنامج. وكلّ منهما موجود ضمن نطاق الاسم System.IO.
يرث الصنف StreamWriter من الصنف TextWriter في حين يرث الصنف StreamReader من الصنف TextReader.
الكتابة إلى ملف نصي
سنعمل في البرنامج Lesson12_01 على إنشاء الملف data.txt وكتابة بعض الأسطر ضمنه:
1 using System; 2 using System.IO; 3 4 namespace Lesson12_01 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string[] lines = { "First Line", "Second Line", "Third Line" }; 11 12 StreamWriter fileWriter = new StreamWriter("data.txt"); 13 14 foreach (string line in lines) 15 { 16 fileWriter.WriteLine(line); 17 } 18 19 fileWriter.Close(); 20 } 21 } 22 }
صرّحنا في السطر 10 عن المصفوفة lines التي عناصرها نصوص، لاحظ أنّنا استخدمنا الطريقة المختصرة لإنشاء كائن مصفوفة وإسناد العناصر الموجودة ضمن الحاضنة {} إلى عناصره مباشرةً.
نصرّح في السطر 12 عن المتغيّر fileWriter حيث نسند إليه كائن من الصنف StreamWriter. عند إنشاء هذا الكائن، مرّرنا اسم الملف "data.txt" إلى بانية الصنف StreamWriter. في الحقيقة تخضع هذه البانية إلى زيادة التحميل overloading، حيث تمتلك ثمانية أشكال مختلفة اخترنا أبسطها، وهو مسار واسم الملف المراد إنشاؤه. وبما أنّنا مرّرنا الاسم فقط دون المسار، فسيتم إنشاء هذا الملف في نفس المجلّد الموجود ضمنه الملف التنفيذي للبرنامج.
بعد ذلك نستخدم التابع WriteLine من المتغيّر fileWriter لكتابة عناصر المصفوفة lines على أسطر منفصلة ضمن الملف data.txt. العبارة البرمجيّة في السطر 19 ضرورية لإغلاق الملف باستدعاء التابع Close وتحرير المصدر الذي يحجزه في نظام التشغيل. جرّب تنفيذ البرنامج، لن تحصل على شيء على الشاشة، ولكن إذا فتحت الملف data.txt (ستجده غالبًا ضمن bindebug ضمن مجلّد المشروع) ستجد الأسطر الثلاثة موجودةً ضمنه.
ملاحظة: يوجد شكل آخر لبانية الصنف StreamWriter يقبل بالإضافة إلى اسم الملف ومساره قيمة منطقيّة (من نوع bool) تُدعى append. إذا مرّرت true مكانها فسيعمل البرنامج إلى الإضافة إلى محتويات الملف data.txt، أمّا إذا مرّرت false فسيعمل على الكتابة عليه. أمّا إذا أهملت هذا الشكل تمامًا كما هو الحال في مثالنا فسيعمل البرنامج على الكتابة على الملف، أي استبدال محتوياته، في كلّ مرّة ننفّذ فيها البرنامج.
في الحقيقة ليس هذا هو الاستخدام الأمثل للصنف StreamWriter والسبب في ذلك أنّ مصادر نظام التشغيل محدودة، حيث يؤدّي التعامل مع الملفات إلى حجز بعض من هذه المصادر، لذلك ينبغي تحرير هذه المصادر فورًا عندما تنتفي الحاجة إليها. قد يبدو أنّنا قد فعلنا ذلك باستخدام التابع Close وهذا صحيح تمامًا، ولكن ليس بالسرعة القصوى الممكنة! هناك أسلوب آخر يسمح بتحرير المصادر بشكل أكثر فعاليّة وسرعة باستخدام الكلمة المحجوزة using. سأعدّل البرنامج Lesson12_01 ليستخدم هذا الأسلوب الجديد. انظر البرنامج Lesson12_02 بعد التعديل:
1 using System; 2 using System.IO; 3 4 namespace Lesson12_02 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 string[] lines = { "First Line", "Second Line", "Third Line" }; 11 12 using (StreamWriter fileWriter = new StreamWriter("data.txt")) 13 { 14 foreach (string line in lines) 15 { 16 fileWriter.WriteLine(line); 17 } 18 } 19 } 20 } 21 }
لاحظ السطر 12 كيف وضعنا عبارة التصريح عن المتغيّر fileWriter والإسناد إليه ضمن عبارة using. في الواقع لن يكون المتغيّر fileWriter مرئيًّا خارج حاضنة using (من السطر 13 حتى السطر 18)، وبمجرّد وصول تنفيذ البرنامج إلى السطر 19 سيتم إغلاق الملف فورًا وتحرير المصدر الذي يحجزه. يظهر من البرنامج السابق أنّنا لم نعد نحتاج إلى استخدام التابع Close.
القراءة من ملف نصي
سنستخدم الصنف StreamReader لهذا الغرض. سيعمل البرنامج Lesson12_03 على قراءة محتويات الملف data.txt السابق وعرضها على الشاشة:
1 using System; 2 using System.IO; 3 4 namespace Lesson12_03 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 using (StreamReader fileReader = new StreamReader("data.txt")) 11 { 12 while(!fileReader.EndOfStream) 13 { 14 string line = fileReader.ReadLine(); 15 Console.WriteLine(line); 16 } 17 } 18 } 19 } 20 }
تخضع بانية الصنف StringReader أيضًا لزيادة التحميل، حيث تمتلك 11 شكلًا مختلفًا تسمح للمبرمج بالتحكّم الكامل بكيفيّة القراءة من الملف. أبسط هذه الأشكال هو الشكل الذي استخدمناه في البرنامج Lesson12_03 حيث سنمرّر لهذه البانية اسم الملف data.txt الذي أنشأناه في البرنامج Lesson12_02 السابق. استخدمنا في هذا البرنامج أيضًا العبارة using (من السطر 10 حتى السطر 17) لتحرير المصدر الذي يحجزه الملف عند الانتهاء من القراءة. ننشئ كائن من الصنف StreamReader ونسنده إلى المتغيّر fileReader ضمن عبارة using في السطر 10. ثمّ نستخدم حلقة while لقراءة محتويات الملف، وذلك لأنّنا من الناحية النظريّة لا نعلم بالتحديد كم سطرًا يحوي الملف. لاحظ شرط استمرار حلقة while حيث تُرجع الخاصيّة EndOfStream للمتغيّر fileReader القيمة true إذا وصلنا إلى نهاية الملف أثناء عمليّة القراءة، وإلّا فإنّها تُرجع false. إذًا، في حال لم نصل بعد إلى نهاية الملف ستُرجع الخاصيّة القيمة EndOfStream القيمة false وبسبب وجود عامل النفي المنطقي (!) قبل هذه الخاصيّة مباشرةً، ستكون القيمة النهائيّة لهذا التعبير هو true مما يسمح لحلقة while بالاستمرار. أمّا عند الوصول إلى نهاية الملف سيحدث العكس تماماً مما يجعل شرط استمرار الحلقة false وينتهي تنفيذ الحلقة.
نقرأ في السطر 14 سطرًا من الملف data.txt ونسنده إلى المتغيّر النصي line في كل دورة، ثمّ نطبع محتويات هذا المتغيّر إلى الشاشة في السطر 15. استخدمنا في عمليّة قراءة سطر من الملف data.txt التابع ReadLine من المتغيّر fileReader الذي يعمل على قراءة سطر واحد في كلّ مرّة من الملف data.txt.
تمارين داعمة
تمرين 1
اكتب برنامجًا يطلب من المستخدم إدخال خمس جُمل، ثم يعمل على تخزين هذه الجُمل على اعتبار أنّ كل جملة يُدخلها المستخدم تمثّل سطرًا نصيًّا. تخزّن كل جملة مع رقم سطرها بحيث يفصل بينهما محرف الجدولة (t).
تمرين 2
استفد من الصنف Student التالي:
class Student { public string Name { get; set; } public int Mark { get; set; } }
في كتابة برنامج يطلب من المستخدم إدخال بيانات خمسة طلّاب (اسم الطالب Name، والدرجة Mark) مستخدمًا المجموعة العموميّة <List<Student في تخزين بيانات هؤلاء الطلّاب (ستحتاج إلى استخدام نطاق الاسم System.Collections.Generic في بداية البرنامج). بعد الانتهاء من الإدخال، يجب على البرنامج حفظ بيانات هؤلاء الطلّاب ضمن الملف students.txt على شكل جدول بسيط، بحيث تصبح محتويات الملف مشابهة للشكل التالي:
Amjad 50 Mohammad 80 Mazen 90 Nour 88 Anwar 40
الخلاصة
تعرّفنا في هذا الدرس على المبادئ الأساسيّة في التعامل مع الملفات النصيّة. حيث تعلّمنا كيفيّة القراءة من الملف النصيّ وكيفيّة الكتابة إليه. وتعلّمنا أساليب مهمّة عند فتح الملف للقراءة أو الكتابة تتمثّل في استخدام العبارة using لكي نحرّر المصدر الذي يحجزه الملف فور الانتهاء من التعامل معه.