Ramificare (programare)

Ramificarea în programare este o operație folosită în cazurile în care execuția sau neexecuția unui anumit set de comenzi trebuie să depindă de îndeplinirea sau neîndeplinirea unei anumite condiții. Branching-ul este una dintre cele trei (împreună cu execuția secvențială a comenzilor și bucla ) structuri de bază ale programării structurate .

Principalele forme de implementare a ramificării în limbaje de programare imperative sunt operatorul condiționat (operatorul if) și operatorul de alegere cu mai multe valori (comutator, case, switch). În limbile timpurii de nivel scăzut, ramificarea este implementată prin operatorul de ramificare condiționată .

Operator condiționat

Operatorul condiționat implementează execuția anumitor comenzi, cu condiția ca o expresie logică (condiție) să ia valoarea „adevărat” true. În majoritatea limbajelor de programare, o declarație condiționată începe cu un cuvânt cheie if(tradus din  engleză  -  „dacă”). Există următoarele forme ale operatorului condiționat -- cu o ramură și două.

La executarea unei instrucțiuni condiționale cu o ramură if <условие> then <команды> end, condiția este evaluată, iar dacă este adevărată, atunci se endexecută comenzile până la cuvântul cheie, în caz contrar execuția programului continuă cu comanda care urmează instrucțiunii condiționate. În limbile de nivel scăzut ( asambleri ), aceasta este singura formă disponibilă a operatorului condiționat. În unele limbi, un cuvânt cheie special este folosit pentru un operator condiționat cu o ramură (de obicei, aceasta then, tradus din  engleză  -  „acea”).

La executarea unui operator condiționat cu două ramuri if <условие> then <команды1> else <команды2> end , dacă condiția este adevărată, se execută comenzile de după cuvântul cheie ; dacă condiția este thenfalsă, se execută comenzile de după cuvântul cheie else. Dacă este necesar să verificați mai multe condiții secvenţial, este posibil să faceţi în cascadă instrucţiunile condiţionale:

dacă condiția 1 atunci comandă 1 altfel dacă condiția 2 atunci comandă 2 altfel dacă condiția 3 atunci comandă 3 ... altfel dacă condiția N + 1 atunci comenzile N + 1 altfel comenzile se termină ;

În acest caz, condițiile vor fi verificate secvențial, iar de îndată ce este îndeplinit adevărat, setul corespunzător de comenzi va fi executat și execuția va merge la comanda care urmează instrucțiunii condiționate. Dacă niciuna dintre condiții nu este adevărată, comenzile din ramură sunt executate else.

O serie de limbaje de programare conțin o construcție specială pentru declarații condiționate în cascadă, care vă permite să scrieți ramificații multiple oarecum mai compact și mai puțin predispus la erori de scriere:

if condiție1 atunci comenzi1 elsif condiție2 atunci comenzi2 elsif condiție3 atunci comenzi3 ... altfel comenzile se termină ; ordinea de execuție a acestei instrucțiuni corespunde exact cascadei de mai sus de instrucțiuni simple if-then-else, iar diferența este pur formală: în loc de imbricate mai multe instrucțiuni condiționale, această construcție este un singur întreg și conține un cuvânt cheie suplimentar elsifcare necesită un alt cuvânt cheie. stare după sine.

Implementări

Pascal a moștenit sintaxa de la Algol -60, conform căreia o singură comandă poate fi plasată în ramurile unui operator condiționat. Prin urmare, pentru a găzdui mai multe comenzi acolo, acestea sunt grupate într-o instrucțiune compusă folosind perechea de cuvinte cheie beginși end. Ramura else este opțională. beginși endsunt necesare numai dacă există mai mulți operatori (de exemplu, din motive de uniformitate a codului ). În exemplu, operatorul de alegere în Pascal:

Dacă condiția , instrucțiunile de început ; end else begin declarații ; sfârşitul ;

