Pętle w programach Basica



      Jeżeli coś ma się wykonać więcej niż jeden raz, umieszczamy to w programie w pętli. Gdy ilość powtórzeń jest znana z góry, stosuje się pętlę wyliczaną FOR-NEXT; w przeciwnym razie - różne odmiany pętli DO-LOOP z badaniem warunku wyjścia z pętli na poczatku lub na końcu pętli. W razie potrzeby można też "wyjść" ze środka pętli - z wyliczanej przez EXIT FOR, a z pętli DO przez EXIT DO.

      W QBasicu jest sześć rodzajów pętli, a licząc z prostą pętlą bez końca - aż siedem. Podobnie jest w Visual Basic. Niektórzy twierdzą, że jest ich za dużo i wystarczyłyby trzy. Mają rację o tyle, że poza pętlą FOR-NEXT wszystkie są odmianami pętli bez końca z badaniem warunku wyjścia i dają się nawzajem zastępować. Ponieważ napotykałem na forach wypowiedzi mówiące o gubieniu się w tej obfitości, oto garść rozważań na temat owych pętli:


Pętla wyliczana (FOR...NEXT)

      Działanie tej pętli nie budzi watpliwości, jest ona dobrze opisana zarówno w Pomocy QB jak i w każdym kursie QBasica. Przypominam ją tu więc dla porządku.

FOR licznik = start TO koniec [STEP przyrost]
    [blok_poleceń]
NEXT [licznik [,licznik]...]

Jeżeli pomijamy STEP, to przyrost o jaki zwiększa się licznik pętli za każdym jej przebiegiem, wynosi domyślnie 1. Przyrost może być zarówno liczbą całkowitą dodatnią, jak i ułamkiem lub liczbą ujemną; w tym ostatnim przypadku pętla odlicza od liczby większej (start) do mniejszej (koniec). Liczby podstawiane do licznika tej pętli rosną więc lub maleją o stałą wielkość, tzn. w postępie arytmetycznym.

Zwykle zmienną licznikową (zwaną też zmienną sterującą) oznaczamy przez i (od iteratio = powtarzanie). Na końcu pętli po NEXT można i umieścić, albo nie, poprawne jest więc samo NEXT, jak i NEXT i. Zwykle je tam jednak umieszczamy, bo przy zagnieżdżonych pętlach łatwiej jest poznać (człowiekowi), której pętli owo NEXT dotyczy. W przypadku dwóch lub więcej pętli ciasno zagnieżdżonych, gdy ich końce spotykają się, można użyć dla wszystkich tych pętli jedno NEXT z wieloma licznikami, ale pamiętać przy tym trzeba, aby wymieniać liczniki w odwrotnej kolejności, tj. licznik pętli najbardziej zagnieżdzonej - na pierwszym miejscu, itd. Natomiast przy użyciu "gołego" NEXT koniecznie musi tych NEXT być tyle, ile było FOR, inaczej QBasic zgłosi błąd #26 "FOR without NEXT" lub #1 "NEXT without FOR". A więc prawidłowe będą wszystkie następujące zapisy:

FOR i = 1 TO x        FOR i = 1 TO x          FOR i = 1 TO x
 FOR j = 0 TO y        FOR j = 0 TO y          FOR j = 0 TO y
   . . .                . . .                   . . .
 NEXT j                NEXT                   NEXT j, i
NEXT i                NEXT

Zmienna licznikowa jest zwykłą zmienną numeryczną i można z nią robić wszystko to, co z innymi zmiennymi liczbowymi. Jeżeli chcemy jej wartość wykorzystać już po zakończeniu pętli, to trzeba przy tym pamiętać, że to NEXT zwiększa jej wartość o przyrost, a FOR porównuje z ograniczeniem stojącym po TO, pętla przestanie wykonywac się, gdy zmienna licznikowa osiągnie wartość większą od tego ograniczenia. Dlatego po wyjściu z pętli zmienna i z powyższych przykładów będzie miała wartość x+1 (ogólnie: koniec + przyrost).

