مقدمه

در درس قبل، با نحوه‌ی مدیریت فایل‌ها و پوشه‌ها با استفاده از کلاس‌های فضای نام System.IO آشنا شدیم. اما آن کلاس‌ها تنها ساختار سیستم فایل را مدیریت می‌کردند. برای کار با محتوای یک فایل، .NET از یک مفهوم انتزاعی و قدرتمند به نام استریم (Stream) استفاده می‌کند. یک استریم، دنباله‌ای از بایت‌ها را نشان می‌دهد که می‌توان از آن خواند، در آن نوشت، یا هر دو کار را انجام داد. زیبایی استریم‌ها در این است که منبع اصلی داده‌ها را از دید ما پنهان می‌کنند. شما با یک API یکسان می‌توانید با یک فایل روی دیسک، یک جریان داده از شبکه، یا حتی یک بلوک حافظه کار کنید. در این درس، با کلاس پایه‌ی Stream و کلاس‌های کمکی StreamReader و StreamWriter برای کار با داده‌های متنی آشنا خواهیم شد.

System.IO.Stream چیست؟

کلاس System.IO.Stream یک کلاس انتزاعی (abstract) است که به عنوان کلاس پایه برای تمام انواع استریم در .NET عمل می‌کند. این کلاس یک نمای استاندارد از یک دنباله‌ی بایت را فراهم کرده و متدهای پایه‌ای مانند Read، Write، Seek (برای جابجایی در استریم) و Close را تعریف می‌کند.

برخی از پیاده‌سازی‌های مشخص این کلاس عبارتند از:

  • FileStream: برای خواندن و نوشتن در یک فایل فیزیکی روی دیسک.
  • MemoryStream: برای کار با داده‌ها مستقیماً در حافظه‌ی RAM.
  • NetworkStream: برای ارسال و دریافت داده از طریق یک سوکت شبکه.

کار مستقیم با بایت‌ها می‌تواند پیچیده باشد، به خصوص زمانی که با داده‌های متنی سروکار داریم. برای ساده‌سازی این فرآیند، .NET کلاس‌های کمکی (helper classes) را فراهم کرده که بر روی یک استریم پایه قرار گرفته و API سطح بالاتری را برای کار با انواع داده‌ی مشخص ارائه می‌دهند.

خواندن فایل‌های متنی با StreamReader

کلاس StreamReader برای خواندن کاراکترها از یک استریم طراحی شده است. این کلاس مسئولیت پیچیده‌ی تبدیل بایت‌ها به کاراکترها را بر اساس یک انکدینگ مشخص (مانند UTF-8) بر عهده می‌گیرد.

از آنجایی که StreamReader با منابع مدیریت‌نشده کار می‌کند، این کلاس اینترفیس IDisposable را پیاده‌سازی می‌کند و ما باید همیشه آن را در یک بلوک using قرار دهیم تا از بسته شدن صحیح فایل و آزاد شدن منابع اطمینان حاصل کنیم.

Copy Icon Program.cs
using System.IO;

// First, let's create a sample file to read.
File.WriteAllText("mydata.txt", "Line 1\nLine 2\nLine 3");

// Use a 'using' block to ensure the StreamReader is properly disposed.
using (StreamReader reader = new StreamReader("mydata.txt"))
{
    string line;
    // Read and display lines from the file until the end of the file is reached.
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(line);
    }
} // The file is automatically closed here.

در این مثال، ما یک StreamReader برای فایل mydata.txt ایجاد می‌کنیم. سپس در یک حلقه‌ی while، با استفاده از متد ReadLine() فایل را خط به خط می‌خوانیم تا به انتهای آن برسیم (در این حالت ReadLine مقدار null را برمی‌گرداند). بلوک using تضمین می‌کند که پس از اتمام کار، متد Dispose خواننده فراخوانی شده و فایل بسته می‌شود.

نوشتن در فایل‌های متنی با StreamWriter

به طور مشابه، کلاس StreamWriter برای نوشتن کاراکترها در یک استریم به کار می‌رود. این کلاس نیز مسئولیت تبدیل کاراکترها به بایت‌ها را بر عهده دارد و باید در یک بلوک using استفاده شود.

Copy Icon Program.cs
// Use a 'using' block for the StreamWriter.
// This will create a new file or overwrite it if it exists.
using (StreamWriter writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("Log started at: " + DateTime.Now);
    writer.WriteLine("This is the first log entry.");
    writer.Write("This is a"); // Write doesn't add a new line.
    writer.Write(" partial entry.");
}

Console.WriteLine("\nlog.txt has been created.");

سازنده‌ی StreamWriter به طور پیش‌فرض، اگر فایل وجود داشته باشد، محتوای آن را پاک کرده و از اول می‌نویسد. اگر بخواهید به انتهای یک فایل موجود اضافه کنید (append)، باید یک پارامتر دوم با مقدار true را به سازنده ارسال کنید:

using (StreamWriter writer = new StreamWriter("log.txt", append: true))
{
    writer.WriteLine("This is an appended log entry.");
}

متدهای کمکی در کلاس File

برای کارهای ساده و سریع، کلاس استاتیک File متدهایی را فراهم می‌کند که تمام مراحل ایجاد استریم، خواندن/نوشتن و بستن آن را در یک فراخوانی واحد انجام می‌دهند. این متدها برای کار با فایل‌های کوچک بسیار راحت هستند.

  • File.ReadAllText(path): کل محتوای یک فایل متنی را خوانده و به صورت یک رشته‌ی واحد برمی‌گرداند.
  • File.WriteAllText(path, content): یک رشته را در یک فایل می‌نویسد (و اگر فایل وجود داشته باشد، آن را بازنویسی می‌کند).
  • File.ReadAllLines(path): تمام خطوط یک فایل متنی را خوانده و آن‌ها را به صورت یک آرایه از رشته‌ها برمی‌گرداند.
  • File.WriteAllLines(path, lines): یک کالکشن از رشته‌ها را در یک فایل می‌نویسد و هر عنصر را در یک خط جداگانه قرار می‌دهد.

هرچند این متدها بسیار راحت هستند، اما برای فایل‌های بسیار بزرگ مناسب نیستند، زیرا کل محتوای فایل را یکجا در حافظه بارگذاری می‌کنند. برای فایل‌های بزرگ، استفاده از StreamReader و خواندن خط به خط یا تکه‌تکه، رویکرد بسیار بهینه‌تری از نظر مصرف حافظه است.