Necesitatea unui operator condiționat în Algol și Pascal a fost criticată încă de la început. Criticii au spus că numeroase instrucțiuni compuse aglomera programul, interferează cu indentarea normală și provoacă erori (dacă uitați instrucțiunea compusă unde este necesară în ultima ramură a instrucțiunii if, compilatorul nu va observa nimic, dar atunci când execută un program dintr-un grup de instrucțiuni care ar trebui executate în această ramură, conform condiției, se va executa doar prima, toate celelalte - întotdeauna). Următoarele generații de limbi - descendenții lui Algol au încercat să scape de acest neajuns. Printre acestea se numără trei limbi larg cunoscute: Algol-68 , Modula-2 și Ada . Construcția declarației if din ele este aproape aceeași, până la cuvintele cheie individuale:

  • Algol-68:
dacă starea ... fi ;
  • Modula-2
IF condiția 1 THEN comanda 1 ELSE IF condiția 2 THEN comanda 2 ... ELSE comanda N END ;
  • Ada
if condition1 then commands1 else if condition2 then commands2 ... else comenziN end if ;

În toate cazurile, „commandX” este orice număr de instrucțiuni separate prin punct și virgulă. În toate cazurile, toate ramurile instrucțiunii condiționate, cu excepția primei ramuri („atunci”), sunt opționale și pot fi omise. Dacă nu există nicio ramură „altfel” și nici una dintre condiții nu este îndeplinită, atunci controlul este transferat la comanda după cuvântul cheie de completare condiționată (END, FI sau END IF).

C și C++ (și după ele Java , C# , PHP și multe alte limbaje) au un operator condiționat care este structural similar cu Pascal. beginDiferența este că condiția trebuie scrisă între paranteze, iar endparantezele sunt folosite în locul cuvintelor cheie {}:

dacă ( < condiție > ) { < operatori > } altfel { < operatori > }

În Nemerle , spre deosebire de majoritatea limbilor, unde un operator ifpoate avea una sau două ramuri, un operator if(sintaxa este complet similară cu limbajul C) trebuie să aibă două ramuri. Un operator condiționat cu o ramură începe cu cuvântul cheie when, în plus, limbajul are un alt operator condiționat - unless, care este un „invers când” - în el se execută comenzile ramurii condiționate dacă condiția este falsă.

când ( condiție ){ instrucțiuni } cu excepția cazului ( condiție ) { instrucțiuni }

În Forth , operatorul condiționat are o formă diferită decât în ​​alte limbi, datorită notației postfix și organizării stivei. Operatorul condițional este format din trei cuvinte IF ELSE THEN[1] .

< condiție > IF < expresia _1_ dacă _ condiția _ este adevărată > ELSE < expresia _2_ dacă _ condiția _ este falsă > ATUNCI

Aici <условие>doar împinge valoarea în partea de sus a stivei, IFanalizează steag și dacă:

  • nu este egal cu zero, atunci se execută expresiile până la ELSEsau THEN;
  • dacă este egal cu zero, atunci expresiile dintre ELSEși sunt executate THEN.

Dacă lipsește ELSE, se obține un selector cu o ramură: expresiile între IFși THENsunt executate numai dacă steag-ul este diferit de zero.

Fortran a avut inițial doar IF aritmetic , în care, în funcție de semnul expresiei, s-a făcut o tranziție la una dintre cele trei etichete. De exemplu, o parte a codului rutinei de rezolvare a ecuațiilor pătratice:

DN = B * B - 4 * A * C IF ( DN ) 90 , 10 , 10 10 D = SQRT ( DN ) X1 = ( - B + D ) / ( 2 * A ) X2 = ( - B - D ) / ( 2 * A )

Apoi au fost adăugate expresii logice (booleene) și un IF logic cu o singură instrucțiune, evaluat de GOTO, mai târziu - un IF structural (cu mai multe condiții), de exemplu:

DN = B * B - 4 * A * C _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 2 * A ) ELSE ! (fără cod pentru discriminant negativ) END IF

Perl acceptă structura if multi-condițională, precum și modificatorii de instrucțiuni, care sunt scrise după partea din instrucțiune care este executată. De exemplu, următoarele două exemple sunt identice ca funcționalitate:

if ( $a == 0 ) { ++ $număr_zero ; } ++ $zero_count if $a == 0 ;

În loc de if, puteți scrie unless, ceea ce face ca valoarea expresiei condiționate să fie inversată înainte de verificare. Aceeași acțiune prin, cu excepția cazului în care:

++ $zero_count exceptând cazul în care $a != 0 ;