W podręcznikach spotyka się ostrzeżenie, że nie wolno zmieniać zmiennej sterującej w środku pętli. Otóż nie tyle nie wolno, co nie należy, bo wprowadza to niepotrzebny bałagan. Ale można. W QBasicu, wyposażonym w polecenie EXIT FOR, którym możemy opuścić pętlę przed zakończeniem odliczania, oraz w pętle DO...LOOP, pewnie nie ma takiej potrzeby, ale np. w GW-Basicu (i Basicach jeszcze uboższych, np. z ośmiobitowców) wyjście z pętli FOR przed osiągnięciem górnej wartości licznika można było uzyskać na dwa sposoby: albo przez GOTO, albo, gdy wartość zmiennej licznikowej nie była gdzieś dalej wykorzystywana, właśnie przez zmianę tej wartości na większą o 1 od końcowej:

        I sposób                       II sposób
   100 FOR I = 1 TO 10            100 FOR I = 1 TO 10
    . . .                          . . .
   150 IF cośtam THEN 210         150 IF cośtam THEN I = 11    'równoważne GOTO 210
    . . .                          . . .
   200 NEXT I                     200 NEXT I
   210  . . .                     210  . . .

Jako ciekawostkę (bo pierwszy Basic - Dartmouth Basic miał tylko 15 poleceń, ale w tym już miał FOR i NEXT) dodam, że efekt pętli wyliczanej można też uzyskać z poskładanych innych poleceń (z nieszczęsnym GOTO w roli głównej...):

   199 REM Początek pętli 
   200 I = I + 1
    . . .
   IF I = 10 THEN 300    'wyjście z pętli 
   290 GOTO 200          'koniec - zapętlenie pętli 
   300  . . .


Pętle DO...LOOP

      Najprostszym przypadkiem, z którego wywodzą się pozostałe, jest pętla bez końca DO: LOOP. Oczywiście pożytek z takiej pętli żaden, poza tym może, że w czasach królowania DOS-a można było w ten sposób zasymulować komuś zawieszenie komputera - jeśli nie znał zbawiennej kombinacji Ctrl+Break. Musi istnieć warunek wyjścia z pętli. I znowu - najprościej - uzyskuje się go przez IF-THEN i EXIT DO. Jeżeli gubisz się w DO WHILE.., DO UNTIL... i tych samych połączeniach z LOOP, to zawsze poradzisz sobie którymś z trzech sposobów z EXIT:

DO
 IF cośtam THEN EXIT DO  'badanie warunku na początku pętli 
  . . .
  . . .
LOOP
_______________________________________________________________

DO
  . . .
 IF cośtam THEN EXIT DO  'badanie warunku gdzieś w środku pętli 
  . . .
LOOP
_______________________________________________________________

DO
  . . .
  . . .
 IF cośtam THEN EXIT DO  'badanie warunku na końcu pętli 
LOOP

W pętlach DO słowa WHILE i UNTIL określają sposób badania warunku, a ich umieszczenie przy DO lub przy LOOP - miejszce tego badania - początek albo koniec pętli. Chcąc badać warunek gdzieś w środku pętli (np. aby uzyskać odgałęzienie programu), stosujemy, tak jak wyżej, IF-THEN i EXIT DO.

Znaczenia słownikowe WHILE i UNTIL częściowo się pokrywają, w QBasicu jednak są to dwie zupełnie odmienne sytuacje. WHILE przekłada się tu jako "podczas gdy", a UNTIL jako "aż do (włącznie)". Od zbadania warunku na poczatku lub przy końcu pętli zależy, ile razy pętla wykona się; możliwe jest też, że nie wykona się ani razu.

Przykłady:

  WHILE

_________________________
 a = 0
 DO WHILE a = 10        'Ta pętla nie wykona się ani razu, bo już 
  a = a + 1             'na wejściu a <> 10. 
  PRINT a
 LOOP
___________________________________________________________________

 a = 0
 DO                     'Ta pętla wykona się tylko jeden raz, wyni- 
  a = a + 1             'kiem będzie a = 1. Badanie warunku przy 
  PRINT a               'LOOP wykaże, że 1 <> 10 i zakończy pętlę. 
 LOOP WHILE a = 10


Uwaga: Jeżeli warunek "=" zastąpimy przez "<=" (wydawałoby się, że tutaj to wszystko jedno...) to obie pętle WHILE wykonają się, i to nie 10, ale 11 razy! Taka już ich natura...


 a = 0
 DO WHILE a < 10        'Ta pętla wykona się 10 razy. Tak samo 
  a = a + 1             'będzie, kiedy warunek umieścimy przy LOOP. 
  PRINT a
 LOOP
___________________________________________________________________

 a = 0
 DO WHILE a > 10        'Ta pętla nie wykona się ani razu, bo a 
  a = a + 1             'na wejściu nie jest > 10. Gdy warunek 
  PRINT a               'umieścimy przy LOOP, pętla wykona się 
 LOOP                   '1 raz, z tego samego powodu. 
___________________________________________________________________

  UNTIL

      Zamiana WHILE na UNTIL spowoduje, że obie pętle z warunkiem a=10 wykonają się po 10 razy. Z chwilą gdy a będzie = 10, DO UNTIL nie pozwoli na wykonanie 11. przebiegu, tak samo LOOP UNTIL nie pozwoli na dalsze zapętlanie.

_________________________
 a = 0
 DO UNTIL a = 10        'Ta pętla wykona się 10 razy. Tak samo 
  a = a + 1             'będzie,kiedy warunek umieścimy przy LOOP. 
  PRINT a
 LOOP
___________________________________________________________________

 a = 0
 DO UNTIL a < 10        'Ta pętla nie wykona się ani razu. Kiedy 
  a = a + 1             'UNTIL z warunkiem umieścimy przy LOOP, 
  PRINT a               'to pętla wykona się, ale tylko 1 raz. 
 LOOP
___________________________________________________________________

 a = 0
 DO UNTIL a > 10        'Ta pętla wykona się 11 razy, zarówno wtedy 
  a = a + 1             'gdy UNTIL jest przy DO, jak i wtedy, gdy 
  PRINT a               'postawimy je przy LOOP. 
 LOOP UNTIL a < 10


  WHILE... WEND

  a = 0
 WHILE a < 10
  a = a + 1
  PRINT a
 WEND

Pętla WHILE-WEND jest identyczna w działaniu z pętlą DO WHILE... LOOP. Dlatego zwykle w QBasicu nie jest używana, a znalazła się w nim tylko po to, aby zapewnić zgodność z programami napisanymi dawniej w GW-Basicu, w którym to języku jest jedyną oferowaną pętlą strukturalną.

W GW-Basicu struktury działające tak jak zaprezentowane tu inne pętle DO otrzymuje się - a jakże - przy pomocy tworzonych przez GOTO pętli bez końca, badając warunek przez IF-THEN tam gdzie to potrzebne: na początku, w środku lub na końcu pętli. Przykład czegoś takiego był już pokazany wyżej.



Co takiego mylącego jest w tych pętlach, że sprawiają kłopot?

      Rozmyślałem nad tym, sprawdzając przykłady dla tego artykułu i sądzę, że przyczyna leży w słowniku i w pewnej niekonsekwencji tych pętli.

