Kirjautuminen

Haku

Tehtävät

Keskustelu: Yleinen keskustelu: Ohjelmointikielten GOTO-rakenne

Sivun loppuun

LCHawk [30.10.2015 20:24:14]

#

Monissa vanhoissa ohjelmointikielissä näyttää olevan GOTO-rakenne, jolla hypätään ohjelman suorituksessa tietylle riville. Osaisiko joku selittää, miksei tätä rakennetta enää käytetä, vaan miksi se on korvattu silmukoilla? Löysin Fortran-kielen Wikipedia-artikkelista tiedon, että GOTO-rakennetta olisi hankalampi optimoida kuin silmukoita, mutta miksi näin on? Eikö se ole käytännössä sama asia?

Eki++ [30.10.2015 20:50:26]

#

En tiedä optimoinnista tai teknisestä toteutuksesta tarpeeksi, että voisin siitä sanoa, mutta ainakin osasyy varmaankin on goto-häsläyksen armoton epäselvyys ja vaikea seurattavuus. Saman asian voi aina hoitaa funktioilla ja silmukoilla paljon selvemmin.

Metabolix [30.10.2015 21:05:33]

#

Oletko koskaan koettanut tehdä oikeaa koodia GOTO-rakenteella? Kokeile! Mielestäni seuraava koodiesimerkki vastaa hyvin kysymykseesi siitä, miksi GOTO-rakenteen sijaan käytetään yleensä nykyään silmukoita. Esimerkissä on sama toiminnallisuus silmukoilla ja GOTO-rakenteilla.

for (int i = 1; i <= 10; ++i) {
  int muuttuja_silmukassa = 1234;
  while (!ehto) {
    koodi;
  }
}
int i = 1;
a:
int muuttuja_silmukassa = 1234;
b:
if (ehto) {
  goto c;
}
koodi;
goto b;
c:
i += 1;
if (i <= 10) {
  goto a;
}

GOTO-rakenne on varmastikin alkujaan tullut ohjelmointiin siksi, että se vastaa prosessorin luontaista toimintatapaa ja on siis ollut helppo toteuttaa. Se on ”korvattu” silmukoilla siksi, että silmukat kuvaavat paremmin ohjelman loogista toimintaa (esimerkiksi ”käy läpi luvut 1–10, ja jokaisella toista asiaa X, kunnes saavutetaan Y”), kun taas GOTO kuvaa ainoastaan ohjelman teknistä toimintaa (esimerkiksi ”aloita ykkösestä, tee X, tarkista ehto Y, ja jos se ei toteudu, mene takaisin X-kohtaan; korota lukua yhdellä, ja jos luku ei ole 11, mene takaisin X-kohtaan”).

GOTO rikkoo loogisen rakenteen, jossa ohjelma koostuu erillisistä koodilohkoista. Ei enää ole selvää, mikä asia on ”silmukan sisällä”, vaan hyppyjä voi tapahtua hyvinkin mielivaltaisesti. Tämä johtaa sotkuiseen koodiin, jonka kulkua on vaikea hahmottaa. Ohjelmointivirheiden mahdollisuus kasvaa. (Jos sanot, että voihan GOTO-koodinkin sisentää kuin silmukan ja olla hyppäämättä samaan paikkaan muualta, vastaan: onneksi olkoon, olet juuri keksinyt uudestaan silmukan!)

Optimointi on vaikeaa osittain samasta syystä. Silmukasta kääntäjän on helppo tunnistaa, että koodia on tarkoitus toistaa ja että toistamiselle on tietty ehto tai kierrosmäärä. Jos koodissa on GOTO, kääntäjälle ei ole mitenkään automaattisesti selvää, onko koodissa ylipäänsä silmukkaa. Muissakin kuin silmukan tapauksessa GOTO vaikeuttaa optimointia siksi, että samaan pisteeseen koodissa voidaan tulla useasta eri paikasta. Toki nykyään optimoinnin puute ei ole yhtä suuri ongelma kuin ennen, koska tietokoneissa riittää resursseja koodin analysointiin ja kääntäjät ovat sen myötä kehittyneet.

