التدفقات وعمليات المجلدات بلغة C#
كل برنامج يتعامل مع الملفات يؤدي عملين: ينقل البايتات (bytes) إلى الملفات ومنها، ويجد طريقه بين المجلدات (directories) التي تحفظها. في .NET تتولى العمل الأول التدفقات (streams)، وتتولى الثاني الأصناف (classes) Directory وDirectoryInfo وFileInfo، وكلها في فضاء الأسماء (namespace) System.IO. يبني هذا الدرس المهارتين معا ببرامج صغيرة تتوقع مخرجاتها قبل أن تشغلها. بني كل برنامج في هذه الصفحة وشغل على ويندوز باستخدام .NET 8، وكل مخرجات معروضة هي المخرجات الحقيقية.
الأهداف
بنهاية هذا الدرس ينبغي أن تكون قادرا على:
- شرح معنى التدفق، والأنواع الثلاثة من أصناف التدفقات التي توفرها .NET: المخازن الخلفية (backing stores) والمحولات (adapters) والمزخرفات (decorators).
- تحرير الملف تحريرا صحيحا باستخدام
IDisposableوDispose()وجملةusing، وشرح ما تتحول إليهusing. - فتح
FileStreamبالقيم الصحيحة منFileModeوFileAccessوFileShare، وتوقعPositionوLengthبعد كل قراءة وكتابة وانتقال (seek). - قراءة النصوص وكتابتها باستخدام
StreamReaderوStreamWriter، والقيم الأولية (primitive values) باستخدامBinaryReaderوBinaryWriter. - استخدام دوال (methods) الصنف
Fileالتي تنجز العمل باستدعاء واحد، وضغط البيانات باستخدام المزخرفDeflateStream. - إنشاء شجرة مجلدات وسرد محتواها وقياس حجمها ونسخها والبحث فيها باستخدام
DirectoryوDirectoryInfoوFileInfo، بطريقة التعاود (recursion).
تحتاج كل البرامج إلى هذين السطرين في أعلاها:
using System;
using System.IO;الجزء 1: ما التدفق
التدفق سلسلة من البايتات
يستقبل البرنامج مدخلات، ويعالجها، وينتج مخرجات. والملف مجموعة من البيانات مخزنة على القرص تحت اسم ومسار مجلد. وعندما يفتح الملف للقراءة أو الكتابة يصبح تدفقا: سلسلة من البايتات تنتقل من مصدر إلى وجهة.
- يستخدم تدفق الإدخال (input stream) للقراءة، ويستخدم تدفق الإخراج (output stream) للكتابة.
- يمكن أن تخزن البيانات في ملف أو على الشبكة أو على الإنترنت، أو أن تمر عبرها. هذه هي المخازن الخلفية للبيانات المتدفقة. ويرى البرنامج التدفق نفسه من البايتات أيا كان المخزن.
- يمكن أن يكون التدفق قابلا للقراءة أو قابلا للكتابة أو كليهما. وبعض التدفقات تدعم الانتقال (seekable): فهي تحتفظ بموضع (position) حالي يمكن الاستعلام عنه وتغييره.
- كل أصناف التدفقات مشتقة من الصنف الأساسي المجرد
System.IO.Stream، ولهذا تبدوReadوWriteوSeekمتماثلة على كل أنواع التدفقات.
مثال محلول: حرف واحد، بايت واحد
ملف نصي جديد لا يحتوي إلا الحرف A. ما حجمه؟ ثم يحتوي A وأربع مسافات وz. ما حجمه الآن؟ توقع الإجابة، ثم اقرأ البرنامج ومخرجاته.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("sample.txt", "A");
Console.WriteLine("Size: " + new FileInfo("sample.txt").Length + " byte(s)");
File.WriteAllText("sample.txt", "A z");
Console.WriteLine("Size: " + new FileInfo("sample.txt").Length + " byte(s)");
byte[] data = File.ReadAllBytes("sample.txt");
foreach (byte b in data)
{
Console.WriteLine(b + " = " + Convert.ToString(b, 2).PadLeft(8, '0') + " = '" + (char)b + "'");
}
}
}المخرجات (ويندوز):
Size: 1 byte(s)
Size: 6 byte(s)
65 = 01000001 = 'A'
32 = 00100000 = ' '
32 = 00100000 = ' '
32 = 00100000 = ' '
32 = 00100000 = ' '
122 = 01111010 = 'z'يعرض المحرر حروفا، أما القرص فيخزن أرقاما. هنا يشغل كل حرف بايتا واحدا (8 بتات): A هو 65، أي 64 + 1، فبتاته 01000001. والمسافة هي 32، وz هو 122. ستة أحرف، ستة بايتات.
ثلاثة أنواع من أصناف التدفقات
في .NET عدد كبير جدا من أصناف التدفقات، لذا تجمعها في ثلاثة أنواع. ويأخذ هذا الدرس صنفا واحدا من كل نوع.
| النوع | الوظيفة | المثال المستخدم هنا |
|---|---|---|
| المخزن الخلفي | يتعامل مباشرة مع المصدر الخارجي (ملف، الشبكة) وينقل البايتات الخام | FileStream |
| المزخرف | يحول البايتات، كأن يضغطها مثلا، ويبقى هو نفسه تدفقا | DeflateStream |
| المحول | يقدم البايتات للتطبيق في شكل مفيد: نصوص، أسطر، أرقام | StreamReader، StreamWriter، BinaryReader، BinaryWriter |
تتدفق البايتات من المصدر الخارجي إلى المخزن الخلفي، وقد تمر عبر مزخرف أو أكثر، ثم تصل إلى البرنامج عبر محول. لماذا ثلاثة أنواع وليس صنفا واحدا كبيرا؟
- فصل الاهتمامات (separation of concerns). الصنف الذي يفعل كل شيء صنف "ملوث": صعب القراءة وصعب الصيانة.
- المحولات قوابس. كمحول السفر الذي يوضع بين المقبس والجهاز، يمكن استبدال المحول دون المساس بالتنفيذ الذي تحته.
- المزخرفات يمكن تكديسها. يمكن ربط أكثر من مزخرف بالتدفق نفسه وقت التشغيل.
أصناف System.IO
| الصنف | يستخدم من أجل |
|---|---|
FileStream | القراءة والكتابة من أي موضع داخل الملف |
BinaryReader | قراءة أنواع البيانات الأولية من تدفق ثنائي |
BinaryWriter | كتابة أنواع البيانات الأولية بالصيغة الثنائية في تدفق ثنائي |
StreamReader | قراءة الحروف من تدفق بايتات |
StreamWriter | كتابة الحروف في تدفق |
StringReader | القراءة من مخزن مؤقت (buffer) نصي |
StringWriter | الكتابة في مخزن مؤقت نصي |
DirectoryInfo | تنفيذ العمليات على المجلدات |
FileInfo | تنفيذ العمليات على الملفات |
الجزء 2: التنظيف باستخدام IDisposable وusing
الموارد المدارة وغير المدارة
تترجم شيفرة C# إلى لغة وسيطة (intermediate language)، وتشغلها بيئة التشغيل المشتركة (common language runtime, CLR). والشيفرة التي تعمل تحت CLR شيفرة مدارة (managed): يعثر جامع المهملات (garbage collector) على الكائنات التي لم تعد مطلوبة ويحرر ذاكرتها. أما الملف المفتوح فهو مقبض (handle) من نظام التشغيل. ولا يعرف جامع المهملات متى انتهيت منه، فلا يستطيع تحريره في الوقت المناسب، وإلى أن يحرر يبقى الملف مفتوحا ومقفلا.
الصنف الذي يحتفظ بمورد كهذا ينفذ الواجهة (interface) IDisposable ويوفر الدالة Dispose(). والقاعدة بسيطة لمن يستخدم الصنف: عندما تنتهي، استدع Dispose()، أو دع جملة using تستدعيها. والأصناف FileStream وStreamReader وStreamWriter وBinaryReader وBinaryWriter كلها تنفذ IDisposable.
مثال محلول: نمط التحرير
عندما يحتفظ صنفك بكائن قابل للتحرير، ينبغي أن ينفذ نمط التحرير (dispose pattern) الموصى به. يملك الصنف LogFile أدناه كائن FileStream، ويطبع سطرا كلما حدث شيء لتتمكن من تتبع الترتيب.
using System;
using System.IO;
class LogFile : IDisposable
{
private readonly FileStream fs; // holds an operating system file handle
private bool disposed = false;
public LogFile(string path)
{
fs = new FileStream(path, FileMode.Create, FileAccess.Write);
Console.WriteLine("LogFile: file opened");
}
public void Write(byte b)
{
fs.WriteByte(b);
}
public void Dispose() // step 2: the public method clients call
{
Dispose(true);
GC.SuppressFinalize(this); // cleanup done, the finalizer is not needed
}
protected virtual void Dispose(bool disposing) // step 3
{
if (disposed)
{
Console.WriteLine("LogFile: already disposed, nothing to do");
return;
}
if (disposing)
{
fs.Dispose(); // release the managed object we own
Console.WriteLine("LogFile: file closed");
}
disposed = true;
}
~LogFile() // safety net: the garbage collector calls it
{
Dispose(false);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("1) using block");
using (LogFile log = new LogFile("log.txt"))
{
log.Write(65);
Console.WriteLine("inside the block");
}
Console.WriteLine("after the block");
Console.WriteLine("2) try / finally");
LogFile log2 = null;
try
{
log2 = new LogFile("log.txt");
log2.Write(66);
}
finally
{
log2?.Dispose();
}
Console.WriteLine("3) Dispose called twice");
LogFile log3 = new LogFile("log.txt");
log3.Dispose();
log3.Dispose();
Console.WriteLine("4) using declaration");
using LogFile log4 = new LogFile("log.txt");
log4.Write(67);
Console.WriteLine("end of Main");
}
}للنمط خمسة أجزاء:
- ينفذ الصنف الواجهة
IDisposable. - الدالة
Dispose()العامة (public) هي ما يستدعيه مستخدمو الصنف. - الدالة
Dispose(bool disposing)المحمية الافتراضية (protected virtual) هي التي تؤدي العمل. تجعلهاprotectedمرئية للأصناف الفرعية، وتسمح لهاvirtualبتوسيعها. ويضمن العلمdisposedأن التنظيف يجري مرة واحدة فقط، مهما تكرر استدعاءDispose(). وعندما تكون قيمةdisposingهي true، تحرر أيضا الكائنات المدارة التي يملكها الصنف (هنا الـFileStream). - تخبر
GC.SuppressFinalize(this)جامع المهملات بأن التنظيف قد تم، فلا حاجة إلى استدعاء دالة الإنهاء (finalizer). - دالة الإنهاء
~LogFile()شبكة أمان: إذا نسي المستخدم استدعاءDispose()، استدعاها جامع المهملات، وهي تستدعيDispose(false).
توقع المخرجات قبل أن تقرأها. وعلى وجه الخصوص: ماذا يطبع الاستدعاء الثاني log3.Dispose()، وأين يظهر file closed بالنسبة إلى log4؟
المخرجات (ويندوز):
1) using block
LogFile: file opened
inside the block
LogFile: file closed
after the block
2) try / finally
LogFile: file opened
LogFile: file closed
3) Dispose called twice
LogFile: file opened
LogFile: file closed
LogFile: already disposed, nothing to do
4) using declaration
LogFile: file opened
end of Main
LogFile: file closedأربع طرق لتحرير المورد
| الطريقة | الشكل | الحكم |
|---|---|---|
استدعاء Dispose() بنفسك | في نهاية الشيفرة | غير موصى به: إذا رمي استثناء (exception) قبله، لا يجري الاستدعاء |
| try / finally | Dispose() في كتلة finally، مع ?. | ينفذ دائما؛ وتمنع ?. مرجعا فارغا (null reference) عندما يفشل المنشئ |
| كتلة using | using (var r = new ...) تليها كتلة | يحرر عند القوس المغلق للكتلة |
| تصريح using (C# 8) | using var r = new ...; | يحرر عند نهاية النطاق المحيط |
جملة using ليست سحرا: يحولها المترجم إلى صيغة try / finally الواردة في الصف الثاني بالضبط. هذه الكتلة
using (LogFile log = new LogFile("log.txt"))
{
log.Write(65);
}تترجم على هذا النحو
LogFile log = new LogFile("log.txt");
try
{
log.Write(65);
}
finally
{
log?.Dispose();
}استخدم صيغة الكتلة عندما يكون كل ما يليها خاصا بالمورد، واستخدم صيغة التصريح عندما ينبغي أن يبقى المورد حتى نهاية الدالة، كما يفعل log4 أعلاه: يطبع end of Main قبل file closed.
الجزء 3: FileStream، المخزن الخلفي
فتح FileStream
FileStream fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);تحدد التعدادات (enumerations) الثلاثة طريقة فتح الملف.
FileMode يحدد هل ينشأ الملف، أم يفتح، أم يقتطع، أم يضاف إلى نهايته:
| العضو | الملف غير موجود | الملف موجود |
|---|---|---|
Append | ينشئه | يفتحه ويضع الموضع عند النهاية |
Create | ينشئه | يكتب فوقه: يضيع المحتوى القديم |
CreateNew | ينشئه | يرمي IOException |
Open | يرمي FileNotFoundException | يفتحه |
OpenOrCreate | ينشئه | يفتحه |
Truncate | يرمي FileNotFoundException | يفتحه ويقتطعه إلى 0 بايت |
FileAccess يحدد ما ستفعله بالتدفق: Read أو Write أو ReadWrite.
FileShare يحدد ما يجوز للتدفقات الأخرى أن تفعله بالملف ما دام تدفقك مفتوحا: None (رفض المشاركة)، وRead، وWrite، وReadWrite، وInheritable (يجوز لعملية فرعية أن ترث المقبض).
مثال محلول: HELLO مع كل قيمة من FileMode
يجرب البرنامج أدناه كل قيمة من FileMode، أولا على ملف غير موجود، ثم على ملف يحتوي HELLO. يفتح الملف باستخدام FileAccess.Write، ويطبع الموضع والطول بعد الفتح مباشرة، ويكتب البايتين a وb، ثم يطبع الملف. توقع الأسطر الاثني عشر قبل قراءة المخرجات.
using System;
using System.IO;
class Program
{
static void Try(FileMode mode, bool exists)
{
string path = "test.txt";
if (File.Exists(path)) File.Delete(path);
if (exists) File.WriteAllText(path, "HELLO");
string label = mode + (exists ? " (file has HELLO)" : " (no file)");
try
{
using (var fs = new FileStream(path, mode, FileAccess.Write))
{
long pos = fs.Position, len = fs.Length;
fs.WriteByte((byte)'a');
fs.WriteByte((byte)'b');
fs.Close();
Console.WriteLine("{0,-32} Position={1} Length={2} -> after writing ab: {3}",
label, pos, len, File.ReadAllText(path));
}
}
catch (Exception ex)
{
Console.WriteLine("{0,-32} {1}", label, ex.GetType().Name);
}
}
static void Main(string[] args)
{
FileMode[] modes = { FileMode.Append, FileMode.Create, FileMode.CreateNew,
FileMode.Open, FileMode.OpenOrCreate, FileMode.Truncate };
foreach (FileMode m in modes) Try(m, false);
Console.WriteLine();
foreach (FileMode m in modes) Try(m, true);
}
}المخرجات (ويندوز):
Append (no file) Position=0 Length=0 -> after writing ab: ab
Create (no file) Position=0 Length=0 -> after writing ab: ab
CreateNew (no file) Position=0 Length=0 -> after writing ab: ab
Open (no file) FileNotFoundException
OpenOrCreate (no file) Position=0 Length=0 -> after writing ab: ab
Truncate (no file) FileNotFoundException
Append (file has HELLO) Position=5 Length=5 -> after writing ab: HELLOab
Create (file has HELLO) Position=0 Length=0 -> after writing ab: ab
CreateNew (file has HELLO) IOException
Open (file has HELLO) Position=0 Length=5 -> after writing ab: abLLO
OpenOrCreate (file has HELLO) Position=0 Length=5 -> after writing ab: abLLO
Truncate (file has HELLO) Position=0 Length=0 -> after writing ab: ab- يحتفظ
Appendبالبيانات القديمة ويكتب بعدها:HELLOab. - يتخلص
CreateوTruncateمن البيانات القديمة: يصبح الملفabفقط. - لا يفرغ
OpenوOpenOrCreateالملف. يبدأ الموضع عند 0، فيكتب البايتان فوقHوEويبقى الباقي:abLLO. - يرفض
CreateNewالملف الموجود، ويرفضOpenوTruncateالملف غير الموجود.
مثال محلول: أي العمليات ترمي استثناء
يحتوي data.txt على HELLO. تفتح كل حالة تدفقها الخاص وتفعل شيئا واحدا. أيها يرمي استثناء، وما الاستثناء؟
using System;
using System.IO;
class Program
{
static void Run(string title, Action action)
{
try
{
action();
Console.WriteLine(title + ": OK");
}
catch (Exception ex)
{
Console.WriteLine(title + ": " + ex.GetType().Name);
}
}
static void Main(string[] args)
{
File.WriteAllText("data.txt", "HELLO");
Run("ReadByte on a Write-only stream", () =>
{
using var fs = new FileStream("data.txt", FileMode.Open, FileAccess.Write);
fs.ReadByte();
});
Run("WriteByte on a Read-only stream", () =>
{
using var fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read);
fs.WriteByte(65);
});
Run("Append with FileAccess.Read", () =>
{
using var fs = new FileStream("data.txt", FileMode.Append, FileAccess.Read);
});
Run("Append, then Seek back to 0", () =>
{
using var fs = new FileStream("data.txt", FileMode.Append, FileAccess.Write);
fs.Seek(0, SeekOrigin.Begin);
});
Console.WriteLine("File still holds: " + File.ReadAllText("data.txt"));
}
}المخرجات (ويندوز):
ReadByte on a Write-only stream: NotSupportedException
WriteByte on a Read-only stream: NotSupportedException
Append with FileAccess.Read: ArgumentException
Append, then Seek back to 0: IOException
File still holds: HELLO- التدفق المفتوح للكتابة
Writeلا يستطيع القراءة، والمفتوح للقراءةReadلا يستطيع الكتابة:NotSupportedException. - لا يسمح بـ
Appendإلا معFileAccess.Write: وأي تركيب آخر يعطيArgumentExceptionقبل أن يمس الملف أصلا. - التدفق المفتوح بـ
Appendيرفض الانتقال رجوعا فوق البيانات التي كانت في الملف:IOException. فالبيانات القديمة محمية.
مثال محلول: FileShare
يبقي التدفق الأول الملف مفتوحا بينما يحاول تدفق ثان فتحه.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("shared.txt", "HELLO");
using (var first = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var second = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
Console.WriteLine("FileShare.Read: second reader opened, first byte = " + second.ReadByte());
}
try
{
using var writer = new FileStream("shared.txt", FileMode.Open, FileAccess.Write, FileShare.Read);
Console.WriteLine("FileShare.Read: a writer opened");
}
catch (IOException ex)
{
Console.WriteLine("FileShare.Read: a writer was refused (" + ex.GetType().Name + ")");
}
}
using (var locked = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.None))
{
try
{
using var second = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
Console.WriteLine("FileShare.None: second reader opened");
}
catch (IOException ex)
{
Console.WriteLine("FileShare.None: second reader was refused (" + ex.GetType().Name + ")");
}
}
}
}المخرجات (ويندوز):
FileShare.Read: second reader opened, first byte = 72
FileShare.Read: a writer was refused (IOException)
FileShare.None: second reader was refused (IOException)مع FileShare.Read يسمح التدفق الأول بدخول قراء آخرين ويرفض الكاتب. ومع FileShare.None يرفض الجميع. وبهذه الطريقة يمنع برنامجان، أو جزآن من برنامج واحد، من إفساد الملف.
خصائص Can والموضع
يعرف الصنف Stream خصائص (properties) يملؤها كل تدفق: CanRead وCanWrite وCanSeek وLength وPosition وReadTimeout. والموضع هو المكان الذي سيقرأ فيه البايت التالي أو يكتب، مثل مؤشر الكتابة في المحرر. يبدأ من 0، وتكتب WriteByte بايتا واحدا عنده وتقدمه بمقدار واحد.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
string path = "sample.txt";
File.WriteAllText(path, ""); // start with an empty file
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
Console.WriteLine("Read only : CanRead={0} CanWrite={1} CanSeek={2}",
fs.CanRead, fs.CanWrite, fs.CanSeek);
}
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
Console.WriteLine("Read write: CanRead={0} CanWrite={1} CanSeek={2}",
fs.CanRead, fs.CanWrite, fs.CanSeek);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.WriteByte(65);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.WriteByte(66);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
}
Console.WriteLine("File now holds: " + File.ReadAllText(path));
}
}المخرجات (ويندوز):
Read only : CanRead=True CanWrite=False CanSeek=True
Read write: CanRead=True CanWrite=True CanSeek=True
Position=0 Length=0
Position=1 Length=1
Position=2 Length=2
File now holds: ABمثال محلول: كتابة البايتات من 65 إلى 122
تكتب حلقة كل بايت من 65 حتى 122. ماذا يظهر بين Z وa؟
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var fs = new FileStream("letters.txt", FileMode.Create, FileAccess.Write))
{
for (int i = 65; i < 123; i++)
{
fs.WriteByte((byte)i);
}
Console.WriteLine("Bytes written: " + fs.Length);
}
Console.WriteLine(File.ReadAllText("letters.txt"));
}
}المخرجات (ويندوز):
Bytes written: 58
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyzZ هو 90 وa هو 97، لذا تكتب أيضا الرموز الستة من 91 إلى 96، وهي تقابل العلامات الواقعة بين الحروف الكبيرة والحروف الصغيرة.
مثال محلول: القراءة إلى مخزن مؤقت والنسخ
تقرأ Read(buffer, offset, count) ما يصل إلى count بايت إلى المصفوفة بدءا من offset، وتعيد عدد البايتات التي قرأتها فعلا. وقد تعيد أقل مما طلبت، وتعيد 0 عند نهاية الملف، لذا تجري القراءة في حلقة. ثم تكتب البايتات في ملف ثان باستخدام Write(buffer, offset, count).
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("sample.txt", "C# streams");
byte[] data;
using (var fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read))
{
data = new byte[fs.Length];
int numBytesToRead = (int)fs.Length;
int numBytesRead = 0;
while (numBytesToRead > 0)
{
int n = fs.Read(data, numBytesRead, numBytesToRead);
if (n == 0) break; // end of the file
numBytesRead += n;
numBytesToRead -= n;
}
Console.WriteLine("Read " + numBytesRead + " bytes:");
foreach (byte b in data) Console.Write(b + " ");
Console.WriteLine();
}
using (var fsw = new FileStream("copy.txt", FileMode.OpenOrCreate, FileAccess.Write))
{
fsw.Write(data, 0, data.Length);
}
Console.WriteLine("copy.txt holds: " + File.ReadAllText("copy.txt"));
}
}المخرجات (ويندوز):
Read 10 bytes:
67 35 32 115 116 114 101 97 109 115
copy.txt holds: C# streamsC هو 67، و# هو 35، والمسافة هي 32. لا يتعامل الـFileStream إلا مع البايتات؛ أما المحولات في الجزء 4 فتحولها إلى نص نيابة عنك.
مثال محلول: الانتقال بعد النهاية
تنقل Seek(offset, origin) الموضع. والموضع الجديد هو
p = b + oحيث p الموضع الجديد، وo الإزاحة التي تمررها، وb تساوي 0 مع SeekOrigin.Begin، والموضع الحالي مع SeekOrigin.Current، والطول مع SeekOrigin.End. لا يجوز أن يكون الموضع الجديد سالبا، لكن يجوز أن يقع بعد نهاية الملف.
ملف جديد فارغ. ينتقل البرنامج إلى 20 ويكتب البايت 65، ثم يقرأ كل البايتات من البداية. توقع الطول والبايتات.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var fs = new FileStream("gap.txt", FileMode.Create, FileAccess.ReadWrite))
{
fs.Seek(20, SeekOrigin.Begin);
fs.WriteByte(65);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.Position = 0;
for (int i = 0; i < fs.Length; i++)
{
Console.Write(fs.ReadByte() + " ");
}
Console.WriteLine();
fs.Seek(-3, SeekOrigin.End);
Console.WriteLine("Seek(-3, End) -> Position=" + fs.Position);
fs.Seek(1, SeekOrigin.Current);
Console.WriteLine("Seek(1, Current) -> Position=" + fs.Position);
}
}
}المخرجات (ويندوز):
Position=21 Length=21
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 65
Seek(-3, End) -> Position=18
Seek(1, Current) -> Position=19طول الملف 21 بايتا. والبايتات التي تسبق A، وعددها 20، هي البايت 0، وليست مسافات: فالمسافة قيمتها 32. يتوقع كثيرون مسافات هنا؛ لكن الملف ببساطة لا يحمل شيئا (صفرا) في الفجوة.
تفاعلي: حرك مؤشر الملف
تعرض الأداة التفاعلية الملف صفا من خانات البايتات، والموضع سهما. اختر الملف على القرص، وقيمة من FileMode وقيمة من FileAccess، وافتحه، ثم اضغط WriteByte وReadByte وSeek وراقب تغير الموضع والطول. تعيد الأمثلة تشغيل الحالات السابقة جملة بعد جملة، بما فيها الاستثناءات. وقد تحقق من قواعدها برنامج بلغة C# جرب كل تركيب من FileMode وFileAccess.
الجزء 4: المحولات والاختصارات ومزخرف
StreamWriter
يرث StreamWriter من الصنف المجرد TextWriter، ويكتب الحروف في تدفق.
| الدالة | ما تفعله |
|---|---|
Write | تكتب في التدفق وتبقى على السطر نفسه |
WriteLine | تكتب البيانات متبوعة بنهاية سطر |
Flush | تفرغ المخازن المؤقتة للكاتب وتجعل البيانات المخزنة تكتب في التدفق |
Close | تفرغ، ثم تغلق الكاتب وتدفقه |
مثال محلول: متى يصل النص إلى الملف؟
توقع حجم note.txt بعد كل خطوة.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
var fs = new FileStream("note.txt", FileMode.Create, FileAccess.Write);
var w = new StreamWriter(fs);
w.Write("Hello");
Console.WriteLine("After Write : file size = " + new FileInfo("note.txt").Length);
w.Flush();
Console.WriteLine("After Flush : file size = " + new FileInfo("note.txt").Length);
w.WriteLine(" OS");
Console.WriteLine("After WriteLine: file size = " + new FileInfo("note.txt").Length);
w.Close(); // flushes, then closes the stream too
Console.WriteLine("After Close : file size = " + new FileInfo("note.txt").Length);
}
}المخرجات (ويندوز):
After Write : file size = 0
After Flush : file size = 5
After WriteLine: file size = 5
After Close : file size = 10يجمع الكاتب النص في مخزن مؤقت في الذاكرة ويكتبه في التدفق على دفعات كبيرة. وحتى Flush أو Close لا يتغير الملف على القرص. وفي ويندوز تشغل نهاية السطر بايتين، 13 و10، لذا تضيف WriteLine(" OS") ثلاثة أحرف وبايتين: 5 بايتات أخرى، فينتهي الملف عند 10.
مثال محلول: إضافة مدخلات المستخدم
يطلب هذا البرنامج نصا ويضيفه إلى نهاية data.txt. تستدعى WriteData مرتين، وكانت المدخلات السطرين first run وsecond run.
using System;
using System.IO;
class Program
{
public void WriteData()
{
FileStream fs = new FileStream("data.txt", FileMode.Append, FileAccess.Write);
StreamWriter w = new StreamWriter(fs);
Console.WriteLine("Enter the string");
string str = Console.ReadLine();
w.WriteLine(str);
w.Flush();
fs.Close();
}
static void Main(string[] args)
{
if (File.Exists("data.txt")) File.Delete("data.txt");
Program p = new Program();
p.WriteData();
p.WriteData();
Console.WriteLine("data.txt now holds:");
Console.Write(File.ReadAllText("data.txt"));
}
}المخرجات (ويندوز):
Enter the string
Enter the string
data.txt now holds:
first run
second runيضيف FileMode.Append مع FileAccess.Write إلى النهاية، فيحتفظ كل تشغيل بالأسطر الموجودة من قبل.
StreamReader
يرث StreamReader من الصنف المجرد TextReader، ويقرأ الحروف.
| العضو | ما يفعله |
|---|---|
Peek | يعيد الحرف المتاح التالي دون أن يستهلكه؛ -1 عند النهاية |
Read | يقرأ الحرف التالي (أو مجموعة من الحروف) من التدفق |
ReadLine | يقرأ سطرا من الحروف ويعيده نصا؛ يعيد null عند النهاية |
BaseStream.Seek | ينقل موضع التدفق الذي تحته، إلى البداية مثلا |
Close | يغلق القارئ وتدفقه ويحرر موارد النظام |
مثال محلول: Peek وRead وReadLine
يحتوي lines.txt ثلاثة أسطر: Streams وand وDirectories. توقع الأسطر الأربعة الأولى من المخرجات.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var sw = new StreamWriter("lines.txt"))
{
sw.WriteLine("Streams");
sw.WriteLine("and");
sw.Write("Directories");
}
using (var sr = new StreamReader("lines.txt"))
{
Console.WriteLine("Peek: " + sr.Peek() + " = '" + (char)sr.Peek() + "'");
Console.WriteLine("Read: " + (char)sr.Read());
Console.WriteLine("Peek: " + (char)sr.Peek());
Console.WriteLine("ReadLine: " + sr.ReadLine());
}
Console.WriteLine("-- ReadLine loop --");
FileStream fs = new FileStream("lines.txt", FileMode.Open, FileAccess.Read);
StreamReader reader = new StreamReader(fs);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
string str = reader.ReadLine();
int count = 0;
while (str != null)
{
count++;
Console.WriteLine("{0}: {1}", count, str);
str = reader.ReadLine();
}
Console.WriteLine("ReadLine returned null after line " + count);
reader.Close();
fs.Close();
Console.WriteLine("-- Peek / Read loop --");
using (var sr = new StreamReader("lines.txt"))
{
int chars = 0;
while (sr.Peek() >= 0)
{
char c = (char)sr.Read();
chars++;
}
Console.WriteLine("Characters read: " + chars);
}
Console.WriteLine("File size: " + new FileInfo("lines.txt").Length + " bytes");
}
}المخرجات (ويندوز):
Peek: 83 = 'S'
Read: S
Peek: t
ReadLine: treams
-- ReadLine loop --
1: Streams
2: and
3: Directories
ReadLine returned null after line 3
-- Peek / Read loop --
Characters read: 25
File size: 25 bytes- تعيد
Peekالقيمة 83، رمزS، وتتركه في مكانه: فيرى الاستدعاء الثاني الحرفSنفسه. - تستهلك
ReadالحرفS، فترىPeekالتالية الحرفt، ولا تعيدReadLineإلا بقية السطر،treams. - تتوقف حلقة
ReadLineعندما تعيدReadLineالقيمة null. - في الملف 21 حرفا ونهايتا سطر، حجم كل منهما 2 بايت: 25 حرفا، 25 بايتا.
مثال محلول: BinaryWriter وBinaryReader
يخزن النص الرقم بأرقامه العشرية. أما BinaryWriter فيخزنه بصيغته الثنائية: يشغل الـint دائما 4 بايتات، والـdouble 8، والـbool 1. ويقرأ BinaryReader القيم من جديد، ويجب أن تقرأ بالترتيب نفسه وبالدالة المطابقة (ReadInt32، ReadDouble، ReadBoolean). توقع حجم الملف.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
BinaryReader datain;
BinaryWriter dataout;
int i = 20;
double j = 101.50;
bool b = true;
dataout = new BinaryWriter(new FileStream("data", FileMode.Create));
Console.WriteLine("Writing " + i);
dataout.Write(i);
Console.WriteLine("Writing " + j);
dataout.Write(j);
Console.WriteLine("Writing " + b);
dataout.Write(b);
Console.WriteLine("Writing " + 12.2 * 15.5);
dataout.Write(12.2 * 15.5);
dataout.Close();
Console.WriteLine("File size: " + new FileInfo("data").Length + " bytes");
datain = new BinaryReader(new FileStream("data", FileMode.Open));
i = datain.ReadInt32();
Console.WriteLine("Reading " + i);
j = datain.ReadDouble();
Console.WriteLine("Reading " + j);
b = datain.ReadBoolean();
Console.WriteLine("Reading " + b);
j = datain.ReadDouble();
Console.WriteLine("Reading " + j);
datain.Close();
byte[] raw = File.ReadAllBytes("data");
Console.WriteLine("First 4 bytes (the int 20): {0} {1} {2} {3}", raw[0], raw[1], raw[2], raw[3]);
Console.WriteLine("Byte 12 (the bool true): " + raw[12]);
}
}المخرجات (ويندوز):
Writing 20
Writing 101.5
Writing True
Writing 189.1
File size: 21 bytes
Reading 20
Reading 101.5
Reading True
Reading 189.1
First 4 bytes (the int 20): 20 0 0 0
Byte 12 (the bool true): 1الحجم 4 + 8 + 1 + 8 = 21 بايتا. يخزن الـint ذو القيمة 20 في البايتات الأربعة 20 0 0 0، البايت الأدنى أولا، وtrue بايت واحد قيمته 1 عند الموضع 4 + 8 = 12.
الصنف File
ينجز الصنف الساكن (static) File مهمة كاملة باستدعاء واحد: يفتح الملف، ويقرأ أو يكتب، ثم يغلقه. فيه ثلاثة أزواج من دوال القراءة والكتابة، إضافة إلى Exists وDelete وMove. ولا توجد دالة Rename: فالنقل وإعادة التسمية عملية واحدة.
| الدالة | ما تفعله |
|---|---|
WriteAllText / ReadAllText | تكتب / تقرأ الملف كله نصا واحدا |
WriteAllLines / ReadAllLines | تكتب / تقرأ مصفوفة من الأسطر |
WriteAllBytes / ReadAllBytes | تكتب / تقرأ مصفوفة من البايتات |
Exists / Delete | تتحقق من وجود الملف / تحذفه |
Move | تنقل الملف أو تعيد تسميته |
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
string[] lines = { "Linux", "Windows", "macOS" };
File.WriteAllLines("os.txt", lines);
string[] back = File.ReadAllLines("os.txt");
Console.WriteLine("ReadAllLines returned " + back.Length + " lines, the second is " + back[1]);
File.WriteAllText("test.txt", "Streams are sequences of bytes");
Console.WriteLine("ReadAllText: " + File.ReadAllText("test.txt"));
File.WriteAllBytes("raw.bin", new byte[] { 72, 105 });
Console.WriteLine("WriteAllBytes wrote: " + File.ReadAllText("raw.bin"));
if (File.Exists("renamed.txt")) File.Delete("renamed.txt");
File.Move("test.txt", "renamed.txt");
Console.WriteLine("test.txt exists: " + File.Exists("test.txt"));
Console.WriteLine("renamed.txt exists: " + File.Exists("renamed.txt"));
File.Delete("renamed.txt");
if (File.Exists("renamed.txt") == false)
Console.WriteLine("File deleted...");
}
}المخرجات (ويندوز):
ReadAllLines returned 3 lines, the second is Windows
ReadAllText: Streams are sequences of bytes
WriteAllBytes wrote: Hi
test.txt exists: False
renamed.txt exists: True
File deleted...مثال محلول: المزخرف DeflateStream
يغلف المزخرف تدفقا آخر ويحول البايتات التي تمر عبره. يضغط DeflateStream (من فضاء الأسماء System.IO.Compression) البيانات في طريقها إلى الداخل باستخدام CompressionMode.Compress، ويفك ضغطها في طريقها إلى الخارج باستخدام CompressionMode.Decompress. يكتب البرنامج عبره 1000 بايت من ABCABC.... توقع هل سيكون الملف أكبر من 1000 بايت أم أصغر.
using System;
using System.IO;
using System.IO.Compression;
class Program
{
static void Main(string[] args)
{
byte[] original = new byte[1000];
for (int i = 0; i < original.Length; i++)
original[i] = (byte)(65 + i % 3); // ABCABCABC...
using (FileStream fs = File.Create("data.bin"))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
{
ds.Write(original, 0, original.Length);
}
Console.WriteLine("Original bytes : " + original.Length);
Console.WriteLine("data.bin on disk: " + new FileInfo("data.bin").Length + " bytes");
int count = 0, first = -1, last = -1;
using (FileStream fs = File.OpenRead("data.bin"))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
{
int b;
while ((b = ds.ReadByte()) != -1)
{
if (first == -1) first = b;
last = b;
count++;
}
}
Console.WriteLine("Bytes after decompress: " + count + " (first " + first + ", last " + last + ")");
}
}المخرجات (ويندوز):
Original bytes : 1000
data.bin on disk: 15 bytes
Bytes after decompress: 1000 (first 65, last 65)البيانات المتكررة تنضغط بدرجة كبيرة جدا: أصبحت الـ1000 بايت 15 بايتا على القرص، وفك الضغط يعيدها كلها، 1000 بايت. والملف المضغوط لا يقرأ نصا؛ ولا يعيده إلى أصله إلا المزخرف. ولاحظ أن حلقة القراءة تتوقف عندما تعيد ReadByte القيمة -1، وهي نهاية التدفق.
الجزء 5: المجلدات
Directory أم DirectoryInfo
المجلد موقع يخزن الملفات والمجلدات الأخرى. وتوفر C# له صنفين: Directory صنف ساكن تأخذ دواله مسارا، بينما يمثل كائن DirectoryInfo مجلدا واحدا بعينه. وبالطريقة نفسها، File صنف ساكن، ويمثل كائن FileInfo ملفا واحدا. والصنفان DirectoryInfo وFileInfo كلاهما مشتق من FileSystemInfo.
خصائص DirectoryInfo:
| الخاصية | الوصف |
|---|---|
Attributes | تجلب السمات أو تضبطها (موروثة من FileSystemInfo) |
CreationTime | تجلب وقت الإنشاء أو تضبطه |
Exists | قيمتها true عندما يكون المجلد موجودا |
FullName | المسار الكامل للمجلد |
Name | اسم المجلد |
أعضاء أخرى مستخدمة أدناه: Parent وLastAccessTime، والدوال Create وCreateSubdirectory وDelete وGetDirectories وGetFiles.
خصائص FileInfo:
| الخاصية | الوصف |
|---|---|
Directory | المجلد الذي ينتمي إليه الملف |
Extension | امتداد الملف، مثل .txt |
FullName | المسار الكامل للملف |
Length | حجم الملف بالبايت |
Name | اسم الملف |
وللصنف FileInfo أيضا CreationTime وLastAccessTime وLastWriteTime وAttributes وExists، والدوال Create وAppendText وDelete وOpen وOpenRead وCopyTo. ويضيف الصنف الساكن Directory الدوال GetCurrentDirectory وGetDirectoryRoot وGetLogicalDrives وGetFileSystemEntries، ودوال الوقت GetCreationTime وGetLastAccessTime وGetLastWriteTime.
مثال محلول: أساسيات المجلدات
ينشئ البرنامج lab3/test، ويطبع معلومات عنه، ويحاول حذفه وفيه ملف، ثم يعيد تسميته، وأخيرا يحذفه حذفا تعاوديا. توقع ماذا تفعل Directory.Delete("lab3/test").
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
if (Directory.Exists("lab3")) Directory.Delete("lab3", true);
DirectoryInfo di = Directory.CreateDirectory("lab3/test");
Console.WriteLine($"Name: {di.Name}, Parent: {di.Parent.Name}");
Console.WriteLine("Exists: " + Directory.Exists("lab3/test"));
var curDir = Directory.GetCurrentDirectory();
Console.WriteLine("Current directory: " + curDir);
Console.WriteLine("Root: " + Directory.GetDirectoryRoot(curDir));
Console.WriteLine("Drives: " + string.Join(" ", Directory.GetLogicalDrives()));
File.WriteAllText("lab3/test/a.txt", "abc");
try
{
Directory.Delete("lab3/test");
}
catch (IOException ex)
{
Console.WriteLine("Delete(test) failed: " + ex.GetType().Name);
}
Directory.Move("lab3/test", "lab3/test2");
Console.WriteLine("After Move: test={0} test2={1}",
Directory.Exists("lab3/test"), Directory.Exists("lab3/test2"));
Directory.Delete("lab3/test2", true);
Console.WriteLine("After Delete(test2, true): " + Directory.Exists("lab3/test2"));
}
}المخرجات (ويندوز):
Name: test, Parent: lab3
Exists: True
Current directory: D:\a\os1-lab-runner\os1-lab-runner\labs\os1-03-p16-dirbasics
Root: D:\
Drives: C:\ D:\
Delete(test) failed: IOException
After Move: test=False test2=True
After Delete(test2, true): Falseسجلت هذه المخرجات على جهاز البناء الذي يشغل برامج الدرس، وهو يعمل على محرك الأقراص D:. أما على جهاز المعمل فيطبع البرنامج عادة Root: C:\ ومجلدا داخل مشروعك، وتختلف قائمة محركات الأقراص من جهاز إلى آخر.
- تعيد
CreateDirectoryكائنDirectoryInfoللمجلد الجديد، وتنشئ في طريقها أي مجلدات أب ناقصة. - المجلد الحالي هو المكان الذي بدأ منه البرنامج؛ وتعطي
GetDirectoryRootمحرك الأقراص (drive) الخاص به، وتسردGetLogicalDrivesكل محركات الأقراص بالصيغةC:\. - لا تعمل
Delete(path)إلا على مجلد فارغ، لذا ترميIOExceptionهنا. أماDelete(path, true)فهي تعاودية: تحذف الملفات والمجلدات الفرعية أولا. - تعيد
Directory.Moveتسمية المجلد، كما تعيدFile.Moveتسمية الملف.
شجرة العينة
تعمل بقية برامج المجلدات على الشجرة الصغيرة نفسها، التي تنشئها هذه الدالة (وفي كل برنامج نسخة منها):
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}site
about.txt 48 bytes "Streams move bytes between a program and a file."
work.txt 13 bytes "Lab work list"
images
logo.bin 300 bytes (300 zero bytes)
wiki
home.txt 19 bytes "Welcome to the wiki"
week1.txt 25 bytes "Week 1: what is a stream?"
old
week0.txt 13 bytes "Week 0: setup"يعيد ويندوز الأسماء بالترتيب الأبجدي.
مثال محلول: السرد باستخدام الأنماط
تأخذ GetDirectories وGetFiles وGetFileSystemEntries (الملفات والمجلدات معا) نمط بحث (search pattern) اختياريا، مثل w* للأسماء التي تبدأ بالحرف w، وقيمة من SearchOption: يبحث TopDirectoryOnly في المجلد المعطى وحده، ويبحث AllDirectories في كل مجلد فرعي أيضا. توقع كل قائمة.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Show(string title, string[] items)
{
Console.WriteLine(title);
foreach (var item in items) Console.WriteLine(" " + item);
}
static void Main(string[] args)
{
MakeSampleTree();
Show("GetDirectories(site):", Directory.GetDirectories("site"));
Show("GetDirectories(site, w*, TopDirectoryOnly):",
Directory.GetDirectories("site", "w*", SearchOption.TopDirectoryOnly));
Show("GetDirectories(site, *, AllDirectories):",
Directory.GetDirectories("site", "*", SearchOption.AllDirectories));
Show("GetFiles(site):", Directory.GetFiles("site"));
Show("GetFileSystemEntries(site, w*):", Directory.GetFileSystemEntries("site", "w*"));
Show("GetFiles(site, w*, AllDirectories):",
Directory.GetFiles("site", "w*", SearchOption.AllDirectories));
Console.WriteLine("FileInfo for every file in site/wiki:");
DirectoryInfo dirinfo = new DirectoryInfo("site/wiki");
FileInfo[] filesInDir = dirinfo.GetFiles();
foreach (FileInfo file in filesInDir)
{
Console.WriteLine(" File Name {0} Extension {1} Size: {2} bytes",
file.Name, file.Extension, file.Length);
}
}
}المخرجات (ويندوز):
GetDirectories(site):
site\images
site\wiki
GetDirectories(site, w*, TopDirectoryOnly):
site\wiki
GetDirectories(site, *, AllDirectories):
site\images
site\wiki
site\wiki\old
GetFiles(site):
site\about.txt
site\work.txt
GetFileSystemEntries(site, w*):
site\wiki
site\work.txt
GetFiles(site, w*, AllDirectories):
site\work.txt
site\wiki\week1.txt
site\wiki\old\week0.txt
FileInfo for every file in site/wiki:
File Name home.txt Extension .txt Size: 19 bytes
File Name week1.txt Extension .txt Size: 25 bytesتعود المسارات نسبية لأن البرنامج مرر المسار النسبي site. تجد GetFiles("site", "w*", SearchOption.AllDirectories) الملفين week1.txt وweek0.txt في عمق wiki، بينما تبقى GetFileSystemEntries("site", "w*") في المستوى الأعلى وتعيد مجلدا واحدا وملفا واحدا.
مثال محلول: حجم المجلد
حجم المجلد هو مجموع أطوال كل الملفات التي تحته. وتعيدها GetFiles("*", SearchOption.AllDirectories) كلها كائنات FileInfo. توقع المجموع، ثم مجموع المجلد الأعلى وحده.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Main(string[] args)
{
MakeSampleTree();
long size = 0;
var dirInfo = new DirectoryInfo("site");
foreach (FileInfo fi in dirInfo.GetFiles("*", SearchOption.AllDirectories))
{
size += fi.Length;
Console.WriteLine("{0,-10} {1,4} bytes running total {2,4}", fi.Name, fi.Length, size);
}
Console.WriteLine($"The directory size: {size} bytes");
long top = 0;
foreach (FileInfo fi in dirInfo.GetFiles()) top += fi.Length;
Console.WriteLine($"Top level files only: {top} bytes");
}
}المخرجات (ويندوز):
about.txt 48 bytes running total 48
work.txt 13 bytes running total 61
logo.bin 300 bytes running total 361
home.txt 19 bytes running total 380
week1.txt 25 bytes running total 405
week0.txt 13 bytes running total 418
The directory size: 418 bytes
Top level files only: 61 bytes48 + 13 + 300 + 19 + 25 + 13 = 418 بايتا. ولا يحتوي المجلد الأعلى وحده إلا about.txt وwork.txt: 48 + 13 = 61 بايتا.
مثال محلول: نسخ المجلد تعاوديا
تنشئ DirectoryCopy المجلد الهدف، وتنسخ إليه ملفات المجلد المصدر، ثم تستدعي نفسها مرة لكل مجلد فرعي. هذا هو التعاود: يعالج كل استدعاء مجلدا واحدا ويسلم مجلداته الفرعية إلى استدعاءات جديدة. توقع ترتيب أسطر المخرجات.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Main(string[] args)
{
MakeSampleTree();
if (Directory.Exists("site-2")) Directory.Delete("site-2", true);
DirectoryCopy("site", "site-2", true);
Console.WriteLine("Copying finished");
Console.WriteLine("Files in site-2: " +
Directory.GetFiles("site-2", "*", SearchOption.AllDirectories).Length);
}
private static void DirectoryCopy(string source, string dest, bool copySubDirs = true)
{
var dir = new DirectoryInfo(source);
if (!dir.Exists)
{
throw new DirectoryNotFoundException(
$"Source directory does not exist or could not be found: {source}");
}
DirectoryInfo[] dirs = dir.GetDirectories();
if (!Directory.Exists(dest))
{
Directory.CreateDirectory(dest);
Console.WriteLine("create " + dest);
}
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(dest, file.Name);
file.CopyTo(tempPath, false);
Console.WriteLine(" copy " + tempPath);
}
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(dest, subdir.Name);
DirectoryCopy(subdir.FullName, tempPath, copySubDirs);
}
}
}
}المخرجات (ويندوز):
create site-2
copy site-2\about.txt
copy site-2\work.txt
create site-2\images
copy site-2\images\logo.bin
create site-2\wiki
copy site-2\wiki\home.txt
copy site-2\wiki\week1.txt
create site-2\wiki\old
copy site-2\wiki\old\week0.txt
Copying finished
Files in site-2: 6يعالج الاستدعاء الأول site: ينشئ site-2 وينسخ الملفين العلويين. ثم يستدعي نفسه للمجلد images (إنشاء، ثم نسخ logo.bin) وللمجلد wiki، وينسخ الاستدعاء الخاص بـwiki ملفيه قبل أن يستدعي نفسه للمجلد old.
مثال محلول: البحث في الملفات عن نص
يقدم المصدر دالة تعاودية تبحث في كل ملف عن جزء من نص. وهي هنا تبحث عن stream في شجرة العينة، فتقرأ كل ملف باستخدام StreamReader.ReadToEnd وتقارن بالحروف الصغيرة. هذه هي الدالة، مقتطعة من البرنامج الكامل في إجابة التمرين 6:
private static void DirSearch(string sDir)
{
try
{
foreach (var directory in Directory.GetDirectories(sDir))
{
foreach (var filename in Directory.GetFiles(directory))
{
using (var streamReader = new StreamReader(filename))
{
var contents = streamReader.ReadToEnd().ToLower();
if (contents.Contains(SearchText))
{
FilesFound.Add(filename);
}
}
}
DirSearch(directory);
}
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
}
}ما الملفات التي تبلغ عنها DirSearch("site")؟ يحتوي about.txt الكلمة "Streams"، ويحتوي week1.txt الكلمة "stream".
DirSearch: files containing the word stream
site\wiki\week1.txtيفوت البحث الملف about.txt. تمر الحلقة الخارجية على المجلدات الفرعية للمجلد site، وتفتح الحلقة الداخلية ملفات كل مجلد فرعي، لذا لا تفتح أبدا ملفات المجلد الذي استدعيت عليه الدالة. وبالنسبة إلى site يعني ذلك about.txt وwork.txt. ويصلح التمرين 6 هذا الخطأ.
تفاعلي: شاهد التعاود
تتجول الأداة التفاعلية في شجرة العينة خطوة بعد خطوة. اختر حجم المجلد، أو النسخ التعاودي، أو الدالة DirSearch كما في المصدر، أو بحثا يفتح ملفات المجلد الأعلى أيضا؛ ثم اضغط خطوة أو تشغيل، وشاهد العقدة الحالية، ومكدس الاستدعاءات (call stack) للاستدعاءات التعاودية، والمجموع التراكمي، ومخرجات الكونسول وهي تنمو. ويطابق ترتيب الزيارة وكل رقم فيها ما خرج في تشغيلات ويندوز أعلاه.
مثال محلول: قوائم التحكم في الوصول (ويندوز فقط)
قائمة التحكم في الوصول (access control list, ACL) قائمة من مدخلات التحكم في الوصول (access control entries, ACEs). يسمي كل مدخل صاحب حق (trustee)، أي مستخدما أو مجموعة، وحقوق الوصول المسموحة له أو الممنوعة عنه أو الخاضعة للتدقيق. وتعيد DirectoryInfo.GetAccessControl() قائمة التحكم في الوصول لمجلد ما. لا يعمل هذا المثال إلا على ويندوز، ولا يحتاج على .NET 8 إلى أي حزمة إضافية.
using System;
using System.IO;
using System.Security.AccessControl;
class Program
{
static void Main(string[] args)
{
var myDir = "test";
Directory.CreateDirectory(myDir);
var dirInfo = new DirectoryInfo(myDir);
DirectorySecurity dSecurity = dirInfo.GetAccessControl();
AuthorizationRuleCollection acl = dSecurity.GetAccessRules(true, true,
typeof(System.Security.Principal.NTAccount));
foreach (FileSystemAccessRule ace in acl)
{
Console.WriteLine("Account: {0}", ace.IdentityReference.Value);
Console.WriteLine("Type: {0}", ace.AccessControlType);
Console.WriteLine("Rights: {0}", ace.FileSystemRights);
Console.WriteLine("Inherited: {0}", ace.IsInherited);
Console.WriteLine("------------------------");
}
}
}المخرجات (ويندوز):
Account: BUILTIN\Administrators
Type: Allow
Rights: FullControl
Inherited: False
------------------------
Account: BUILTIN\Administrators
Type: Allow
Rights: FullControl
Inherited: True
------------------------
Account: NT AUTHORITY\SYSTEM
Type: Allow
Rights: FullControl
Inherited: True
------------------------
Account: CREATOR OWNER
Type: Allow
Rights: 268435456
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: ReadAndExecute, Synchronize
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: CreateDirectories
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: CreateFiles
Inherited: True
------------------------كل كتلة مدخل واحد: الحساب، وهل الحقوق مسموحة أم ممنوعة، والحقوق، وهل المدخل موروث من المجلد الأب. المدخل الأول وحده فيه Inherited: False؛ أما المدخلات الستة الأخرى فمصدرها المجلد الأب.
التمارين
نحو 30 دقيقة. توقع كل إجابة على الورق أولا، ثم اكتب البرنامج وشغله لتتحقق من نفسك. الإجابات في نهاية الصفحة.
التمرين 1: تتبع المؤشر (نحو 6 دقائق)
File.WriteAllText("data.txt", "STREAM");
using (var fs = new FileStream("data.txt", FileMode.Open, FileAccess.ReadWrite))
{
fs.ReadByte(); // 1
fs.ReadByte(); // 2
fs.Seek(2, SeekOrigin.Current); // 3
fs.WriteByte(33); // 4
fs.Seek(-1, SeekOrigin.End); // 5
fs.ReadByte(); // 6
fs.ReadByte(); // 7
fs.Seek(8, SeekOrigin.Begin); // 8
fs.WriteByte(90);
}لكل سطر مرقم اذكر القيمة التي تعيدها ReadByte (إن وجدت) والموضع بعد السطر. ثم اذكر الطول النهائي وكل بايتات الملف. الرموز التي تحتاج إليها: S 83، T 84، R 82، E 69، A 65، M 77.
التمرين 2: اختر الوضع وتوقع النتيجة (نحو 5 دقائق)
- (a) يجب أن يزيد ملف السجل سطرا واحدا في كل تشغيل وأن يحتفظ بالأسطر القديمة. أي قيمة من
FileModeوأي قيمة منFileAccessتستخدم؟ وبعد ثلاثة تشغيلات، كم سطرا فيه؟ - (b) تنفذ
new FileStream("new.txt", FileMode.CreateNew, FileAccess.Write)مرتين متتاليتين، بدءا من عدم وجود الملف. ماذا يحدث في كل مرة؟ - (c) يحتوي
hello.txtعلىHELLO. يفتح باستخدامFileMode.OpenوFileAccess.Write، وتستدعىWriteByte((byte)'J'). ماذا يحتوي الملف بعد الإغلاق؟ - (d) ثم يفتح
hello.txtباستخدامFileMode.TruncateوFileAccess.Read. ماذا يحدث، وماذا يحتوي الملف بعد ذلك؟
التمرين 3: حجم سجل ثنائي (نحو 4 دقائق)
using (var bw = new BinaryWriter(new FileStream("rec.bin", FileMode.Create)))
{
bw.Write(7);
bw.Write(true);
bw.Write(3.5);
bw.Write(false);
bw.Write(1000);
}كم بايتا حجم rec.bin؟ اكتب شيفرة القراءة التي تطبع القيم الخمس من جديد. ما البايتات الأربعة الأخيرة في الملف؟ (تلميح: 1000 = 3 x 256 + 232.)
التمرين 4: عد الأسطر (نحو 5 دقائق)
اكتب برنامجا يكتب الأسطر الخمسة Windows وLinux وmacOS وFreeBSD وAndroid في os.txt باستخدام StreamWriter، ثم يقرأ الملف من جديد باستخدام StreamReader.ReadLine ويطبع عدد الأسطر وأطول سطر مع طوله. ما حجم الملف على ويندوز؟
التمرين 5: اطبع الشجرة (نحو 6 دقائق)
اكتب دالة تعاودية PrintTree(DirectoryInfo dir, string indent) تطبع اسم المجلد متبوعا بالرمز / (مثل site/)، ثم ملفاته بأحجامها، ثم تستدعي نفسها لكل مجلد فرعي بإزاحة أعمق. وتعيد المجموع الكلي لأحجام كل الملفات التي تحت المجلد. شغلها على شجرة العينة واطبع المجموع، ثم اطبع حجم site/wiki وحده.
التمرين 6: أصلح البحث (نحو 5 دقائق)
لا تفتح DirSearch أبدا ملفات المجلد الذي تستدعى عليه. اكتب FileSearch(string sDir) التي تبحث أولا في ملفات sDir نفسه، ثم تستدعي نفسها لكل مجلد فرعي. شغل الدالتين على site بنص البحث stream. ما الملفات التي يجب أن تبلغ عنها FileSearch؟
الإجابات
التمرين 1
| السطر | ما تعيده ReadByte | الموضع بعده |
|---|---|---|
| 1 | 83 (S) | 1 |
| 2 | 84 (T) | 2 |
| 3 | 4 | |
| 4 | 5 (يكتب فوق البايت 4، ويبقى الطول 6) | |
| 5 | 5 (الطول 6 ناقص 1) | |
| 6 | 77 (M) | 6 |
| 7 | -1 (نهاية الملف) | 6 |
| 8 | 9 بعد كتابة 90 (Z) عند 8؛ والطول 9 |
البايتات النهائية هي 83 84 82 69 33 77 0 0 90: أي STRE!M، ثم بايتان صفريان يملآن الفجوة، ثم Z. والبرنامج الذي يتحقق من ذلك:
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("data.txt", "STREAM");
using (var fs = new FileStream("data.txt", FileMode.Open, FileAccess.ReadWrite))
{
Console.WriteLine("1) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
Console.WriteLine("2) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
fs.Seek(2, SeekOrigin.Current);
Console.WriteLine("3) Seek(2, Current): Position = {0}", fs.Position);
fs.WriteByte(33);
Console.WriteLine("4) WriteByte(33): Position = {0}, Length = {1}", fs.Position, fs.Length);
fs.Seek(-1, SeekOrigin.End);
Console.WriteLine("5) Seek(-1, End): Position = {0}", fs.Position);
Console.WriteLine("6) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
Console.WriteLine("7) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
fs.Seek(8, SeekOrigin.Begin);
fs.WriteByte(90);
Console.WriteLine("8) Seek(8, Begin), WriteByte(90): Position = {0}, Length = {1}", fs.Position, fs.Length);
}
Console.WriteLine("Bytes: " + string.Join(" ", File.ReadAllBytes("data.txt")));
}
}المخرجات (ويندوز):
1) ReadByte() = 83, Position = 1
2) ReadByte() = 84, Position = 2
3) Seek(2, Current): Position = 4
4) WriteByte(33): Position = 5, Length = 6
5) Seek(-1, End): Position = 5
6) ReadByte() = 77, Position = 6
7) ReadByte() = -1, Position = 6
8) Seek(8, Begin), WriteByte(90): Position = 9, Length = 9
Bytes: 83 84 82 69 33 77 0 0 90التمرين 2
- (a)
FileMode.AppendمعFileAccess.Write: كل تشغيل يضيف إلى النهاية، فتعطي ثلاثة تشغيلات 3 أسطر. - (b) تنشئ المحاولة الأولى الملف؛ وترمي الثانية
IOExceptionلأن الملف موجود بالفعل. - (c)
JELLO: لا يفرغOpenالملف، ويكتب البايت فوقHعند الموضع 0. - (d)
ArgumentException: يحتاجTruncateإلى صلاحية الكتابة. ويجري الفحص قبل أن يمس الملف، لذا يبقى محتواهJELLO.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
// (a) a log that grows by one line on every run
if (File.Exists("log.txt")) File.Delete("log.txt");
for (int run = 1; run <= 3; run++)
{
using var w = new StreamWriter(new FileStream("log.txt", FileMode.Append, FileAccess.Write));
w.WriteLine("run " + run);
}
Console.WriteLine("(a) log.txt has " + File.ReadAllLines("log.txt").Length + " lines");
// (b) CreateNew twice
if (File.Exists("new.txt")) File.Delete("new.txt");
for (int i = 1; i <= 2; i++)
{
try
{
using var fs = new FileStream("new.txt", FileMode.CreateNew, FileAccess.Write);
Console.WriteLine("(b) attempt " + i + ": created");
}
catch (Exception ex)
{
Console.WriteLine("(b) attempt " + i + ": " + ex.GetType().Name);
}
}
// (c) Open + Write over HELLO
File.WriteAllText("hello.txt", "HELLO");
using (var fs = new FileStream("hello.txt", FileMode.Open, FileAccess.Write))
{
fs.WriteByte((byte)'J');
}
Console.WriteLine("(c) hello.txt holds " + File.ReadAllText("hello.txt"));
// (d) Truncate + Read
try
{
using var fs = new FileStream("hello.txt", FileMode.Truncate, FileAccess.Read);
Console.WriteLine("(d) opened");
}
catch (Exception ex)
{
Console.WriteLine("(d) " + ex.GetType().Name);
}
Console.WriteLine("(d) hello.txt still holds " + File.ReadAllText("hello.txt"));
}
}المخرجات (ويندوز):
(a) log.txt has 3 lines
(b) attempt 1: created
(b) attempt 2: IOException
(c) hello.txt holds JELLO
(d) ArgumentException
(d) hello.txt still holds JELLOالتمرين 3
4 + 1 + 8 + 1 + 4 = 18 بايتا. يجب أن تستخدم شيفرة القراءة الترتيب نفسه: ReadInt32 وReadBoolean وReadDouble وReadBoolean وReadInt32. والبايتات الأربعة الأخيرة هي الـint ذو القيمة 1000، البايت الأدنى أولا: 232 3 0 0، لأن 1000 = 3 x 256 + 232.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var bw = new BinaryWriter(new FileStream("rec.bin", FileMode.Create)))
{
bw.Write(7);
bw.Write(true);
bw.Write(3.5);
bw.Write(false);
bw.Write(1000);
}
Console.WriteLine("Size: " + new FileInfo("rec.bin").Length + " bytes");
using (var br = new BinaryReader(new FileStream("rec.bin", FileMode.Open)))
{
Console.WriteLine(br.ReadInt32() + " " + br.ReadBoolean() + " " + br.ReadDouble() + " "
+ br.ReadBoolean() + " " + br.ReadInt32());
}
byte[] raw = File.ReadAllBytes("rec.bin");
Console.WriteLine("Last 4 bytes: {0} {1} {2} {3}", raw[14], raw[15], raw[16], raw[17]);
}
}المخرجات (ويندوز):
Size: 18 bytes
7 True 3.5 False 1000
Last 4 bytes: 232 3 0 0التمرين 4
في كل من Windows وFreeBSD وAndroid 7 أحرف؛ وتحتفظ الحلقة بأول واحد تجده لأنها لا تستبدل أطول سطر إلا عندما يكون السطر الجديد أطول منه تماما. والملف 31 حرفا إضافة إلى خمس نهايات أسطر، حجم كل منها 2 بايت على ويندوز: 41 بايتا.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (StreamWriter sw = new StreamWriter("os.txt"))
{
sw.WriteLine("Windows");
sw.WriteLine("Linux");
sw.WriteLine("macOS");
sw.WriteLine("FreeBSD");
sw.WriteLine("Android");
}
int count = 0;
string longest = "";
using (StreamReader sr = new StreamReader("os.txt"))
{
string line = sr.ReadLine();
while (line != null)
{
count++;
if (line.Length > longest.Length) longest = line;
line = sr.ReadLine();
}
}
Console.WriteLine("Lines: " + count);
Console.WriteLine("Longest: " + longest + " (" + longest.Length + " characters)");
Console.WriteLine("File size: " + new FileInfo("os.txt").Length + " bytes");
}
}المخرجات (ويندوز):
Lines: 5
Longest: Windows (7 characters)
File size: 41 bytesالتمرين 5
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static long PrintTree(DirectoryInfo dir, string indent)
{
long total = 0;
Console.WriteLine(indent + dir.Name + "/");
foreach (FileInfo f in dir.GetFiles())
{
Console.WriteLine(indent + " " + f.Name + " (" + f.Length + ")");
total += f.Length;
}
foreach (DirectoryInfo sub in dir.GetDirectories())
{
total += PrintTree(sub, indent + " ");
}
return total;
}
static void Main(string[] args)
{
MakeSampleTree();
long total = PrintTree(new DirectoryInfo("site"), "");
Console.WriteLine("Total: " + total + " bytes");
long wiki = 0;
foreach (FileInfo fi in new DirectoryInfo("site/wiki").GetFiles("*", SearchOption.AllDirectories))
wiki += fi.Length;
Console.WriteLine("wiki: " + wiki + " bytes");
}
}المخرجات (ويندوز):
site/
about.txt (48)
work.txt (13)
images/
logo.bin (300)
wiki/
home.txt (19)
week1.txt (25)
old/
week0.txt (13)
Total: 418 bytes
wiki: 57 bytesيطابق المجموع حجم المجلد في المثال المحلول، 418 بايتا، ويحتوي wiki على 19 + 25 + 13 = 57 بايتا.
التمرين 6
يجب أن تبلغ FileSearch عن site\about.txt وsite\wiki\week1.txt. ويشغل البرنامج الكامل الدالتين كلتيهما:
using System;
using System.Collections.Generic;
using System.IO;
class Program
{
private static List<string> FilesFound { get; } = new List<string>();
private const string SearchText = "stream";
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
private static void DirSearch(string sDir)
{
try
{
foreach (var directory in Directory.GetDirectories(sDir))
{
foreach (var filename in Directory.GetFiles(directory))
{
using (var streamReader = new StreamReader(filename))
{
var contents = streamReader.ReadToEnd().ToLower();
if (contents.Contains(SearchText))
{
FilesFound.Add(filename);
}
}
}
DirSearch(directory);
}
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static void FileSearch(string sDir)
{
foreach (var filename in Directory.GetFiles(sDir))
{
using (var streamReader = new StreamReader(filename))
{
if (streamReader.ReadToEnd().ToLower().Contains(SearchText))
FilesFound.Add(filename);
}
}
foreach (var directory in Directory.GetDirectories(sDir))
{
FileSearch(directory);
}
}
private static void Main(string[] args)
{
MakeSampleTree();
DirSearch("site");
Console.WriteLine("DirSearch: files containing the word " + SearchText);
foreach (var file in FilesFound) Console.WriteLine(" " + file);
FilesFound.Clear();
FileSearch("site");
Console.WriteLine("FileSearch: files containing the word " + SearchText);
foreach (var file in FilesFound) Console.WriteLine(" " + file);
}
}المخرجات (ويندوز):
DirSearch: files containing the word stream
site\wiki\week1.txt
FileSearch: files containing the word stream
site\about.txt
site\wiki\week1.txt