מה זה?

עד עכשיו כל הנתונים בתוכניות שלנו נעלמו ברגע שהתוכנית נסגרה — משתנים חיים רק בזיכרון RAM בזמן ריצה. כדי לשמור נתונים לאורך זמן (בין הרצות), צריך לכתוב אותם לקובץ על הדיסק. C# נותנת כמה רמות גישה: File למבצעים פשוטים וקצרים, StreamReader/StreamWriter לשליטה מדויקת יותר, במיוחד עם קבצים גדולים.

מילות מפתח שחשוב לזכור

• File.ReadAllText / File.WriteAllText — הדרך הפשוטה ביותר: קורא/כותב את כל תוכן הקובץ בפעולה אחת

• StreamReader / StreamWriter — קריאה/כתיבה שורה-שורה או בזרימה, יעילים יותר לקבצים גדולים

• using statement — מבטיח שמשאב (כמו קובץ פתוח) ייסגר אוטומטית, גם אם נזרקת חריגה באמצע

• File.Exists — בודק אם קובץ קיים לפני שמנסים לגשת אליו

// File — the simple way, for small files
File.WriteAllText("notes.txt", "Hello from the file!");
string content = File.ReadAllText("notes.txt");
Console.WriteLine(content);   // Hello from the file!

// writing/reading several lines at once
File.WriteAllLines("names.txt", new[] { "Dana", "Avi", "Rotem" });
string[] names = File.ReadAllLines("names.txt");
// StreamWriter/StreamReader with using — precise control, and guaranteed closing
using (StreamWriter writer = new StreamWriter("log.txt", append: true))
{
    writer.WriteLine($"{DateTime.Now}: action performed");
}   // the file closes automatically at the end of the block, even if an exception occurred

using (StreamReader reader = new StreamReader("log.txt"))
{
    string line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(line);
    }
}
// combined with try/catch — the file might not exist, or might not be accessible
try
{
    if (!File.Exists("data.txt"))
    {
        Console.WriteLine("File does not exist");
        return;
    }
    string data = File.ReadAllText("data.txt");
    Console.WriteLine(data);
}
catch (IOException ex)
{
    Console.WriteLine($"Read error: {ex.Message}");
}

הסבר עיקרי

File מול StreamReader/StreamWriter — File.ReadAllText נוח כשהקובץ קטן וסביר לטעון אותו כולו לזיכרון בבת אחת. StreamReader קורא שורה-שורה (ReadLine) בלי לטעון את כל הקובץ לזיכרון בבת אחת — קריטי לקבצים גדולים (למשל קובץ נתונים של מיליוני שורות, כמו שנפגוש בפרויקט המסכם של היחידה).

using פותר בעיה אמיתית — קובץ פתוח הוא משאב חיצוני (Handle) שמערכת ההפעלה מנהלת; אם שוכחים לסגור אותו, הוא נשאר “תפוס” גם אחרי שהתוכנית סיימה להשתמש בו — עלול לחסום גישה עתידית. using מבטיח קריאה אוטומטית ל-Dispose() (שסוגר את הקובץ) ברגע שהבלוק מסתיים — גם אם נזרקה חריגה באמצע. זו למעשה גרסה מובנית ל-finally שכבר תרגלנו, ייעודית למשאבים.

File.Exists לפני גישה — קובץ יכול “להיעלם” בין רגע לרגע (נמחק, נייד חיצוני שהוצא) — File.Exists בודק לפני שמנסים לגשת, אבל גם בדיקה כזו לא מבטלת את הצורך ב-try/catch, כי מצב יכול להשתנות בין הבדיקה לגישה בפועל (Race Condition) — שילוב שניהם הוא הפרקטיקה הבטוחה.

יתרונות

File.ReadAllText/WriteAllText נותנים תחביר קצר לפעולות פשוטות; StreamReader/StreamWriter יעילים בזיכרון לקבצים גדולים כי לא טוענים הכל בבת אחת; using מבטיח סגירת משאבים אוטומטית גם בתרחישי שגיאה, בלי finally ידני בכל מקום.

חסרונות

File.ReadAllText על קובץ ענק (למשל כמה ג’יגה-בייט) עלול לצרוך זיכרון רב מיותר; פעולות קובץ תמיד עלולות להיכשל מסיבות חיצוניות (הרשאות, קובץ נעול על ידי תהליך אחר, דיסק מלא) — דורש טיפול-שגיאות תמיד, לא רק “לפעמים”.

נקודות חשובות

• File.ReadAllText/WriteAllText = פעולה שלמה בבת אחת; StreamReader/StreamWriter = שורה-שורה, יעיל לקבצים גדולים

• using מבטיח Dispose() אוטומטי (סגירת משאב) גם כשנזרקת חריגה — כמו finally מובנה

• File.Exists לא מבטל צורך ב-try/catch — מצב הקובץ יכול להשתנות בין הבדיקה לגישה

• פעולות קובץ הן תמיד מקור פוטנציאלי ל-IOException — יש לצפות לזה תמיד

טעויות נפוצות

• לפתוח StreamWriter/StreamReader בלי using ולשכוח לסגור — משאב “תקוע” פתוח

• לסמוך רק על File.Exists בלי try/catch, ולהתעלם מהאפשרות שהמצב משתנה בין הבדיקה לגישה בפועל

• לטעון קובץ ענק שלם עם File.ReadAllText כשדרושה רק קריאה חלקית — בזבוז זיכרון מיותר

סיכום

File.ReadAllText/WriteAllText נותנים גישה פשוטה לקבצים קטנים בפעולה אחת; StreamReader/StreamWriter נותנים שליטה מדויקת יותר, יעילה לקבצים גדולים, קריאה/כתיבה שורה-שורה. using מבטיח סגירה אוטומטית ובטוחה של הקובץ גם בתרחיש חריגה, ופעולות קובץ תמיד דורשות try/catch — קבצים הם משאב חיצוני שעלול להיכשל מסיבות שאין לתוכנית שליטה עליהן.

דוקומנטציה רשמית

Microsoft Learn — File and Stream I/O

Microsoft Learn — using statement


תרגילים

תרגיל 1 — כתיבה וקריאה בסיסית

המשימה: כתבו לקובץ todo.txt שלוש משימות עם File.WriteAllLines, ואז קראו אותן חזרה עם File.ReadAllLines והדפיסו כל אחת עם מספר סידורי.

בדיקה: שלוש השורות מודפסות בסדר הנכון עם מספור.

תרגיל 2 — StreamWriter עם append

המשימה: כתבו פונקציה LogAction(string message) שמוסיפה שורה לקובץ activity.log עם StreamWriter ו-append: true, כולל using. קראו לה 3 פעמים עם הודעות שונות.

בדיקה: קובץ activity.log מכיל את כל 3 השורות (לא רק האחרונה) — הוכחה ש-append: true עובד נכון.


פרויקט מסכם

המשימה: בנו “מערכת גיבוי הערות” פשוטה — התוכנית קוראת פקודות מהמשתמש בלולאה: add <טקסט> מוסיף הערה, list מציג את כולן, exit מסיים.

דרישות:

  1. כל הערה נשמרת מיד לקובץ notes.txt עם StreamWriter (append: true) ברגע ההוספה — לא רק בזיכרון
  2. פקודת list קוראת את כל הקובץ מחדש עם File.ReadAllLines ומדפיסה ממוספר
  3. טיפול ב-IOException סביב כל פעולת קובץ, עם הודעת שגיאה ברורה אם הכתיבה/קריאה נכשלת

בדיקה: הוספת 3 הערות ואז list מציגה את כל השלוש; סגירת התוכנית ופתיחתה מחדש עם list עדיין מציגה את ההערות — הוכחה שהן נשמרו לדיסק, לא רק לזיכרון.

מה בפרק הבא

בפרק הבא — JSON: איך הופכים אובייקטים ב-C# לפורמט טקסט סטנדרטי לשמירה/העברה (Serialize), ובחזרה (Deserialize) — הבסיס לכל תקשורת עם שרתים ו-APIs.