1. Wprowadzenie: dlaczego pliki lubią "wygłupiać się"?
Bywa, że otwierasz dokument — i nagle system mówi: "Plik nie znaleziony". Albo próba zapisu kończy się komunikatem: "Odmowa dostępu". To właśnie te sytuacje, gdy z plikami dzieje się coś dziwnego. Jeśli z kodowaniami mamy do czynienia w postaci "krzaków", to tutaj problemy są związane już z samym systemem plików.
System plików można sobie wyobrazić jako dużą bibliotekę, a aplikację — jako bibliotekarza. I kiedy prosi ona o plik, mogą być różne odpowiedzi:
- Książki (czyli pliku) nie ma — po prostu nie istnieje.
- Sekcja biblioteki, gdzie powinna być książka, też nie istnieje — brak wymaganej katalogu.
- Książka jest "zamknięta" albo wypożyczona — plik jest używany przez inny proces lub brak praw dostępu.
- Półka jest pełna — brak miejsca na dysku, żeby utworzyć nowy plik.
- Albo na przykład próbujesz włożyć książkę do działu zwrotów DVD — czyli wykonywana jest operacja nieobsługiwana.
W języku C# wszystkie te sytuacje wyrażane są przez wyjątki. Zadaniem developera jest nie tylko patrzeć, jak program "upada", ale potrafić przewidzieć możliwe problemy i obsłużyć je elegancko. Mało kto chce, żeby użytkownik zobaczył tajemniczy komunikat o błędzie pełen niezrozumiałego tekstu.
Znamy już konstrukcję try-catch. To nasza deska ratunku, która pozwala "złapać" wyjątek i podjąć działania, zamiast pozwolić programowi zakończyć się awaryjnie.
// To nasz stary znajomy, przypomnienie z wykładu 57
try
{
// Tutaj piszemy kod, który może wyrzucić błąd
// Na przykład próba czytania pliku
}
catch (Exception ex) // Łapiemy dowolny wyjątek
{
// Tutaj obsługujemy błąd
Console.WriteLine($"Ojej, wystąpił błąd: {ex.Message}");
}
Dziś zagłębimy się w specyficzne wyjątki, które pojawiają się przy pracy z plikami. Dzięki temu będziemy pisać bardziej niezawodny kod, który potrafi "rozmawiać" z systemem plików, nawet gdy on postanowi "kaprysić".
Wyjątki — to nie bugi, to sygnały SOS!
Ważne, żeby zrozumieć: wyjątek to nie zawsze błąd w twoim kodzie. Często to sygnał, że coś poszło nie tak w zewnętrznym środowisku, z którym twój kod współpracuje. System plików to jasny przykład takiego środowiska. Możesz napisać całkowicie poprawny kod do czytania pliku, ale jeśli użytkownik usunął plik zanim twoja aplikacja zdążyła go przeczytać, dostaniesz wyjątek. I to jest normalne! Twoim zadaniem jako developera jest nauczyć program reagować na takie sytuacje.
Przyjrzyjmy się najczęstszym "sygnałom SOS", które możesz napotkać przy pracy z plikami.
2. FileNotFoundException: pliku wcale nie było
To chyba najpowszechniejszy wyjątek przy pracy z plikami. Występuje, gdy próbujesz otworzyć, przeczytać lub wykonać inną operację na pliku, który nie istnieje pod wskazaną ścieżką.
Przykład z życia: prosisz kolegę, żeby przyniósł ci książkę "Programowanie w C# 14 dla początkujących" z jego biblioteki, a on odpowiada: "Takiej książki nie mam". Dokładnie tak samo twoja aplikacja może zapytać system operacyjny: "Daj mi plik settings.txt", a system odpowie: "Przykro mi, takiego nie ma".
Spróbujmy napisać kod, który czyta plik abracadabra.txt dla naszej aplikacji-menadżera zadań. Jeśli pliku nie ma, powinniśmy poinformować o tym użytkownika, a nie po prostu "upadać".
try
{
using var reader = new StreamReader("abracadabra.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Plik nie znaleziony: " + ex.FileName);
}
W życiu codziennym to jak iść na przystanek, a autobus nie przyjeżdża — i żadna marszrutka też. Smuteczek.
Do zanotowania: Często ten wyjątek idzie w parze z błędną ścieżką do pliku (np. zapomniałeś, że pracujesz z innego katalogu).
3. DirectoryNotFoundException: foldery gdzieś wyparowały
Ten wyjątek jest bardzo podobny do FileNotFoundException, ale dotyczy nie samego pliku, a katalogu (folderu), w którym ten plik powinien się znajdować. Jeśli wskazujesz ścieżkę, np. "C:\MyDocuments\MyProject\Data\report.txt", a folder Data nie istnieje, dostaniesz DirectoryNotFoundException.
W naszej aplikacji, gdybyśmy chcieli zapisać ustawienia w podfolderze data, np. "./data/app_settings.txt", a takiego folderu by nie było, przy próbie zapisu lub odczytu natrafimy na ten problem.
DirectoryNotFoundException można złapać osobno, tak jak FileNotFoundException, albo może on być częścią bardziej ogólnego IOException, o którym pogadamy później.
try
{
using var writer = new StreamWriter(@"C:\very\strange\path\file.txt");
writer.WriteLine("Hello world");
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine("Katalog nie znaleziony!");
}
Częsty błąd: Katalog może zostać usunięty w dowolnym momencie (np. ktoś sprząta tymczasowe pliki) lub masz błędnie ustawioną ścieżkę zapisu.
4. UnauthorizedAccessException: wstęp wzbroniony!
Wyobraź sobie, że chcesz położyć książkę na półkę, a tam wisi tabliczka "Dostęp tylko dla personelu". To właśnie UnauthorizedAccessException! Występuje, gdy twoja aplikacja nie ma wymaganych uprawnień do pliku lub katalogu. Może to być spowodowane:
- Brakiem uprawnień użytkownika: Próbujesz zapisać plik w folderze, do którego zapisu mogą dokonać tylko administratorzy (np. C:\Windows).
- Plik oznaczony jako "tylko do odczytu": Próbujesz zmodyfikować plik, który ma atrybut tylko do odczytu.
- Plik systemowy lub ukryty: I ma specyficzne ograniczenia.
To bardzo powszechny problem w środowiskach korporacyjnych albo gdy użytkownicy instalują programy w chronionych katalogach.
Spróbujmy zapisać plik w katalogu systemowym, do którego zwykły użytkownik nie ma praw. (Uwaga: uruchamiaj taki kod ostrożnie, żeby nie zaśmiecać katalogów systemowych, albo w "sandboxie").
try
{
using var writer = new StreamWriter("/system/settings.conf");
writer.WriteLine("Wszystka władza studentom!");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Brak dostępu do pliku lub katalogu!");
}
Jeśli uruchomisz ten kod bez uprawnień administratora, najpewniej zobaczysz komunikat "Dostęp do tego katalogu zabroniony". Jeśli uruchomisz go jako administrator, plik prawdopodobnie zostanie utworzony. Ten przykład pokazuje, jak ważne jest obsługiwanie UnauthorizedAccessException, żeby użytkownik zrozumiał, dlaczego aplikacja nie może wykonać operacji.
5. IOException: uniwersalne "oj-oj" systemu plików
IOException – to najbardziej ogólny wyjątek związany z operacjami I/O. Rzucany jest, gdy pojawia się jakiś problem z samym urządzeniem wejścia/wyjścia lub z systemem plików, który nie wchodzi w zakres bardziej specyficznych wyjątków, takich jak FileNotFoundException czy UnauthorizedAccessException.
Typowe scenariusze, kiedy możesz dostać IOException:
- Plik jest już używany przez inną aplikację: Na przykład próbujesz usunąć plik, który jest otwarty w Notatniku lub w innym twoim programie.
- Dysk jest pełny: Brak miejsca na dysku do zapisu pliku.
- Uszkodzony plik lub system plików: Rzadko, ale się zdarza.
- Problemy sieciowe: Jeśli plik znajduje się na dysku sieciowym i połączenie zostało zerwane.
- Nazwa pliku lub katalogu zbyt długa. (To może być też PathTooLongException, ale czasem trafia pod IOException).
IOException to taki "uniwersalny klucz" do wielu problemów. Często, gdy łapiesz IOException, warto też zerknąć na jego właściwość Message, żeby dostać więcej szczegółów, co dokładnie poszło nie tak.
try
{
using var file = new FileStream("busyfile.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
// w jakiś sposób trzymamy plik otwarty
// jednocześnie gdzie indziej:
using var writer = new StreamWriter("busyfile.txt");
writer.WriteLine("Próba zapisu...");
}
catch (IOException ex)
{
Console.WriteLine("Błąd wejścia-wyjścia: " + ex.Message);
}
Ważna uwaga: IOException — to klasa bazowa dla wielu innych "plikowych" wyjątków.
6. Inne, ale nie mniej ważne "niespodzianki"
PathTooLongException
To rzadszy, ale jednak spotykany problem: twoja ścieżka (albo nazwa pliku/katalogu) jest za długa dla systemu operacyjnego. Na przykład, jeśli postanowisz włożyć do nazwy pliku skróconą zawartość "Wojny i Pokoju", Windows ci tego nie wybaczy.
W Windows historyczne ograniczenie to 260 znaków dla pełnej ścieżki. Nowe wersje OS i .NET pozwalają włączyć "długie ścieżki", ale nie zawsze działa to domyślnie.
try
{
string veryLongPath = new string('a', 300); // 300 znaków!
using var writer = new StreamWriter(veryLongPath + ".txt");
writer.WriteLine("To za długie imię pliku!");
}
catch (PathTooLongException ex)
{
Console.WriteLine("Nazwa pliku lub ścieżka za długa!");
}
NotSupportedException
To rzadki, ale "surrealistyczny" przypadek, gdy przekazujesz do konstruktora typu StreamReader albo FileStream niepoprawny ciąg ścieżki, np. ze zabronionymi znakami lub używasz "magicznych" ścieżek typu C:::\wow???\file.txt.
7. Przydatne niuanse
Za co odpowiadają które wyjątki
| Wyjątek | Powód wystąpienia | Przykład sytuacji |
|---|---|---|
|
Plik nie znaleziony | Otwarcie nieistniejącego pliku |
|
Katalog nie znaleziony | Otwarcie pliku w usuniętym folderze |
|
Brak dostępu (uprawnień) | Zapis do chronionego katalogu |
|
Ogólny błąd wejścia-wyjścia | Plik otwarty przez inny proces |
|
Ścieżka za długa | Za długa nazwa pliku/folderu |
|
Nieprawidłowy format ścieżki | Ścieżka z zabronionymi znakami |
Często występujące błędy i specjalne przypadki
Przykład: Plik zajęty przez inny proces
Wyobraź sobie, że jako prawdziwy hacker otworzyłeś plik tekstowy w Notepad i zapomniałeś go zamknąć. W tym momencie twoja aplikacja próbuje zapisać do tego samego pliku. Tutaj przychodzi IOException (albo nawet "sharing violation").
Przykład: Brak dostępu
Spróbuj zapisać dane w C:\Windows bez uprawnień administratora — dostaniesz UnauthorizedAccessException. To samo może się zdarzyć, jeśli otworzyłeś plik tylko do odczytu, a próbujesz go zapisać.
Przykład: Nieprawidłowa ścieżka
W Windows nie można nadawać plikom nazw ze znakami <>:"/\|?*. Jeśli spróbujesz zapisać taki plik — program rzuci NotSupportedException (lub ArgumentException).
Przykład: Brak miejsca na dysku
To, co może wydawać się zaskakujące, również rzuca IOException — np. kiedy dysk jest przepełniony (dlatego warto czasem pamiętać o folderze Downloads).
Jak nie wpaść w tarapaty: najlepsze praktyki wykrywania błędów
- Sprawdzaj istnienie pliku za pomocą File.Exists i Directory.Exists przed próbą otwarcia pliku. Ale uwaga: plik może zniknąć lub pojawić się już po sprawdzeniu (klasyczny race condition).
- Nigdy nie "połykaj" wyjątków całkowicie (nie rób po prostu catch { }), chyba że masz ścisły logger błędów. Zawsze przynajmniej loguj lub pokaż użytkownikowi, co się stało.
- Staraj się łapać konkretne wyjątki (FileNotFoundException, DirectoryNotFoundException), a nie tylko ogólny Exception.
- Dla aplikacji cross-platformowych weź pod uwagę, że uprawnienia, formaty ścieżek, długości nazw plików — różnią się na Windows, Linux, macOS.
- Jeśli przetwarzasz pliki tekstowe, zawsze jawnie podawaj kodowanie — inaczej niespodzianki gwarantowane.
- Do operacji masowych na plikach warto używać przetwarzania "bulk" z uwzględnieniem możliwych awarii na każdym kroku.
Ściąga po typowych wyjątkach
| Wyjątek | Kiedy występuje? | Jak zapobiec/obsłużyć? |
|---|---|---|
|
Brak pliku pod wskazaną ścieżką | Sprawdzaj plik przez File.Exists lub twórz go |
|
Ścieżka zawiera nieistniejący katalog | Sprawdzaj ścieżkę, twórz katalogi przez Directory.CreateDirectory |
|
Brak uprawnień do pliku/katalogu, plik tylko do odczytu, zajęty przez inny proces | Uruchamiaj jako odpowiedni użytkownik, sprawdzaj ACL, poprawnie zamykaj pliki |
|
Ogólny błąd I/O, plik zajęty, brak miejsca na dysku | Używaj try-catch, staraj się nie trzymać plików otwartych |
|
Ścieżka lub nazwa pliku za długa | Skracaj ścieżkę, używaj ścieżek względnych |
|
Nieprawidłowy format ścieżki | Sprawdzaj ciąg ścieżki pod kątem zabronionych znaków |
Schemat blokowy "Co robić przy błędzie z plikiem?"
flowchart TD
A[Operacja na pliku] --> B{Rzucono wyjątek?}
B -- Nie --> C[Operacja zakończona pomyślnie]
B -- Tak --> D{Jaki typ wyjątku?}
D -- FileNotFound --> E[Poproś użytkownika o wskazanie właściwego pliku lub stwórz go]
D -- DirectoryNotFound --> F[Utwórz brakujący katalog]
D -- UnauthorizedAccess --> G[Poproś użytkownika o uruchomienie z właściwymi uprawnieniami]
D -- IOException --> H[Sprawdź, kto trzyma plik, sprawdź dysk]
D -- PathTooLong --> I[Skróć ścieżkę]
D -- NotSupported --> J[Sprawdź format ścieżki]
D -- Other --> K[Pokaż komunikat i przejrzyj logi]
8. Jak typowe wyjątki wyglądają w realnej aplikacji?
Rozwijając nasz ćwiczebny projekt, załóżmy, że mamy mini-program, który zapisuje notatki użytkownika do pliku, a potem je czyta.
string notesPath = "notes.txt";
Console.Write("Wprowadź notatkę: ");
string note = Console.ReadLine();
try
{
// Zapisujemy notatkę
using var writer = new StreamWriter(notesPath, true, Encoding.UTF8);
writer.WriteLine(note);
// Czytamy wszystkie notatki
Console.WriteLine("Twoje notatki:");
using var reader = new StreamReader(notesPath, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
Console.WriteLine("Plik z notatkami nie znaleziony. Spróbuj utworzyć go ręcznie.");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("Ścieżka do pliku z notatkami jest nieprawidłowa. Sprawdź, czy folder istnieje.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Brak uprawnień do zapisu lub odczytu pliku. Uruchom program jako administrator.");
}
catch (IOException ex)
{
Console.WriteLine("Wystąpił błąd wejścia-wyjścia: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Nieoczekiwany błąd: " + ex.Message);
}
W tym przykładzie widać typową sytuację, z jaką może się spotkać każda aplikacja: próba zapisania danych do pliku, a potem ich odczyt. Każdy blok catch obsługuje konkretną klasę błędów — od braku pliku lub folderu po problemy z uprawnieniami i ogólny błąd I/O. Dzięki temu aplikacja nie "upada" przy pierwszym hicie, tylko informuje użytkownika, co dokładnie poszło nie tak i co można zrobić. Takie podejście czyni program bardziej niezawodnym i przyjaznym.
GO TO FULL VERSION