Monissa nykyisissäkin kielissä on GOTO-rakenne. Eräs jotenkuten hyväksyttävä käyttötarkoitus sille on useasta sisäkkäisestä silmukasta poistuminen (kun break ei riitä). Useimmissa tilanteissa kuitenkin koodi on mahdollista ja selvempää toteuttaa jollain muulla tavalla.

On tilanteita, joissa GOTO ei edes toimi järkevästi. Jos esimerkiksi hypätään muuttujan määrittelyn yli, onko muuttujaa olemassa ja mitä muuttuja sisältää? Joissain kielissä tällaiset hypyt on kielletty, ja joissain kielissä tämä on varmasti ollut myös yhtenä perusteluna sille, että GOTO-rakennetta ei ole toteutettu ollenkaan.

Jalmari91 [31.10.2015 10:06:06]

#

Mielestäni yksi hyvä käyttötarkoitus gotolle C:ssä on Linux ytimestäkin tuttu:

int funktio()
{
        int err;

        lukitse mutex, allokoi muistia...

        err = tee_jotain();
        if (err < 0) {
                goto jotain_epäonnistui;
        }

        ...

        err = 0;

jotain_epäonnistui:
        ... deallokoi muistia, avaa mutex
        return err;
}

Jos taas jokaisessa virheentarkistuksessa tehdään erikseen resurssien vapautukset, niin helposti unohtuu jostain haarasta jokin vapautus ja muutenkin tulee turhaan ylimääräistä koodia. Lisäksi mielestäni kyseinen tapa ei aiheuta sekavuutta koodiin (pikemminkin päinvastoin).

peran [31.10.2015 13:01:28]

#

Jalmari91 kirjoitti:

Mielestäni yksi hyvä käyttötarkoitus gotolle C:ssä on Linux ytimestäkin tuttu:

C++:ssa on ainakin try catch-rakenne tätä varten.

Jalmari91 [31.10.2015 13:53:07]

#

peran kirjoitti:

C++:ssa on ainakin try catch-rakenne tätä varten.

Mielestäni yksi hyvä käyttötarkoitus gotolle C:ssä on Linux ytimestäkin tuttu:

jlaire [31.10.2015 14:03:26]

#

Tuo on tosiaan se yleisin poikkeus. Monet hyvin toteutetut C-kirjastot käyttävät gotoa tällä tavalla joka paikassa.

peran kirjoitti:

C++:ssa on ainakin try catch-rakenne tätä varten.

C++:ssa on destructorit tätä varten (RAII). Resurssien vapautus catch-blokissa on virhealtista ja huonoa C++-tyyliä.

Jaska [31.10.2015 18:15:54]

#

Miksi tuossa Putkan logossa käytetään gotoa eikä jotain nykyaikaisempaa virheenkäsittelyä? Gotosta löytyy mielipiteitä myös kansainvälisellä ohjelmointipalstalla.

Metabolix [31.10.2015 19:05:27]

#

Jaska kirjoitti:

Miksi tuossa Putkan logossa käytetään gotoa eikä jotain nykyaikaisempaa virheenkäsittelyä?

Kyseessä on logo eikä ohjelma. Teksti on ollut logossa vuodesta 2003 saakka, eli sillä on iloinen historiansa eikä sen muuttamisesta olisi mitään hyötyä. Teksti myös sopii hyvin logoon: ”ON ERROR GOTO Ohjelmointiputka” on mahdollista lukea ja ymmärtää, vaikka ei osaisi ohjelmoida, ja ”ON ERROR GOTO” sopii nätisti yhdelle riville. Miten muotoilisit vastaavan selkokielisen viestin logoon jollain modernimmalla rakenteella?

vesikuusi [01.11.2015 14:03:10]

#

Itsehän luulin että Jaska heitti läppää.

jlaire [22.11.2015 05:27:44]

#

