CodeGym /Kursy /C# SELF /Typowe wyjątki przy pracy z plikami

Typowe wyjątki przy pracy z plikami

C# SELF
Poziom 38 , Lekcja 0
Dostępny

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
FileNotFoundException
Plik nie znaleziony Otwarcie nieistniejącego pliku
DirectoryNotFoundException
Katalog nie znaleziony Otwarcie pliku w usuniętym folderze
UnauthorizedAccessException
Brak dostępu (uprawnień) Zapis do chronionego katalogu
IOException
Ogólny błąd wejścia-wyjścia Plik otwarty przez inny proces
PathTooLongException
Ścieżka za długa Za długa nazwa pliku/folderu
NotSupportedException
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ć?
FileNotFoundException
Brak pliku pod wskazaną ścieżką Sprawdzaj plik przez File.Exists lub twórz go
DirectoryNotFoundException
Ścieżka zawiera nieistniejący katalog Sprawdzaj ścieżkę, twórz katalogi przez Directory.CreateDirectory
UnauthorizedAccessException
Brak uprawnień do pliku/katalogu, plik tylko do odczytu, zajęty przez inny proces Uruchamiaj jako odpowiedni użytkownik, sprawdzaj ACL, poprawnie zamykaj pliki
IOException
Ogólny błąd I/O, plik zajęty, brak miejsca na dysku Używaj try-catch, staraj się nie trzymać plików otwartych
PathTooLongException
Ścieżka lub nazwa pliku za długa Skracaj ścieżkę, używaj ścieżek względnych
NotSupportedException
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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION