Otwieranie pliku z ogólną obsługą błędów

0

W większości przypadków, gdy potrzebowałem otwierać dowolny plik stosowałem taką, lub podobną konstrukcję:

var
  fInput: File of Byte;
  wCount: Word;
begin
  AssignFile(fInput, 'C:\File.dat');

  try
    Reset(fInput);
    BlockRead(fInput, wCount, SizeOf(wCount));
  finally
    CloseFile(fInput);
  end;

  ReadLn;
end.

Co oczywiście wyklucza obsługę jakichkolwiek błędów podczas odczytu;


Załóżmy, że potrzebuję otworzyć plik, przy czym sprawdzić, czy faktycznie został otwarty, po czym jeśli operacja powiodła się rozpocząć wczytywanie danych; Jeżeli podczas odczytu danych wystąpi błąd to też go wychwycić; Po wszystkim plik zamknąć i gotowe; Błąd podczas odczytu może się zdarzyć jeśli plik będzie uszkodzony, np. zbyt krótki lub wczytywane dane będą nieprawidłowe, np. odczytuję datę, ale pobrany ciąg danych będzie uszkodzony czy przekłamany, czyli wartość dla daty będzie nieprawidłowa;

Na myśl przychodzi taka konstrukcja:

var
  fInput: File of Byte;
  wCount: Word;
begin
  AssignFile(fInput, 'C:\File.dat');

  try
    try
      Reset(fInput);

      try
        BlockRead(fInput, wCount, SizeOf(wCount));
      except
        on Exception do
          WriteLn('Read Exception');
      end;
    finally
      CloseFile(fInput);
    end;
  except
    on Exception do
      Write('Reset Exception.');
  end;

  ReadLn;
end.

i wszystko byłoby w porządku (bo jeśli plik nie istnieje bądź jest blokowany przez inny proces wyjątek zostanie poprawnie przechwycony), jednak żeby następnie przechwycić błąd podczas odczytu danych trzeba było dodać kolejny blok try .. except pod Reset, dzięki czemu tworzy się drabinka, choć taka konstrukcja nawet by mnie satysfakcjonowała, bo spełnia założenia;


Można także wykorzystać obsługę błdów I/O i odpowiednio sprawdzić IOResult:

var
  fInput: File of Byte;
  wCount: Word;
begin
  AssignFile(fInput, 'C:\File.dat');
  {$I-}
  Reset(fInput);
  {$I+}

  if IOResult = 0 then
    try
      try
        BlockRead(fInput, wCount, SizeOf(wCount));
      except
        on Exception do
          WriteLn('Read Exception');
      end;
    finally
      CloseFile(fInput);
    end
  else
    WriteLn('Reset Exception');

  ReadLn;
end.

i też poprawnie zostaje obsłużony błąd otwarcia, a jeśli plik zostanie otwarty poprawnie ale odczyt danych się nie powiedzie - plik zostanie zamknięty a błąd wychwycony i wyświetlony komunikat Read Exception; Taka kontrukcja też spełnia założenia, więc także można by ją wykorzystywać;

Sprawdzam różne konstrukcje i zamykanie pliku w tej sytuacji musi być wykonane jedynie wtedy, gdy IOResult = 0, bo inaczej dostaję nieznany wyjątek programowy; To nie jest dziwne, bo zamkniętego pliku nie można zamknąć;


Moje pytanie brzmi: w jaki sposób poprawnie obsługiwać błędy związane z dostępem do pliku oraz odczytywaniem danych?

Nie potrzebuję rozróżniać nie wiadomo ile błędów, jedynie błąd otwarcia pliku i błąd odczytu danych z pliku; Z racji tej, że na razie nie mam jakiegoś super skutecznego sposobu chciałbym się dowiedzieć w jaki sposób Wy implementujecie obsługę błędów związanych z wczytywaniem danych z pliku; Proszę o przedstawienie Waszych metod, z których korzystacie na co dzień;

1

Moje pytanie brzmi: w jaki sposób poprawnie obsługiwać błędy związane z dostępem do pliku oraz odczytywaniem danych?

Wszystkie sposoby które przedstawiłeś są poprawne. Osobiście najczęściej używam wysłużonego {$I-} żeby uniknąć wyjątku albo zostawiam {$I+} i po prostu pozwalam się w takim wypadku programowi wyłożyć na głównym systemie obsługi błędów (jeżeli plik musi dać się otworzyć żeby program poprawnie działał).

Proszę o przedstawienie Waszych metod, z których korzystacie na co dzień;

procedure TMyPe.Read(s: ansistring);
var
  f:file of boolean;
  i,j,n:integer;
  oldfmode:byte;
const
  MZMagic:word=23117;
  PEMagic:dword=17744;
begin
  oldfmode:=filemode;
  filemode:=0;
  assign(f,s);
  {$I-}reset(f);
  if IOResult<>0 then raise exception.Create('Cannot read file');
  BlockRead(f,doshead,sizeof(doshead),i);
  if i<>sizeof(doshead) then raise exception.Create('Cannot read header!');
  if MZMagic<>doshead.e_magic then raise exception.Create('This file isn''t magic!');
  n:=filepos(f);
  setlength(subdata,doshead.e_lfanew-n);
  BlockRead(f,subdata[0],length(subdata),i);
  //seek(f,doshead.e_lfanew);
  if filepos(f)<>doshead.e_lfanew then raise exception.Create('Subdata read fail');
  BlockRead(f,nthead,sizeof(nthead),i);
  if i<>sizeof(nthead) then raise exception.Create('Cannot read nt header');
  if nthead.Signature<>PEMagic then raise exception.Create('This isn''t valid file for me.');

Jeżeli nie chciałbym aby program używał wyjątków to zrobił bym sobie result z booleanem albo intem i używał exit(-1); i podobnych. Rozwiązanie może i nieidealne ale preferuję programowanie liniowe nad wiele rozgałęzień i długich bloków if. Jeżeli się zastanawiasz czemu np. tutaj nie używam w ogóle finally aby zwolnić pliki etc. to wynika to z tego że błędy przy odczycie plików zazwyczaj traktuję jako non-recoverable error a ta biblioteka jest używana głównie w aplikacjach konsolowych gdzie wychodzenie z exceptionem nie jest niczym wyjątkowym więc nigdy nawet nie przechwytuję tych wyjątków, najwyżej dodam piękny czerwony komunikat.
Generalnie to w moim kodzie rzadko kiedy można się doszukać bloków finally albo except ponieważ wyjątki traktuję jako poważne problemy które raczej nie powinny występować w przypadku poprawnej operacji programu.

1

Wg mnie, poprawnie obsługujesz.
Możesz też całość dać w {$I-} {$I+} (lub tylko BlockRead()/BlockWrite) oraz badać IOResult po BlockRead()/BlockWrite;

Albo zostaw w spokoju mechanizm pascala z epoki kamienia łupanego i przejdź na porządny TFileStream obsługa błędów staje się jednolita.

0
-321oho napisał(a)

Wszystkie sposoby które przedstawiłeś są poprawne.

Poprawne tak, jednak pierwsze nie posiada obsługi błędów, stąd wszystkimi zajmie się system, a to w ogóle nie wchodzi w grę; Chcę obsługiwać wszystkie błędy jakie mogą powstać w programie i sygnalizować je własnym oknem dialogowym (z odpowiednią informacją);

-321oho napisał(a)

Osobiście najczęściej używam wysłużonego {$I-} żeby uniknąć wyjątku albo zostawiam {$I+} i po prostu pozwalam się w takim wypadku programowi wyłożyć na głównym systemie obsługi błędów (jeżeli plik musi dać się otworzyć żeby program poprawnie działał).

Wymienione dyrektywy to dobra sprawa, bo podobnie do wyjątków pozwalają na rozróżnienie typu błędu, ale że jest ich mnóstwo (sygerując się np. listą) to wolałbym traktować je ogólnie; Bo użytkownikowi nie są potrzebne takie informacje, po co mu wiedzieć czy uchwyt jest właściwy czy nie, czy suma kontrolna się zgadza itd.; Najważniejszą grupę błędów mam wyznaczoną:

  1. plik nie istnieje,
  2. brak dostępu do pliku (plik jest blokowany przez inny proces),
  3. nieprawidłowa budowa pliku,
  4. nieprawidłowy nagłówek,
  5. nieoczekiwany koniec pliku (czyli pętla wczytująca odpowiednie bloki danych zbyt wcześnie natrafia na koniec pliku)
    i to w sumie tyle - uznałem, że tylko takie informacje są potrzebne użytkownikowi do ocenienia przyczyny błędu; Niestety (a może nawet "stety") użytkownicy mogą być totalnymi matołami, więc trzeba było wybrać te najważniejsze, które nawet totalny laik potrafiłby zrozumieć i odpowiednio zareagować;
-321oho napisał(a)

Jeżeli nie chciałbym aby program używał wyjątków to zrobił bym sobie result z booleanem albo intem i używał exit(-1); i podobnych. Rozwiązanie może i nieidealne ale preferuję programowanie liniowe nad wiele rozgałęzień i długich bloków if.

Ja nie mam nic przeciwko wyjątkom i mogę je stosować, jednak wychodzenie Exit'em z parametrem nie wchodzi w grę, bo ustalenie kodu wykonania funkcji zaimplementowane jest wewnątrz niej; A co długich if'ów to mnie nie przeszkadzają, nie mam z nimi w ogóle problemów; Czasem jednak lepiej jest przerywać działanie procedury przez Exit i liniowo sprawdzać różne rzeczy, ale to wszystko zależy od wykonywanych operacji;

_13th_Dragon napisał(a)

Albo zostaw w spokoju mechanizm pascala z epoki kamienia łupanego i przejdź na porządny TFileStream obsługa błędów staje się jednolita.

Taka konstrukcja mi nie przeszkadza, bo nie najgorzej się z niej korzysta, ale chyba już czas zacząć korzystać ze strumieni; Naszkicowałem sobie kod realizujący wczytanie pliku do strumienia i odczytujący jedną liczbę, jednak aby obsłużyć błędy musiałem podobnie jak wcześniej zapisać trzy bloki:

var
  fsInput: TFileStream;
  wCount: Word;
begin
  try
    fsInput := TFileStream.Create('C:\File.dat', fmOpenRead or fmShareDenyNone);

    try
      try
        fsInput.ReadBuffer(wCount, SizeOf(wCount));
      except
        on Exception do
          WriteLn('Read Exception');
      end;
    finally
      fsInput.Free();
    end;
  except
    on Exception do
      WriteLn('Open Exception');
  end;

  ReadLn;
end.

W takim przypadku gdy plik nie istnieje lub jest pusty wyświelony zostanie komunikat Open Exception, a jeśli błąd spowoduje kopiowanie danych ze strumienia do zmiennych zostanie wyświetlony komunikat Read Exception; Dobrze myślę, czy wypada to zabezpieczyć inaczej?

1

Poco tak komplikować:

var
  fsInput: TFileStream;
  wCount: Word;
begin
  try
    fsInput := TFileStream.Create('C:\File.dat', fmOpenRead or fmShareDenyNone);
    fsInput.ReadBuffer(wCount, SizeOf(wCount));
    fsInput.Free();
  except
    on E:EFileCreateError do WriteLn('Nie udało się stworzyć pliku');
    //on E: ....
    on E:Exception do WriteLn('Open Exception '+E.ClassName);
  end;
 
  ReadLn;
end.

Pozostałe rodzaje wyjątków popatrz w źródle lub w dokumentacji lub zwyczajnie metodą eksperymentu, pokaże się nazwa kłasy.

1
  try
    fsInput := TFileStream.Create('C:\File.dat', fmOpenRead or fmShareDenyNone);
 
    try
      try
        fsInput.ReadBuffer(wCount, SizeOf(wCount));
      except
        on Exception do
          WriteLn('Read Exception');
      end;
    finally
      fsInput.Free();
    end;
  except
    on Exception do
      WriteLn('Open Exception');
  end;

A czemu nie tak?

try
    fsInput := TFileStream.Create('C:\File.dat', fmOpenRead or fmShareDenyNone);
  except
    on Exception do
      WriteLn('Open Exception');
  end;
 
    try
      try
        fsInput.ReadBuffer(wCount, SizeOf(wCount));
      except
        on Exception do
          WriteLn('Read Exception');
      end;
    finally
      fsInput.Free();
    end;

Oczywiście nie masz tu jeszcze rozpoznania błędu, również on Exception do writeln('zgaduje blad'); jest złe bo równie dobrze może wystąpić jakiś problem którego nie przewidziałeś a ty go zaszufladkujesz jako 'coś'.

Ja nie mam nic przeciwko wyjątkom i mogę je stosować, jednak wychodzenie Exit'em z parametrem nie wchodzi w grę, bo ustalenie kodu wykonania funkcji zaimplementowane jest wewnątrz niej;

Zawsze możesz zrobić procedurę która będzie wywoływać generyczną procedurę i z dokładnych kodów błędów wyłapywać te które nas interesują i je zwracać w odpowiedni sposób. Będzie to rozwiązanie które pozwoli pisać zarówno dokładne procedury co poszło nie tak (nr. błędu) jak i ogólnie (wiadomość odpowiadająca zakresowi numerów).

nieprawidłowa budowa pliku,
nieprawidłowy nagłówek,
nieoczekiwany koniec pliku (czyli pętla wczytująca odpowiednie bloki danych zbyt wcześnie natrafia na koniec pliku)

Pierwszy błąd jest ogólną definicją kolejnych dwóch.
Błąd drugi może wynikać z błędu trzeciego.
Błąd trzeci może wynikać np. z uszkodzonego nagłówka który mimo wszystko przeszedł.

Jeżeli chcesz mieć debiloodporną konstrukcję to po prostu wszystko wrzuć do ogólnej definicji 'plik uszkodzony' bo to że wydaje ci się że plik nie powinien się jeszcze kończyć może równie dobrze oznaczać uszkodzenie nagłówka. Nie lubię zbyt dokładnych opisów błędu bo często wprowadzają w błąd.

Poza tym, właściwie nie wiem do czego Furious zmierzasz. Szukasz najlepszej metody odczytu pliku debilo-odpornie? Bo na początku pytałeś o procedury które się używa najczęściej: Najczęściej używa się jednego, góra dwóch komunikatów błędów o problemie z plikami, do tego można dorzucić jakiś numer który coś znaczy (ale nikt nie wie co).

0
-31oho napisał(a)

Oczywiście nie masz tu jeszcze rozpoznania błędu, również on Exception do writeln('zgaduje blad'); jest złe bo równie dobrze może wystąpić jakiś problem którego nie przewidziałeś a ty go zaszufladkujesz jako 'coś'.

Racja, to jest złe podejście, bo wyłapywanie wyjątków na zasadzie on Exception do jest zbyt ogóle i daje możliwości rozpoznania typu błędu i jego przyczyny;

-321oho napisał(a)

Pierwszy błąd jest ogólną definicją kolejnych dwóch.
Błąd drugi może wynikać z błędu trzeciego.
Błąd trzeci może wynikać np. z uszkodzonego nagłówka który mimo wszystko przeszedł.

Ah, za bardzo się rozpędziłem i zdublowałem niektóre... Ogólnie zamierzałem sprawdzić dlaczego nie można poprawnie wczytać danych ze strumienia (lub utworzyć danego strumienia) i przekazać wiadomość użyszkodnikowi; Interesują mnie błędy związane z:

  1. niemożnością otwarcia pliku (obojętne czy nie istnieje, czy jest blokowany),
  2. błędnym nagłówkiem (czyli albo jest uszkodzony, albo to niewłaściwy plik),
  3. nieprawidłową budową (czyli dane są niekompletne lub uszkodzone);
    czyli do pierwszego podchodzi wyjątek EFOpenError, do drugiego własny EInvalidHeader, a do trzeciego EReadError; Kod jakim sprawdzałem wyjątki (selektywnie) poniżej:
type
  EInvalidHeader = class(Exception);
var
  wCount: Word;
begin
  try
    with TFileStream.Create('C:\File.dat', fmOpenRead) do
    begin
      ReadBuffer(wCount, SizeOf(wCount));

      if wCount <> 0 then
        raise EInvalidHeader.Create('');

      Free();
    end;
  except
    on EFOpenError    do WriteLn('Stream open error');
    on EReadError     do WriteLn('Stream read error');
    on EInvalidHeader do WriteLn('Invalid file header');
    else
      WriteLn('Unknown exception');
  end;

  ReadLn;
end.

A jeśli wystąpi wyjątek inny, niż sprawdzane to wyświetlony zostanie komunikat Unknown exception - w programie zostanie przekazany komunikat np. Wystąpił nieoczekiwany błąd podczas odczytu danych z pliku.;

Nigdy nie obsługiwałem ręcznie (selektywnie) wyjątków, więc musiałem się nauczyć ich obsługi, ale warto było;

-321oho napisał(a)

Nie lubię zbyt dokładnych opisów błędu bo często wprowadzają w błąd.

Ja też nie lubię, a skoro program ma być po części "debiloodporny" to ten wspomniany "debil" musi rozumieć co się do niego mówi, więc szczegółowe opisy błędów nie wchodzą w grę, ale też zbyt ogólne nie mogą być; Tak jak jest - jest dobrze;

-321oho napisał(a)

Poza tym, właściwie nie wiem do czego Furious zmierzasz. Szukasz najlepszej metody odczytu pliku debilo-odpornie? Bo na początku pytałeś o procedury które się używa najczęściej: Najczęściej używa się jednego, góra dwóch komunikatów błędów o problemie z plikami, do tego można dorzucić jakiś numer który coś znaczy (ale nikt nie wie co).

W sumie to miałem dwa cele - poznać Wasze sposoby obsługi błędów i zaimplementowanie w miarę debiloodpornego algorytmu na wypadek problemów z odczytem; I jedno i drugie zostało rozwiązane - wykorzystanie klasy TFileStream, selektywna obsługa wyjątków, w miarę ogólnikowe komunikaty; Dziękuję za odpowiedzi - teraz czas zaimplementować je w programie;

1 użytkowników online, w tym zalogowanych: 0, gości: 1