Pentru o instrucțiune compusă (bloc), este permisă doar forma structurală, nu modificatorul. De exemplu:

if ( $a == 0 ) { ... } else if ( $a > 0 ) { ... } else { ... }

Cuvântul cheie final nu este necesar, din cauza cerinței ca instrucțiunile în condițiile să fie formatate în blocuri {…}.

Nu există echivalent cu excepția cazului în care pentru ramurile elsif.

Erlang folosește două declarații condiționale - dacă și caz. Ambele au o valoare de rezultat care este egală cu valoarea ultimei instrucțiuni din ramura executată și pot fi utilizate (atribuite unui nume, transmise unei funcții...), deci nu există o instrucțiune condițională ternară separată în ea . În instrucțiunea case, se efectuează potrivirea modelelor , cu posibilitatea unor condiții suplimentare asupra valorilor din instrucțiunea comparată, iar în instrucțiunea if, doar testarea condiției. Testele de gardă permit un set limitat de operații și funcții încorporate.

Exemplu de caz (ștergerea unei intrări de eveniment din arborele de timp):

{ NewEBW , NewEBN } = case dict : găsiți ( EBN , Node ) de eroare -> { EBW , EBN }; { ok , Expiry } -> { gb_trees : delete_any ({ Expiry , Node }, EBW ), dict : erase ( Node , EBN )} end ,

Exemple despre dacă:

if NeedLater -> erlang : send_after ( trunc ( 1000 * ( 1 + Elapsed )), self (), i_apply_nodes_portion ); adevărat -> nup final , După2 = dacă %% Dacă a fost prea mult în urmă, programați imediat timeout. După1 =< ? EXPIRY_STEP_MIN -> ? EXPIRY_STEP_MIN ; După1 >= ? EXPIRY_STEP_MAX -> ? EXPIRY_STEP_MAX ; adevărat -> După 1 sfârșit ,

Operator cu alegere multiplă

Designul comutatorului are mai multe (două sau mai multe) ramuri. Comutatorul execută o ramură dată în funcție de valoarea expresiei cheii evaluate. Diferența fundamentală dintre această instrucțiune și un operator condiționat este că expresia care determină alegerea ramurii executabile returnează nu o valoare logică, ci o valoare întreagă, sau o valoare al cărei tip poate fi turnat într-un număr întreg. Unele limbi permit ca anumite expresii de tip non-intreger (de exemplu, șiruri de text) să fie folosite într-un comutator.

Prototipul construcției sintactice moderne a fost instrucțiunea de salt folosită în vechile limbaje de programare. Această comandă a specificat o expresie selector care a returnat o valoare întreagă și un set de etichete. Când comanda a fost executată, expresia a fost evaluată, iar valoarea ei a fost folosită ca număr al etichetei (în lista de comenzi) la care s-a făcut tranziția. Astfel de construcții au fost, de exemplu, în limbajele de programare Fortran ("computed GOTO") și BASIC . Partea atractivă a designului este eficiența sa destul de ridicată: pentru a determina ramura dorită (marcatorul de salt), nu este necesar să comparați secvențial rezultatul expresiei selectorului cu multe valori, este suficient să stocați o serie de instrucțiuni de sărituri necondiționate. cu adresele necesare în memorie pentru ca atunci când comanda este executată, elementul dorit să fie calculat direct din valorile expresiei. În acest caz, viteza de execuție a comenzii nu depinde de numărul de etichete. În limbile moderne, implementarea unei instrucțiuni switch este adesea implementată ca un tabel de salt, constând din comenzi de salt necondiționate către fragmentele de cod corespunzătoare. Expresia care trebuie evaluată este convertită într-o valoare de schimbare a tabelului de salt care specifică comanda care trebuie executată. În limbile în care expresia selectorului poate avea o valoare care nu este întreagă, este departe de a fi întotdeauna posibil să se evalueze direct ramura dorită a construcției comutatorului, așa că folosesc alte metode pentru optimizarea execuției.

În limbajele de programare moderne de nivel înalt, o comandă de comutare are de obicei un nume switch(tradus din  engleză  -  „comutator”) sau case(tradus din  engleză  -  „caz”). Cu toate acestea, selecția etichetelor calculate poate fi reținută în limbaje moderne de programare de nivel scăzut, cum ar fi instrucțiunea JL a limbajului de programare STL pentru controlerele logice programabile S7-300 și S7-400 fabricate de Siemens .