Palaanpa vähän vanhaan aiheeseen. Huomasin sattumalta, että algoritmijumala Knuth käyttää gotoa edelleen hyvin ahkerasti. TAOCP-kirjasarjan pseudokoodi on myös täynnä gotoja, mitä olin aiemmin ihmetellyt, mutta ilmeisesti se on Knuthille intuitiivisin tapa esittää algoritmeja. Vuoden 1974 kirjoitus Structured Programming with go to Statements (pdf) on historiallisesti mielenkiintoinen. Silmukoista ja gotoista on ilmeisesti väitelty 60-luvulta asti.

Olen alkanut käyttämään gotoa tietynlaisissa silmukoissa, koska se on mielestäni joskus selkeämpi kuin vaihtoehdot. Tulostetaan kaksiulotteisen taulukon rivit, joiden kaikki arvot ovat parillisia.

Goton avulla:

for (int y = 0; y < N; ++y) {
  for (int x = 0; x < N; ++x)
    if (value[y][x] % 2 != 0)
      goto skip;
  std::cout << y << '\n';
  skip:;
}

Ilman gotoa:

for (int y = 0; y < N; ++y) {
  bool all_even = true;
  for (int x = 0; x < N; ++x) {
    if (value[y][x] % 2 != 0) {
      all_even = false;
      break;
    }
  }
  if (all_even) {
    std::cout << y << '\n';
  }
}

Ilman gotoa tarvitaan ylimääräinen muuttuja. Sisemmän silmukan siirtäminen erilliseen funktioon mahdollistaisi goton korvaamisen returnilla, mutta minusta se olisi aika teennäistä "hyvien sääntöjen" kirjaimellista noudattamista.

fergusq [22.11.2015 09:27:49]

#

jlaire kirjoitti:

– –

Juuri esimerkkisi vuoksi esimerkiksi Javassa on niin sanottu break-goto, joka sopii tuonlaisiin tilanteisiin.

for (int y = 0; y < N; ++y) {
	skip:
	{
		for (int x = 0; x < N; ++x)
			if (value[y][x] % 2 != 0)
				break skip;
		System.out.println(y);
	}
}

tai

skip: for (int y = 0; y < N; ++y) {
	for (int x = 0; x < N; ++x)
		if (value[y][x] % 2 != 0)
			continue skip;
	System.out.println(y);
}

Nämä breakin ja continuen yleistykset ovat selkeitä vaihtoehtoja C:n raa'alle gotolle. Itse kannatan niiden lisäämistä C-standardiin, koska se helpottaisi suuresti gotottoman koodin kirjoittamista.

jlaire [22.11.2015 09:54:16]

#

Tuollainen continue <label>; on ihan kätevä. Jos tätä esimerkkiä kuitenkin laajentaa niin, että ulompi silmukka jatkuu vielä skip:n jälkeen, continue ei enää auta.

Lisäys: Ylimääräisen blokin lisääminen vain siksi, että voi sanoa "break <label>" eikä "goto <label>", on minusta juuri sellaista uskonnollista goton välttelyä, mikä ei selkeytä koodia ollenkaan.

fergusq [22.11.2015 10:18:08]

#

jlaire kirjoitti:

Ylimääräisen blokin lisääminen vain siksi, että voi sanoa "break <label>" eikä "goto <label>", on minusta juuri sellaista uskonnollista goton välttelyä, mikä ei selkeytä koodia ollenkaan.

No joo, olet varmaan oikeassa. Break <label> on periaatteessa sama asia kuin goto, mutta "turvallinen", sillä muuttujamäärittelyjen yli tai ohjausrakenteiden sisään ei voi hypätä.

groovyb [22.11.2015 13:58:01]

#

Saatika että harvoissa moderneissa kielissä pitää monimutkaisilla loopeilla tehdä ko. asioita. esimerkiksi:

C#

int[] lukuarray = {4,8,7,5,2,5,1,9};
lukuarray.Where(x => x % 2 == 0).ToList().ForEach(print);

//....
void print(int tulos)
{
   Console.WriteLine(tulos.ToString());
}

Ruby

lukuarray = [4,8,7,5,2,5,1,9]
p lukuarray.select {|x| x % 2 == 0}

Mitä tulee GOTO:n käyttämiseen, en keksi juuri muuta käyttöä kuin switch case:sta toiseen toiseen siirtymisen.

switch (a)
{
    case 1:
        DoSomething();
        goto case 2;
    case 2:
        DoSomethingElse();
        break;
    default:
        DoSomethingDefault();
        break;
}

jlaire [22.11.2015 15:35:12]

#

C#- ja Ruby-esimerkkisi ratkaisevat jonkin muun kuin annetun ongelman, mutta ainakin ne ovat tyylikkäitä ja funktionaalisia.

En olekaan aiemmin törmännyt tuollaiseen gotoon, aika hauska idea. :D Harmi ettei ole validia syntaxia C++:ssa.

Imperatiivinen ohjelmointi on kieltämättä tosi viime vuostuhatta, sisältäähän moderni C++ myös lambdat ja useita käteviä apufunktioita (tässä kävisi hyvin std::all_of).

groovyb [22.11.2015 16:15:40]

#

kappas, en huomannutkaan että kyseessä oli kaksiulotteinen taulukko, tuijotin vain jakojäännöstä :)

jalski [22.11.2015 16:43:09]

#

groovyb kirjoitti:

Mitä tulee GOTO:n käyttämiseen, en keksi juuri muuta käyttöä kuin switch case:sta toiseen toiseen siirtymisen.

switch (a)
{
    case 1:
        DoSomething();
        goto case 2;
    case 2:
        DoSomethingElse();
        break;
    default:
        DoSomethingDefault();
        break;
}

Esimerkki tosin on kohtuullisen huono, eikös tuossa goto ole tarpeeton? Ykkös tapauksesta pudotaan kuitenkin suoraan kakkos tapaukseen ilman break komentoa.

groovyb [22.11.2015 22:31:31]

#

no varmaan pointin tuosta ymmärtää jokatapauksessa, tai vaihtoehtoisesti kannattaa lopettaa ohjelmoiminen jos ei ymmärrä.

Grez [22.11.2015 22:39:36]

#

jalski kirjoitti:

Esimerkki tosin on kohtuullisen huono, eikös tuossa goto ole tarpeeton? Ykkös tapauksesta pudotaan kuitenkin suoraan kakkos tapaukseen ilman break komentoa.

Ei ole, koska se oli C# koodia. Jos tuota goto case 2; riviä ei ole, niin koodi ei käänny, vaan tulee virhe:
Error 23 Control cannot fall through from one case label ('case 1:') to another

Tässä on ilmeisesti ideana että ei tule huolimattomuusvirheitä että vahingossa unohtaa laittaa break;. Täytyy laittaa joko break; tai jokin muu case:n päättävä ennen seuraava casea. Identtisiä caseja voi kylläkin olla useampi putkeen, tyyliin:

case 1:
case 2:
  //teejotain
  break;

groovyb [22.11.2015 22:46:44]

#

Juuri noin kuten Grez sanoi, ehkä pidin tuota syntaksikysymystä enemmänkin itsestään selvänä, koska C# pääsääntöisesti tulee väännettyä. Olisin tuosta ehkä voinut selvennyksen pistää mukaan.

Yucca [23.11.2015 19:40:01]

#

Vastaan alkuperäiseen kysymykseen, vaikka keskustelu onkin kulkenut vähän muita polkuja.

”Osaisiko joku selittää, miksei tätä [GOTO-]rakennetta enää käytetä, vaan miksi se on korvattu silmukoilla?”

Kyllähän sitä käytetään, mutta suhteellisesti vähemmän kuin ennen. Osittain tämä johtuu siitä, että ohjelmointikieliin toteutettiin rakenteisia lauseita, joilla voidaan tehdä silmukoita helposti. Vanha FORTRAN ei sellaisia sisältänyt, paitsi ominaisuuksiltaan aika rajallisen DO-lauseen.

Toinen syy on asenneilmaston muutos. Muutoksen kai katsotaan alkaneen Dijsktran kuuluisasta kirjoituksesta ”Go To Statement Considered Harmful” (vuonna 1968), jonka perusajatus on, että GOTO-lauseet tekevät ohjelman loogisesta rakenteesta paljon vaikeammin hahmotettavan. Tämä on laajasti omaksuttu, useinkin opittuna ideana, jota ei kyseenalaisteta. (Monikohan idean nykyisistä kannattajista on oikeasti kirjoittanut tai lukenut GOTO-koodia?) Pääosin ajatus onkin oikea, mutta ainakin jossain vaiheessa siihen suhtauduttiin lähes uskonnollisen fanaattisesti.

”Löysin Fortran-kielen Wikipedia-artikkelista tiedon, että GOTO-rakennetta olisi hankalampi optimoida kuin silmukoita, mutta miksi näin on? Eikö se ole käytännössä sama asia?”

Tässä on ensinnäkin käsitesekaannus. ”Silmukka” on looginen käsite, ja silmukka voidaan mainiosti toteuttaa GOTO-lauseen avulla. Kuten mainitsin, vanhassa FORTRAN-kielessä se oli jopa ainoa tapa tehdä silmukka, ellei kyseessä ole hyvin yksinkertainen iteraatio, jossa jokin kokonaislukumuuttuja vain käy läpi arvot tietystä alkuarvosta tiettyyn loppuarvoon.

GOTO-rakenne ei sinänsä ole mitenkään hankala toteuttaa. GOTO-lause on suunnilleen yksinkertaisin ajateltavissa oleva lause: se kääntyy normaalisti yhdeksi konekäskyksi. ”Optimointi” voi tarkoittaa eri asioita, mutta tavallisesti se tässä yhteydessä tarkoittaa silmukoiden ”optimointia” esimerkiksi niin, että kääntäjä siirtää silmukan sisältä sen ulkopuolelle kaiken mahdollisen (niin että esimerkiksi samaa vakiolauseketta ei lasketa uudelleen joka kierroksella), mutta myös paljon muuta – ennen muinoin esimerkiksi usein käytettyjen muuttujien pitämistä rekistereissä, autoincrement-käskyjen käyttöä, laskennan toteuttamista rinnakkaisesti esimerkiksi vektoriprosessorissa jne. – tekniikat vaihtelevat käskykannan, kokonaisarkkitehtuurin yms. mukaan.

Olennaista on, että kääntäjä pystyy tunnistamaan silmukan ja että silmukka on luonteeltaan optimoitavissa, ei niinkään se, millä ohjelmointikielen lauseilla silmukka on toteutettu. Tietenkin esimerkiksi while-, repeat-, for each- tms. lauseella tehty silmukka on ikään kuin luonnostaan tunnistettavissa, mutta ei jonkin IF- ja GOTO-lauseilla tehdyn silmukan (jos se on rakenteeltaan yksinkertainen silmukka) tunnistaminen ole rakettitiedettä, ainakaan verrattuna kääntäjän tekemiseen ja koodin optimointiin yleisesti.

jalski [23.11.2015 20:19:03]

#

jlaire kirjoitti:

C#- ja Ruby-esimerkkisi ratkaisevat jonkin muun kuin annetun ongelman, mutta ainakin ne ovat tyylikkäitä ja funktionaalisia.

Imperatiivinen ohjelmointi on kieltämättä tosi viime vuostuhatta, sisältäähän moderni C++ myös lambdat ja useita käteviä apufunktioita (tässä kävisi hyvin std::all_of).

Viime vuosituhat toimii ihan hyvin:

dcl lukuarray(2,4) fixed bin(31) init( 2, 4, 6, 8,
                                       2, 5, 1, 9 );

dcl (i, j) fixed bin;

do i = lbound(lukuarray,1) to hbound(lukuarray,1);
  if ^any(bit(lukuarray(i,*))) & (30)'0'b||'1'b then
    put skip edit((lukuarray(i,j) do j = lbound(lukuarray,2) to hbound(lukuarray,2))) (f(5), X(2));
end;

Erona alkuperäiseen C ongelmaan, tulostaa koko rivin sisällön pelkän rivinumeron sijaan.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta