Namespace System.IO zawiera wszystko do pracy z plikami. W większości zadań wystarczy jedno wywołanie na statycznej klasie File: otwiera plik, czyta go lub zapisuje i zamyka. Przy dużych plikach albo gdy potrzebujesz dokładnej kontroli, użyj StreamReader i StreamWriter. Do każdego przykładu poniżej dodaj using System.IO;.
Zapis i odczyt całego pliku
File.WriteAllText tworzy plik (albo nadpisuje istniejący) z podanym tekstem. File.ReadAllText zwraca cały plik jako jeden string. File.AppendAllText dopisuje na końcu:
Wynik:
True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False
Ścieżka względna, taka jak "notes.txt", jest rozwiązywana względem bieżącego katalogu procesu, a to nie zawsze folder, w którym leży twój program (usługa, zadanie harmonogramu czy test runner mogą startować gdzie indziej). Dla plików dostarczanych razem z aplikacją buduj ścieżkę od AppContext.BaseDirectory.
WriteAllText nadpisuje bez pytania. Jeśli utrata starej zawartości ma znaczenie, najpierw sprawdź File.Exists albo zapisz do pliku tymczasowego i przenieś go na miejsce przez File.Move.
Linie: WriteAllLines, ReadAllLines i ReadLines
Pliki z rekordami, po jednym w linii, są na tyle częste, że mają własne metody. WriteAllLines zapisuje każdy string z nową linią na końcu; ReadAllLines zwraca string[]:
Wynik:
4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00
Różnica między tymi dwoma metodami czytającymi ma znaczenie przy rozmiarze. ReadAllLines wczytuje cały plik do pamięci, zanim cokolwiek zwróci. ReadLines zwraca leniwy IEnumerable<string>, który czyta w trakcie iterowania, więc foreach po logu o rozmiarze 5 GB zużywa kilka kilobajtów. Z LINQ wyrażenie ReadLines(...).Where(...).Take(10) przestaje czytać, gdy znajdzie dziesięć dopasowań.
Parsowanie CSV przez Split(',') działa dla prostych plików generowanych przez ciebie. Prawdziwe pliki CSV mają pola w cudzysłowach zawierające przecinki; do nich użyj biblioteki takiej jak CsvHelper.
Zwróć uwagę na InvariantCulture w decimal.Parse: bez tego parsowanie "4.50" na komputerze z ustawieniami niemieckimi lub portugalskimi traktuje kropkę jako separator tysięcy.
StreamWriter i StreamReader
Metody File otwierają i zamykają plik przy każdym wywołaniu. Aby zapisywać wiele kawałków w czasie albo czytać plik za duży na pamięć bez LINQ, otwórz strumień raz. Zawsze obejmij go blokiem using, żeby został zamknięty nawet po rzuceniu wyjątku; niezamknięty writer może nigdy nie opróżnić bufora i plik skończy się pusty albo ucięty.
Wynik:
2: WARN disk 85% full
4: ERROR connection refused
ReadLine zwraca null na końcu pliku i to kończy pętlę while. StreamWriter buforuje wyjście i zapisuje je na dysk, gdy bufor się zapełni, gdy wywołasz Flush() i gdy zostanie zwolniony; blok using gwarantuje ten ostatni przypadek.
Od C# 8 deklaracja using zwalnia strumień na końcu otaczającego bloku, bez dodatkowych nawiasów klamrowych:
using var writer = new StreamWriter(path); // C# 8: disposed when the method returns
writer.WriteLine("INFO server started");
Ścieżki i katalogi
Buduj ścieżki przez Path.Combine, a nie przez sklejanie stringów. Wstawia właściwy separator dla systemu operacyjnego (\ w Windows, / w Linuksie i macOS) i go nie dubluje. Klasa Path potrafi też rozkładać ścieżki na części, a Directory tworzy i listuje foldery:
Wynik:
february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False
Jedna pułapka: jeśli dalszy argument Path.Combine jest ścieżką bezwzględną (zaczyna się od / lub \ albo, w Windows, od litery dysku), wszystko przed nim zostaje odrzucone, więc Path.Combine("reports", "/2026") daje /2026. Przekazuj części względne albo użyj Path.Join (.NET Core 3.0 i nowsze), która nigdy niczego nie odrzuca.
Directory.GetFiles zwraca pliki w kolejności, w jakiej poda je system plików, a ta różni się między Windows i Linuksem, więc sortuj wynik, gdy kolejność ma znaczenie. Directory.EnumerateFiles to wersja leniwa, w takiej samej relacji jak ReadLines do ReadAllLines. Przekaż SearchOption.AllDirectories, aby uwzględnić podfoldery.
Obsługa błędów
Operacje na plikach zawodzą z powodów spoza twojego programu: plik nie istnieje, nie ma folderu, inny proces trzyma plik otwarty, dysk jest pełny, uprawnienia są złe. Każdy przypadek ma własny typ wyjątku. FileNotFoundException i DirectoryNotFoundException dziedziczą po IOException, więc łap je przed nim; UnauthorizedAccessException nie dziedziczy, więc potrzebuje własnego catch:
Wynik:
missing file, using defaults
missing folder, using defaults
theme=dark
File.Exists przed odczytem wydaje się prostsze, ale nie zwalnia z try: plik może zniknąć albo zostać zablokowany przez inny program w chwili między sprawdzeniem a odczytem. Używaj Exists, żeby zdecydować, co zrobić, a catch, żeby przetrwać to, co faktycznie się stanie.
Kodowanie znaków
File.WriteAllText, WriteAllLines i StreamWriter domyślnie zapisują UTF-8 bez znacznika kolejności bajtów (BOM), a metody czytające rozpoznają UTF-8, UTF-16 i UTF-32 po znaczniku BOM, jeśli jest obecny. Przekaż Encoding, gdy plik musi mieć konkretny format:
Wynik:
plain.txt 16 bytes, reads back unchanged: True
bom.txt 19 bytes, reads back unchanged: True
utf16.txt 30 bytes, reads back unchanged: True
é i ã zajmują w UTF-8 po dwa bajty, dlatego 14 znaków potrzebuje 16 bajtów, a wszystkie trzy pliki odczytują się poprawnie, bo metoda czytająca rozpoznaje znacznik BOM. Encoding.UTF8 wygląda jak ustawienie domyślne, ale dodaje BOM, który niektóre narzędzia (starsze importery CSV, skrypty powłoki) pokazują jako śmieci na początku pierwszej linii; użyj new UTF8Encoding(false), gdy chcesz jawnie UTF-8 bez niego. Odczyt pliku w złym kodowaniu nie rzuca wyjątku: daje znaki zastępcze, więc sprawdź, jakiego kodowania używa plik pochodzący z zewnątrz.
Asynchroniczne operacje na plikach
W aplikacjach webowych i kodzie UI blokowanie wątku na operacjach dyskowych go marnuje. .NET Core 2.0 i nowsze mają asynchroniczne wersje metod File:
await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");
StreamReader.ReadLineAsync i StreamWriter.WriteLineAsync działają tak samo na strumieniach.
Typowe błędy
- Brak zwolnienia strumienia. Bez
usingzbuforowany tekst może nigdy nie trafić na dysk, a plik pozostaje zablokowany, dopóki nie zadziała garbage collector. - Budowanie ścieżek przez
+ "\\" +. Psuje się w Linuksie i macOS. UżywajPath.Combine. - Wczytywanie ogromnych plików przez
ReadAllTextlubReadAllLines. Czytaj je strumieniowo przezReadLinesalboStreamReader. - Poleganie na bieżącym katalogu. Zależy od tego, jak uruchomiono program. Używaj ścieżek bezwzględnych albo
AppContext.BaseDirectory. - Parsowanie liczb z plików według ustawień regionalnych komputera. Dla plików z danymi przekazuj
CultureInfo.InvariantCulture. - Oczekiwanie, że
File.Existszapobiegnie wyjątkom. Plik może się zmienić między sprawdzeniem a użyciem.
Najczęściej zadawane pytania
Jak zapisać tekst do pliku w C#?
File.WriteAllText("notes.txt", text); tworzy plik albo nadpisuje go, jeśli istnieje, zapisuje string w UTF-8 i zamyka plik. Użyj File.WriteAllLines(path, lines) dla kolekcji linii i File.AppendAllText(path, text), aby dopisać na końcu zamiast zastępować. Wszystkie trzy są w System.IO.
Jak odczytać plik tekstowy w C#?
string text = File.ReadAllText("notes.txt"); wczytuje cały plik do jednego stringa, a string[] lines = File.ReadAllLines(path); dzieli go na linie. Dla dużych plików użyj File.ReadLines(path) w pętli foreach, która czyta po jednej linii zamiast ładować wszystko, albo StreamReader.
Jak dopisać tekst do pliku w C#?
File.AppendAllText(path, text) dodaje tekst na końcu pliku i tworzy plik, jeśli nie istnieje; File.AppendAllLines(path, lines) robi to samo dla linii. Przy strumieniu otwórz go w trybie dopisywania: new StreamWriter(path, append: true). Pamiętaj, żeby przy dopisywaniu samodzielnie dodać Environment.NewLine albo \n.
Jak czytać plik linia po linii w C#?
foreach (string line in File.ReadLines(path)) { ... } czyta plik strumieniowo, po jednej linii, więc zużycie pamięci jest stałe nawet przy logach o rozmiarze gigabajtów. Starszy odpowiednik to StreamReader w bloku using z while ((line = reader.ReadLine()) != null). Unikaj ReadAllLines dla dużych plików: najpierw ładuje każdą linię do tablicy.
Jak sprawdzić, czy plik istnieje w C#?
File.Exists(path) zwraca true, jeśli plik istnieje i program może go zobaczyć, a Directory.Exists(path) robi to samo dla folderów. Nie polegaj na tym, żeby uniknąć wyjątków: plik może zostać usunięty lub zablokowany między sprawdzeniem a odczytem, więc nadal obsługuj FileNotFoundException i IOException wokół właściwej operacji na pliku.