1. Wprowadzenie
Operacje na plikach prawie zawsze są związane z okolicznościami zewnętrznymi względem programu: plik może zostać usunięty, przeniesiony, zablokowany przez inny program, nagle użytkownik może stracić uprawnienia albo zabraknąć miejsca na dysku. To wszystko może prowadzić do wyjątków — specjalnych sytuacji, kiedy wykonanie programu zostaje przerwane i "wyrzucany" jest obiekt wyjątku (Exception). Jeśli nie przechwycisz i nie obsłużysz takiego wyjątku, twój program zakończy się błędem i pokaże użytkownikowi straszny czerwony tekst w konsoli (albo, co gorsza — cicho upadnie).
W C# do obsługi takich sytuacji używa się bloku try-catch (albo, prościej, "pułapka na błędy"). Pozwala on nie tylko "złapać" błąd, ale też spokojnie zdecydować, co dalej zrobić: pokazać przyjazny komunikat, zaproponować wybór innego pliku, zapisać błąd do loga albo po prostu spróbować ponownie.
W prawdziwych aplikacjach
Jeśli piszesz konsolowy kalkulator, to pewnie możesz obyć się bez łapania błędów przy pracy z plikami. Ale jeśli tworzysz program do przetwarzania dokumentów w firmie albo grę zapisującą stan — nieobsługiwanie takich błędów to jak chodzenie po linie bez zabezpieczenia: prędzej czy później coś się wydarzy.
2. Przypomnienie pracy z wyjątkami
Podstawowa składnia try-catch
Zanim przejdziemy do konkretnych sytuacji z plikami, przypomnijmy podstawową składnię konstrukcji try-catch (którą już spotykaliśmy na wykładach o wyjątkach):
try
{
// Tutaj kod, który może "wyrzucić" wyjątek
}
catch (Exception ex)
{
// Tutaj łapiemy wszystkie możliwe wyjątki... Ale lepiej tak nie robić!
Console.WriteLine("Coś poszło nie tak: " + ex.Message);
}
Wewnątrz try piszemy potencjalnie niebezpieczny kod, a w catch — co robić, jeśli coś pójdzie nie tak.
Żart programisty: "try-catch — to taki cyfrowy odpowiednik czapki-niewidki na błędy. Błąd jest, ale go nie widzisz!"
Wyjątki plikowe
Kiedy pracujesz z plikami przez .NET, najczęściej spotykasz wyjątki — na przykład przy tworzeniu strumienia, odczycie lub zapisie. Oto kilka typowych scenariuszy i związanych z nimi wyjątków:
- Plik nie znaleziony → FileNotFoundException
- Brak dostępu do pliku/katalogu → UnauthorizedAccessException
- Problemy ze ścieżką (np. niedozwolone znaki, zbyt długa ścieżka) → PathTooLongException, ArgumentException
- Plik jest już używany przez inny proces → IOException
- Brak miejsca na dysku → IOException
Najczęściej przy operacjach plikowych spotykamy pochodne IOException. Wszystkie one dziedziczą od bazowego typu System.Exception.
3. Praktyka: łapanie i obsługa błędów przy pracy z plikami
Spójrzmy na typowe przypadki na przykładach naszego "rozwijającego się" aplikacji: załóżmy, że próbujemy odczytać powitanie z pliku, przetworzyć je i wypisać użytkownikowi. Dodamy tu sprawdzanie błędów za pomocą try-catch.
Przykład 1: Obsługa "plik nie znaleziony"
string filePath = "hello.txt";
try
{
string greeting = File.ReadAllText(filePath);
Console.WriteLine("Zawartość pliku: " + greeting);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Plik nie znaleziony! Sprawdź, czy plik " + filePath + " istnieje.");
// Możemy dodatkowo wypisać szczegóły
Console.WriteLine("Szczegóły techniczne: " + ex.Message);
}
Jeśli plik "hello.txt" nie istnieje, program nie zakończy się błędem, tylko wypisze przyjazny komunikat. Tak po prostu czynimy program odpornym na "głupie" błędy użytkownika.
Przykład 2: Brak uprawnień
Weźmy trudniejszą sytuację: plik istnieje, ale nie mamy praw do jego odczytu (np. ktoś ustawił tylko prawa zapisu albo plik leży w chronionym folderze).
try
{
string secret = File.ReadAllText("C:\\Windows\\System32\\config.txt");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Brak dostępu do pliku! Spróbuj uruchomić program jako administrator.");
Console.WriteLine("Przyczyna techniczna: " + ex.Message);
}
Przykład 3: Ogólne IOException
Niektóre błędy mogą być związane z konfliktem blokad (np. inny proces trzyma plik otwarty), brakiem miejsca na dysku albo problemami sprzętowymi:
try
{
File.WriteAllText("important.txt", "Ważna informacja!");
}
catch (IOException ex)
{
Console.WriteLine("Błąd przy pracy z plikiem: najprawdopodobniej jest używany przez inny program albo na dysku brak miejsca.");
Console.WriteLine("Przyczyna techniczna: " + ex.Message);
}
4. Przechwytywanie wielu wyjątków: selektywna obsługa
Czasami trzeba reagować różnie na różne typy wyjątków. W .NET można podać kilka bloków catch — od bardziej specyficznych do ogólnych (inaczej kompilator urządzi twojemu kodowi głośny włoski strajk).
try
{
string content = File.ReadAllText("file.txt");
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Plik nie znaleziony.");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Brak dostępu do pliku!");
}
catch (IOException ex)
{
Console.WriteLine("Inny błąd wejścia-wyjścia: " + ex.Message);
}
Ważne: jeśli postawisz catch (Exception ex) pierwszym, pozostałe bloki stracą sens, bo typ bazowy przechwyci wszystko!
5. Zagnieżdżone try-catch i ponowne próby
Czasem chcesz nie tylko obsłużyć błąd, ale i dać użytkownikowi szansę "naprawić się" — na przykład zaproponować wpisanie ścieżki do istniejącego pliku:
string filePath;
string content;
int attempts = 0;
const int maxAttempts = 3;
do
{
Console.Write("Wpisz ścieżkę do pliku: ");
filePath = Console.ReadLine();
try
{
content = File.ReadAllText(filePath);
Console.WriteLine("Zawartość pliku:\n" + content);
break;
}
catch (FileNotFoundException)
{
Console.WriteLine("Plik nie znaleziony! Spróbuj jeszcze raz.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Brak dostępu do pliku! Spróbuj innego pliku.");
}
attempts++;
}
while (attempts < maxAttempts);
if (attempts == maxAttempts)
Console.WriteLine("Zbyt dużo nieudanych prób.");
Ten kod to mały "przyjazny interfejs" dla użytkownika, który zapomniał, gdzie zapisał plik.
6. Typowe błędy i cechy: od lenistwa do świadomości
Bardzo częsty błąd początkujących (a nawet doświadczonych deweloperów) — łapać wszystkie wyjątki po kolei, pisać po prostu catch (Exception) i wypisywać "Wystąpił błąd!", nie zastanawiając się nad przyczynami. Takie podejście jest złe z kilku powodów. Po pierwsze, ukrywa rzeczywiste niedociągnięcia w logice biznesowej aplikacji. Po drugie, inne, niezwiązane z plikami błędy (np. literówki w kodzie albo błąd matematyczny) mogą zostać przypadkowo przeoczone, i znalezienie prawdziwej przyczyny potrwa dużo dłużej.
Znacznie lepiej — jawnie łapać tylko te błędy, które potrafisz sensownie obsłużyć. Jeśli nie wiadomo, jaki błąd zaszedł, lepiej pozwolić mu "upaść" — czyli nie łapać go wcale: niech aplikacja padnie, ale zobaczysz stack trace i zrozumiesz, co trzeba poprawić.
Cecha: Niektóre wyjątki mogą mieć zagnieżdżone przyczyny (InnerException). Warto je analizować dla dokładniejszej diagnostyki, zwłaszcza gdy piszesz logi błędów.
Jeszcze jeden niuans — jeśli po bloku catch dalsze działanie programu jest niemożliwe (np. nie udało się otworzyć głównego pliku konfiguracyjnego), możesz zakończyć działanie przez return albo nawet wyrzucić wyjątek ponownie (throw;), żeby nie tworzyć "zombie-aplikacji" z połową funkcji.
GO TO FULL VERSION