De exemplu, în C, sintaxa comenzii este:

comutator ( i ) { cazul 0 : cazul 1 : // secvența de instrucțiuni break ; cazul 2 : // secvența de instrucțiuni break ; implicit : ; }

Aici i este o expresie de selecție care trebuie să fie de tip întreg castabil, fiecare ramură de execuție începe cu cuvântul cheie case, urmat de valoarea expresiei sub care urmează să fie executată această ramură. O caracteristică interesantă a limbajului C este că în el comutatorul este interpretat exact ca o comandă pentru a sări pe o etichetă calculată, iar anteturile de ramuri ( case значение :) joacă rolul de etichete. Pentru a ieși din instrucțiunea switch după ce codul de ramură este completat, este utilizată o comandă specială break. Dacă nu există o astfel de comandă în ramură, după executarea codului ramurii selectate, va începe executarea codului care urmează. Această caracteristică poate fi folosită pentru optimizare, deși poate provoca erori greu de găsit (dacă programatorul pierde accidental o pauză, compilatorul nu va arunca o eroare, dar programul va rula incorect). Ramura implicită este executată atunci când niciuna dintre celelalte ramuri nu este adecvată.

Sintaxa comenzii C switch este moștenită de multe limbi, dar semantica sa nu este întotdeauna complet asemănătoare C. De exemplu, C# vă permite să utilizați o expresie selector de tip șir și etichete adecvate.

Caracteristici ale calculului expresiilor logice

Ordinea de execuție a unui program cu instrucțiuni condiționate poate fi afectată semnificativ de logica evaluării expresiilor condiționate adoptată în limbaj. Când condiția este o expresie booleană complexă, cum ar fi „f(x) > 0 ȘI g(y) > 0”, există două strategii pentru evaluarea rezultatului său:

  • Calcul complet: calculați f(x), g(y), comparați rezultatele cu zero, apoi efectuați o operație AND asupra rezultatelor. La fel și, de exemplu, Visual Basic.
  • Calcul incomplet: calculați f(x), comparați valoarea cu zero. Dacă rezultatul comparației este „adevărat”, atunci evaluați restul expresiei. Dacă prima condiție este falsă, atunci săriți peste a doua condiție, inclusiv calculul g(y) inclus în ea, deoarece pentru operația „ȘI”, dacă unul dintre operanzi este fals, întreaga expresie este evident falsă.

A doua opțiune este cea mai comună pentru limbaje industriale (în special pentru Algol, Fortran, C++, C, Java, JavaScript, ECMAScript, JScript, C#, Python). Aceste limbi au o regulă strictă: „O expresie logică este întotdeauna evaluată de la stânga la dreapta și evaluarea ei se oprește de îndată ce rezultatul întregii expresii este definit”. Aceasta înseamnă că, dacă o expresie constă din mai multe subcondiții combinate cu operatorul AND (ȘI), atunci evaluarea expresiei se va opri de îndată ce una dintre subcondiții se dovedește a fi falsă (deoarece „fals AND orice valoare” are întotdeauna ca rezultat „fals”), și invers, dacă mai multe subcondiții sunt combinate cu operatorul SAU (OR), evaluarea se va opri după prima subcondiție adevărată, deoarece în acest caz întreaga expresie este adevărată, indiferent de evaluarea ulterioară. Dar o expresie care conține operatorul SAU exclusiv (XOR) nu poate fi evaluată pe deplin, deoarece una dintre valorile din ea nu poate determina rezultatul calculului întregii expresii.

Limbile Ada și Erlang folosesc cuvinte cheie diferite pentru aceste variante: cuvintele și și sau înseamnă evaluare completă, și apoi, sau altfel (Ada) și, de asemenea, orelse (Erlang) - incomplet. În Erlang, andalso și orelse au o prioritate mai mică decât operatorii de comparație, ceea ce evită parantezele în jurul termenilor elementari. Limbajul Visual Basic .NET are cuvinte cheie similare: AndAlso și OrElse.

Ordinea fixă ​​de evaluare a subcondițiilor (expresia logică se evaluează întotdeauna de la stânga la dreapta) este introdusă pentru a putea controla ordinea de evaluare a expresiei și a pune în ea mai întâi acele condiții care ar trebui evaluate mai întâi. Apropo, așa diferă expresiile logice de cele aritmetice, pentru care, în majoritatea limbilor, ordinea evaluării subexpresiilor (cu excepția cazului în care este determinată de prioritatea și asociativitatea operațiilor) nu este specificată de limbaj și poate fi diferită în cazuri diferite.

Alegerea acestei logici de execuție se datorează faptului că vă permite să simplificați expresiile logice care folosesc elemente dependente. Exemplul clasic este o căutare liniară într-o matrice:

// Căutare într-o matrice de numere întregi în limbajul Pascal // Parametri - valoarea dorită și un tablou deschis de numere întregi // Rezultat - indexul elementului găsit sau -1 dacă elementul nu este găsit funcția Find ( e : integer ; var a : matrice de întregi ) : întreg ; var i : întreg ; începe i := 0 ; în timp ce ( i <= High ( a )) AND ( a [ i ] <> e ) do inc ( i ) ; // !!! if i <= High ( a ) then Find := i else Find := - 1 ; sfârşitul ;

Algoritmul implementat de program este destul de evident, dar există o subtilitate în implementare (vezi linia marcată cu semne de exclamare): condiția buclei constă din două părți conectate de operatorul AND. Prima subcondiție verifică dacă indexul i a depășit matricea, a doua verifică dacă elementul curent al matricei nu este egal cu valoarea dorită. Dacă tabloul nu conține valoarea dorită, atunci după verificarea ultimului element, valoarea variabilei i va crește cu unu; la următoarea iterație, prima subcondiție va fi falsă și bucla se va încheia fără a verifica a doua subcondiție. Dacă expresiile logice au fost evaluate complet, atunci dacă nu a existat niciun element în matrice după ultima iterație, ar apărea o eroare: o încercare de a determina a[i] ar cauza un acces incorect la memorie.

Trebuie remarcat că, pe lângă evaluarea incompletă a valorii expresiei, ordinea fixă ​​de evaluare a subcondițiilor joacă, de asemenea, un rol important aici: deoarece verificarea matricei în afara limitelor este scrisă prima, va fi întotdeauna efectuate înaintea verificării pentru atingerea valorii dorite. Dacă ordinea în care au fost evaluate subexpresiile ar fi nedefinită, ar fi imposibil să se garanteze corectitudinea fragmentului de program dat.

Cu calculul complet al expresiilor logice, algoritmul de mai sus ar trebui să fie scris aproximativ în următoarea formă:

// Căutare într-o matrice de numere întregi în limbajul Pascal // Varianta ipotetică cu evaluarea completă a expresiilor logice // Parametri - valoarea dorită și un tablou deschis de numere întregi // Rezultat - indexul elementului găsit sau -1 dacă elementul nu a fost găsită funcția Find ( e : integer var a : array of integer ) : întreg ; _ var i : întreg ; f : boolean ; // variabilă suplimentară - flag de terminare a buclei begin i := 0 ; f := fals ; în timp ce nu f do if i > High ( a ) then f := true else if a [ i ] = e then f := true else inc ( i ) ; if i <= High ( a ) then Find := i else Find := - 1 ; sfârşitul ;

După cum puteți vedea, a trebuit să stabilim artificial ordinea în care au fost calculate condițiile de ieșire și să introducem o variabilă suplimentară. Tocmai pentru a evita astfel de trucuri se introduce evaluarea incompletă a expresiilor logice.

Notă: Codul de mai sus este un exemplu de utilizare a instrucțiunii IF, dar nu mai mult. Acest cod nu poate fi folosit ca regulă pentru scrierea algoritmilor în Pascal.

Algoritmul optim pentru găsirea unui număr într-o matrice este:

function Find ( e : integer ; var a : array of integer ) : intreg ; var i : întreg ; începe Rezultatul := - 1 ; for i := Low ( a ) to High ( a ) do begin if a [ i ] = e then begin Result := i ; rupe ; sfârşitul ; sfârşitul ; sfârşitul ;

Note

  1. Forth are un operator <условие> ?DUP <выражение>care dublează o expresie dacă o condiție este adevărată