Menu

C# כתיבה לקובץ וקריאה מקובץ: File, StreamWriter, StreamReader

איך כותבים לקובץ וקוראים קובץ ב-C#: File.WriteAllText, ReadAllText, AppendAllText, WriteAllLines ו-ReadAllLines לקבצים שלמים, StreamWriter ו-StreamReader לקבצים גדולים, נתיבים עם Path.Combine, תיקיות, קידודים, והחריגות שקוד שעובד עם קבצים צריך לטפל בהן.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

ב-namespace System.IO יש את כל מה שצריך לעבודה עם קבצים. לרוב המשימות מספיקה קריאה אחת למחלקה הסטטית File: היא פותחת את הקובץ, קוראת או כותבת אותו, וסוגרת אותו. לקבצים גדולים או לשליטה מדויקת, השתמשו ב-StreamReader וב-StreamWriter. הוסיפו using System.IO; לכל אחת מהדוגמאות שבהמשך.

כתיבה וקריאה של קובץ שלם

File.WriteAllText יוצר קובץ (או דורס קובץ קיים) עם הטקסט שנותנים לו. File.ReadAllText מחזיר את כל הקובץ כמחרוזת אחת. File.AppendAllText מוסיף לסוף:

פלט:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

נתיב יחסי כמו "notes.txt" מחושב ביחס לתיקייה הנוכחית של התהליך, שהיא לא תמיד התיקייה שבה התוכנית נמצאת (שירות, משימה מתוזמנת או מריץ בדיקות יכולים להתחיל ממקום אחר). לקבצים שמגיעים יחד עם האפליקציה, בנו את הנתיב מ-AppContext.BaseDirectory.

WriteAllText דורס בלי לשאול. אם חשוב לא לאבד את התוכן הישן, בדקו קודם File.Exists או כתבו לקובץ זמני ואז העבירו אותו למקומו עם File.Move.

שורות: WriteAllLines, ReadAllLines ו-ReadLines

קבצים של רשומות, אחת בכל שורה, נפוצים מספיק כדי שיהיו להם מתודות משלהם. WriteAllLines כותב כל מחרוזת ואחריה ירידת שורה; ReadAllLines מחזיר string[]:

פלט:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

ההבדל בין שני הקוראים חשוב כשמדובר בגודל. ReadAllLines קורא את כל הקובץ לזיכרון לפני שהוא מחזיר. ReadLines מחזיר IEnumerable<string> עצל שקורא תוך כדי מעבר, כך ש-foreach על לוג של 5 GB משתמש בכמה קילובייטים. עם LINQ, ReadLines(...).Where(...).Take(10) מפסיק לקרוא אחרי שמצא עשר התאמות.

פענוח CSV עם Split(',') עובד לקבצים פשוטים שיצרתם בעצמכם. ב-CSV מהעולם האמיתי יש שדות במירכאות שמכילים פסיקים; לאלה השתמשו בספרייה כמו CsvHelper.

שימו לב ל-InvariantCulture ב-decimal.Parse: בלעדיו, פענוח של "4.50" במחשב שמוגדר לגרמנית או לפורטוגזית קורא את הנקודה כמפריד אלפים.

StreamWriter ו-StreamReader

המתודות של File פותחות וסוגרות את הקובץ בכל קריאה. כדי לכתוב הרבה חלקים לאורך זמן, או כדי לקרוא קובץ גדול מדי לזיכרון בלי LINQ, פתחו stream פעם אחת. תמיד עטפו אותו ב-using כדי שייסגר גם אם נזרקת חריגה; writer שלא נסגר עלול לא לרוקן את ה-buffer שלו לעולם, והקובץ נשאר ריק או קטוע.

פלט:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine מחזיר null בסוף הקובץ, וזה מה שמסיים את לולאת ה-while. StreamWriter אוגר את הפלט שלו ב-buffer וכותב אותו לדיסק כשה-buffer מתמלא, כשקוראים ל-Flush(), וכשהוא משוחרר (disposed); בלוק ה-using מבטיח את המקרה האחרון.

מאז C# 8, הצהרת using משחררת את ה-stream בסוף הבלוק העוטף, בלי הסוגריים המסולסלים הנוספים:

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

נתיבים ותיקיות

בנו נתיבים עם Path.Combine ולא בשרשור מחרוזות. הוא מוסיף את המפריד הנכון למערכת ההפעלה (\ ב-Windows, / ב-Linux וב-macOS) ולא מכפיל אותו. המחלקה Path גם מפרקת נתיבים לחלקים, ו-Directory יוצרת תיקיות ומציגה את התוכן שלהן:

פלט:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

מלכודת אחת: אם ארגומנט מאוחר יותר ל-Path.Combine הוא נתיב מושרש (מתחיל ב-/ או ב-\, או באות כונן ב-Windows), כל מה שלפניו נזרק, כך ש-Path.Combine("reports", "/2026") הוא /2026. העבירו חלקים יחסיים, או השתמשו ב-Path.Join (.NET Core 3.0 ואילך), שלא זורק אף חלק.

Directory.GetFiles מחזיר קבצים בסדר שבו מערכת הקבצים נותנת אותם, והסדר הזה שונה בין Windows ל-Linux, אז מיינו את התוצאה כשהסדר חשוב. Directory.EnumerateFiles היא הגרסה העצלה, אותו יחס כמו בין ReadLines ל-ReadAllLines. העבירו SearchOption.AllDirectories כדי לכלול תיקיות משנה.

טיפול בשגיאות