Jeżeli WHILE przetłumaczymy jako podczas gdy, a UNTIL jako aż do, to już z przykładów powyżej widać, że te pętle nie wszędzie zachowują się zgodnie z takim tłumaczeniem.
      Przekładając DO WHILE a < 10 na Wykonuj gdy a  jest mniejsze od 10, ktoś może się spodziewać tylko 9 przebiegów pętli. Tymczasem w pętli wykonującej a=a+1   a=9 (przepuszczone jeszcze, jako mniejsze od 10, przez "strażnika" - DO WHILE) uzyskuje wartość 9+1 = 10 i dopiero ona powoduje, że pętla nie rozpoczyna się na nowo. Podobnie dzieje się, gdy warunek umieścimy przy LOOP: a musi nie spełniać warunku, aby WHILE zatrzymało tutaj bieg pętli.
Z tego samego powodu warunek a<=10 powoduje 11. wykonanie pętli.

      Lepiej jest z tłumaczeniem UNTIL jako aż do lub dopóki nie: LOOP UNTIL a = 10  to Wykonuj aż do a=10, kiedy pamiętamy że chodzi o "10 włącznie". Ale znowu: przy wejściowym a=0 DO UNTIL a < 10 to w przekładzie Wykonuj aż do a < 10 albo Wykonuj, dopóki nie a < 10 i w tym przypadku prawidłowy jest ten drugi przekład: a=0 to równocześnie a<10, warunek jest spełniony i pętla w ogóle się nie rozpoczyna. Postawienie tego warunku przy LOOP spowoduje wykonanie pętli jeden raz (bo nie ma "strażnika" na wejściu), natomiast nie nastąpi zapętlenie przez LOOP.

Nie udało mi się sformułować zwięzłej reguły, jak stosować te pętle. Za każdym razem kiedy muszę użyć którąś z nich, sprawdzam w QBasicu doświadczalnie na prostym przykładzie, czy wychodzi mi to, co chciałem...


Jak nie pogubić się w tej obfitości?

      Wyjść jest kilka. Ja sam zwykle rezygnuję z WHILE i UNTIL, tworząc pętle DO-LOOP bez końca z badaniem warunku przez IF-THEN i wyjściem przez EXIT DO. Odrobina kodu więcej, ale jest mi to intuicyjnie bliższe. Bowiem IF nie zwodzi przy warunkach: tam <10 nigdy nie oznacza oraz 10.

      Ted Felix w swoim kursie QBasica używa tylko konstrukcji DO... LOOP WHILE (głównie do sprawdzania naciśniętego klawisza przy wykorzystaniu INKEY$), a Bob Seguin stosuje w tym samym celu LOOP UNTIL. I rzeczywiście - stosując tylko DO WHILE... oraz LOOP WHILE (albo DO UNTIL i LOOP UNTIL) i odpowiednio konstruując warunek (także uzupełniając badanie warunku zaprzeczeniem NOT), można uzyskać to samo, co przy zastosowaniu wszystkich czterech konstrukcji, np.:

 DO WHILE a < 10          =     DO WHILE NOT (a = 10)     =     DO UNTIL a = 10
  a = a + 1                      a = a + 1                       a = a + 1
  PRINT a                        PRINT a                         PRINT a
 LOOP                           LOOP                            LOOP


 DO WHILE NOT EOF(1)      =     DO UNTIL EOF(1)           =     DO
  LINE INPUT #1, Linia$          LINE INPUT #1, Linia$           LINE INPUT #1, Linia$
  GOSUB AnalizaLinii             GOSUB AnalizaLinii              GOSUB AnalizaLinii
 LOOP                           LOOP                             IF EOF(1) THEN EXIT DO
                                                                LOOP

Jak to nieraz bywa w programowaniu, ten sam efekt można uzyskać różnymi drogami. Jeżeli nie jesteśmy zmuszeni do stosowania jednej z nich (np. szkolnym zadaniem "użyj w programie wszystkie pętle strukturalne"), to możemy wybrać sobie taką, którą najlepiej "czujemy" czy rozumiemy. Z niepolecanym GOTO włącznie...



Damian Gawrych "Deger", luty 2008
http://deger.republika.pl