ב-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 סביב פעולת הקובץ עצמה.