פעולות על קבצים נכשלות מסיבות שמחוץ לתוכנית שלכם: הקובץ חסר, התיקייה לא קיימת, תהליך אחר מחזיק אותו פתוח, הדיסק מלא, ההרשאות שגויות. לכל אחת יש טיפוס חריגה משלה. FileNotFoundException ו-DirectoryNotFoundException יורשות מ-IOException, ולכן תפסו אותן לפניה; UnauthorizedAccessException לא יורשת ממנה, ולכן היא צריכה catch משלה:

פלט:

missing file, using defaults
missing folder, using defaults
theme=dark

File.Exists לפני קריאה נראה פשוט יותר, אבל הוא לא מבטל את הצורך ב-try: הקובץ יכול להיעלם, או להינעל על ידי תוכנית אחרת, ברגע שבין הבדיקה לקריאה. השתמשו ב-Exists כדי להחליט מה לעשות, וב-catch כדי לשרוד את מה שקורה בפועל.

קידוד

File.WriteAllText, WriteAllLines ו-StreamWriter כותבים כברירת מחדל UTF-8 בלי byte order mark, והקוראים מזהים UTF-8, UTF-16 ו-UTF-32 לפי byte order mark אם יש כזה. העבירו Encoding כשקובץ חייב להיות בפורמט מסוים:

פלט:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é ו-ã תופסים שני בתים כל אחד ב-UTF-8, ולכן 14 תווים צריכים 16 בתים, ושלושת הקבצים נקראים בחזרה נכון כי הקורא מזהה את ה-byte order mark. Encoding.UTF8 נראה כמו ברירת המחדל אבל מוסיף byte order mark, שחלק מהכלים (מייבאי CSV ישנים, סקריפטים של shell) מציגים כזבל בתחילת השורה הראשונה; השתמשו ב-new UTF8Encoding(false) בשביל UTF-8 מפורש בלי סימן כזה. קריאה של קובץ בקידוד הלא נכון לא זורקת חריגה: היא מייצרת תווי החלפה, אז בררו באיזה קידוד משתמש קובץ שלא אתם יצרתם.

קלט ופלט אסינכרוני של קבצים

באפליקציות web ובקוד של ממשק משתמש, חסימה של thread על קלט ופלט של דיסק מבזבזת אותו. ב-.NET Core 2.0 ואילך יש גרסאות async של המתודות של File:

await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");

StreamReader.ReadLineAsync ו-StreamWriter.WriteLineAsync עובדות באותה צורה על streams.

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

  • לא לשחרר stream. בלי using, טקסט שנמצא ב-buffer עלול לא להגיע לדיסק לעולם, והקובץ נשאר נעול עד שה-garbage collector רץ.
  • לבנות נתיבים עם + "\\" +. זה נשבר ב-Linux וב-macOS. השתמשו ב-Path.Combine.
  • לטעון קבצים ענקיים עם ReadAllText או ReadAllLines. הזרימו אותם עם ReadLines או עם StreamReader.
  • להסתמך על התיקייה הנוכחית. היא תלויה באופן שבו התוכנית הופעלה. השתמשו בנתיבים מוחלטים או ב-AppContext.BaseDirectory.
  • לפענח מספרים מקבצים לפי התרבות (culture) של המחשב. העבירו CultureInfo.InvariantCulture לקבצי נתונים.
  • לצפות ש-File.Exists ימנע חריגות. הקובץ יכול להשתנות בין הבדיקה לשימוש.

שאלות נפוצות

איך כותבים טקסט לקובץ ב-C#?

File.WriteAllText("notes.txt", text); יוצר את הקובץ, או דורס אותו אם הוא קיים, כותב את המחרוזת ב-UTF-8 וסוגר אותו. השתמשו ב-File.WriteAllLines(path, lines) עבור אוסף של שורות וב-File.AppendAllText(path, text) כדי להוסיף לסוף במקום להחליף. שלושתן נמצאות ב-System.IO.

איך קוראים קובץ טקסט ב-C#?

string text = File.ReadAllText("notes.txt"); קורא את כל הקובץ למחרוזת אחת, ו-string[] lines = File.ReadAllLines(path); מפצל אותו לשורות. לקבצים גדולים השתמשו ב-File.ReadLines(path) בתוך foreach, שקורא שורה אחת בכל פעם במקום לטעון הכול, או ב-StreamReader.

איך מוסיפים לסוף של קובץ ב-C#?

File.AppendAllText(path, text) מוסיף טקסט לסוף הקובץ ויוצר את הקובץ אם הוא לא קיים; File.AppendAllLines(path, lines) עושה את אותו הדבר עם שורות. עם stream, פתחו אותו במצב הוספה: new StreamWriter(path, append: true). זכרו להוסיף בעצמכם Environment.NewLine או \n כשמוסיפים טקסט.

איך קוראים קובץ שורה אחרי שורה ב-C#?

foreach (string line in File.ReadLines(path)) { ... } מזרים את הקובץ שורה אחת בכל פעם, כך שצריכת הזיכרון נשארת קבועה גם עבור לוגים של גיגה-בייטים. המקבילה הישנה יותר היא StreamReader בבלוק using עם while ((line = reader.ReadLine()) != null). הימנעו מ-ReadAllLines לקבצים גדולים: הוא טוען קודם את כל השורות למערך.

איך בודקים אם קובץ קיים ב-C#?

File.Exists(path) מחזיר true אם הקובץ קיים והתוכנית רשאית לראות אותו, ו-Directory.Exists(path) עושה את אותו הדבר לתיקיות. אל תסמכו עליו כדי להימנע מחריגות: הקובץ יכול להימחק או להינעל בין הבדיקה לקריאה, ולכן עדיין טפלו ב-FileNotFoundException וב-IOException סביב פעולת הקובץ עצמה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל