Obsługa błędów - Programowanie jest łatwe

Czasami do radzenia sobie z błędami nie trzeba wyjątków tylko przemyślanej konstrukcji funkcji. Podzielę się z kilkoma dobrymi radami w tej kwestii.

electrical-cable-mess-2654084_1920.jpg Wyobraźmy sobie prostą funkcję do przeszukiwania wzorca, która ma zwrócić pozycję, na której wzorzec występuje. Będzie to wartość z zakresu 0 do n. Co w przypadku, gdy wzorzec nie zostanie napotkany?

Z tego typu dylematami spotykamy się na co dzień. Najprostsze wydaje się zwracać wartość ujemną w przypadku, gdy czegoś nie ma, nie występuje, nie działa jak należy. Dla dobra późniejszych implementacji, które będą wykorzystywać ten fragment kodu warto przyjąć, że każdy błąd będzie posiadać swój kod błędu, czyli kolejną wartość ujemną. W ten sposób łatwiej będzie kontrolować sytuację w "kodzie wyżej".

W przypadku, gdy operujemy na funkcjach, które zwracają informację "udało się albo błąd" warto przyjąć konwencję, która panuje w systemach Unix/Linux (errno, czyli w przypadku, gdy wszystko poszło jak należy zwracana jest wartość 0, w innym przypadku wartość -1 i dodatkowo ustawiany jest kod błędu. Dla uproszczenia można pominąć ustawianie kodu błędu i po prostu zwracać go jako wartość większą, albo mniejszą od 0, należy jednak konsekwentnie stosować wartości dodatnie albo ujemne.

Pisząc kod warto także, w ramach możliwości, a to zawsze jest możliwe, stosować odpowiednią dyscyplinę jeżeli chodzi o to, w jaki sposób są zwracane wartości. Np. Zakładamy, że poprawna wartość zawsze zwracana jest na końcu funkcji albo odwrotnie- błąd jest zwracany na końcu a w środku wystąpienie return oznacza zwrócenie wartości prawidłowej. Mieszanie tych rzeczy utrudnia późniejszą analizę kodu, a także, moim zdaniem, jest nieco niechlujne. Trzecim sposobem jest przechowywanie wyniku funkcji w zmiennej, która zostaje zwrócona na końcu funkcji (albo w środku, jeżeli nie ma sensu kontynuować algorytmu ze względu na zastaną sytuację). Wybór odpowiedniego podejścia do zwracanych wartości ma też tę zaletę, że narzuca styl pisania i łatwiej jest zabrać się za pisanie pierwszych linii algorytmu.

Poniżej przykłady każdego z tych sposób na podstawie funkcji do wyszukiwania wystąpienia pierwszej pozycji wzorca:

Poprawna wartość na końcu

To podejście jest korzystne, gdy np. nie chcemy kontynuować algorytmu, kiedy napotkamy błąd.

int szukaj(const char *tekst, const char *wzorzec) {
  int pozycja = 0;
  int i = 0;
  int k = 0;

  while (tekst[i] != 0) {
    if (tekst[i] == wzorzec[0]) {
      k = 0;
      while (wzorzec[k] != 0) { 
        if (tekst[i+k] == 0) {
          // koniec tekstu, wzorzec dluzszy niz tekst
          return -2;
        }

        if (tekst[i+k] != wzorzec[k]) {
          break;
        }
        k++;
      }

      if (wzorzec[k] == 0) {
        pozycja = i;
        break;
      }
    }
    i++;
  }

  if (pozycja[i] == 0) {
    // przeszukano cały tekst ale nie znaleziono wzorca
    return -1;
  }

  return pozycja;
}

Błąd na końcu, poprawna wartość najszybciej, jak to możliwe

To podejście może być interesujące, gdy potrzebny jest punkt, w którym obsłużymy jakiekolwiek napotkanie błędu- na samym końcu funkcji możemy dodać jeszcze ekstra kod, który coś zrobi w związku z napotkaniem błędu, np. zwolni pamięć.

int szukaj(const char *tekst, const char *wzorzec) {
  int blad = 0;
  int i = 0;
  int k = 0;

  while (tekst[i] != 0) {
    if (tekst[i] == wzorzec[0]) {
      k = 0;
      while (wzorzec[k] != 0) { 
        if (tekst[i+k] == 0) {
          // koniec tekstu, wzorzec dluzszy niz tekst
          blad = -1;
          break;
        }

        if (tekst[i+k] != wzorzec[k]) {
          break;
        }
        k++;
      }

      if (wzorzec[k] == 0) {
        return i;
      }

    }
    i++;
  }

  if (pozycja[i] == 0) {
    // przeszukano cały tekst ale nie znaleziono wzorca
    blad = -1;
  }

  return blad;
}

Jeden punkt zwrotu funkcji

Może być użyteczne, gdy na końcu funkcji potrzebujemy jeszcze coś sprawdzić w obu przypadkach, gdy wszystko poszło tak jak należy albo gdy jednak natrafiono na błąd.

int szukaj(const char *tekst, const char *wzorzec) {
  int wartosc_koncowa = -1;
  int i = 0;
  int k = 0;

  while (tekst[i] != 0) {
    k = 0;
    while (wzorzec[k] != 0) {

      if (tekst[i+k] == 0) {
        // koniec tekstu, wzorzec dluzszy niz tekst
        wartosc_koncowa = -2;
        break;
      }

      if (tekst[i+k] != wzorzec[k]) {
        break;
      }

      k++;
    }

    if (wzorzec[k] == 0) {
      // wzorzec pasuje
      wartosc_koncowa = i;
      break;
    }

    i++;
  }

  return wartosc_koncowa;
}

Jakie jest Twoje podejście do takich sytuacji? Czy kiedykolwiek rozmyślałeś/aś o tym jak obsługiwanie błędów wpływa na konstrukcję funkcji?

Dodano: 2018-04-10 09:58 przez Piotr Poźniak

obsługa błędów , warunki , return ,
Piotr Poźniak
O autorze:

Programuję od ponad 15 lat. Prowadzę software house. Angażuję i zachęcam wszystkich do programowania w ramach inicjatywy Programowanie jest łatwe.