Wątek przeniesiony 2023-03-02 11:00 z Off-Topic przez Riddle.

Instrukcja goto

1

Przykłady były już podawane wielokrotnie — goto może zastąpić jednorazowe funkcje, może zastąpić pętle, wykluczyć konieczność mnożenia zmiennych loklanych, zmniejszyć ilość kodu do pisania i trzymać daną logikę w jednym miejscu, co zdecydowanie ułatwia nie tylko analizę, ale i debugowanie (w końcu wszystko jest w jednym miejscu, w obrębie wzroku).

No może zastąpić to wszystko, prawda. Ale w takim wypadku po co Ci w ogóle te wszystkie konstrukcje? Możesz przecież je zastąpić goto i mieć "wszystko w jednym miejscu". To czemu używać Pascala czy C, skoro z paroma makrami masz to samo w asemblerze? I "zdecydowanie ułatwia nie tylko analizę, ale i debugowanie" to raczej mocno subiektywna opinia, a do tego na moje niezbyt zauważalna empirycznie. W mikro skali może i ma to jakiś sens, ale wtedy i tak łatwiej IMHO to zapisać używając po prostu pętli czy funkcji. Bo jeśli mamy kod w stylu:

int a = 1;

if (foo) goto step1;
a += 2;

step1:
if (bar) goto step2;
a += 3;

step2:
a += 4;

To jakoś lepiej dla mnie wygląda mimo wszystko:

int a = 1 + maybe(foo, 2) + maybe(bar, 3) + 4;

Podobnie z pętlami jak masz:

loop:

// …

if (cond) goto loop;

To nie widzę zalety nad:

do {
  // …
} while (cond);

Masz na myśli ten przykład?

Nie znam Pascala zbytnio, ale przecież nie musisz robić z tego metody klasy. Dodatkowo dzieje się tam na moje strasznie dużo i ciężko jest mi wywnioskować skąd się bierze część zmiennych, więc mam nawet problem by to przetłumaczyć na coś co znam. Dodatkowo początkowo myślałem, że rozwiązanie, które dawał @Riddle jest błędne, bo po wcięciach i zachowaniu goto miałem wrażenie, że będzie tam pętla testująca do oporu. I dalej jakoś mam wrażenie, że przenosząc to do osobnej funkcji byłoby to zdecydowanie łatwiej testować, bo nie musisz znać zachowania FontsEditor.Dialogs.Message.Execute by móc przetestować samą walidację, dzięki czemu możesz zdecydowanie łatwiej testować mniejszą część kodu bez potrzeby symulacji GUI. A jeśli nie chcesz by ta procedura była publiczna, to można to ogarnąć poprzez zagnieżdżoną funkcję skoro Pascal to obsługuje. Więc jak najbardziej nie widzę sensu na goto.

Co do tego przykładu to mam pomysł, ale niestety nie mam teraz czasu by go zawodzić. Idea jest taka, że zamiast robić wszystko w jednej funkcji to rozbijasz na osobne, które budują Ci dialog i zwracają jaka akcja ma się wykonać, następnie sama akcja się wykonuje już niezależnie od samych dialogów. W ten sposób można to sobie zrobić nawet bardziej elastyczne, bo możesz dodawać nowe akcje bez rozbudowywania drabinki ifów coraz bardziej.

1
hauleth napisał(a):

Możesz przecież je zastąpić goto i mieć "wszystko w jednym miejscu". To czemu używać Pascala czy C, skoro z paroma makrami masz to samo w asemblerze?

Fajny sofizmat rozszerzenia.

I "zdecydowanie ułatwia nie tylko analizę, ale i debugowanie" to raczej mocno subiektywna opinia, a do tego na moje niezbyt zauważalna empirycznie.

Jeśli dla Ciebie skakanie po funkcjach jest łatwiejsze do debugowania niż skakanie po linijkach w obrębie ciała jednej funkcji no to faktycznie jest to kwestia subiektywna. Bo dla mnie im mniej wywołań tym mniej rzeczy do śledzenia i pamiętania, więc jeśli mam do wyboru w danym przypadku skorzystanie z goto lub wydzielenia kilku linijek do kilku funkcji (nieważne czy zagnieżdżonych czy nie), to biorę goto.

Masz na myśli ten przykład?

Nie, mam na myśli każdy przypadek, w którym za pomocą goto wyklucza się konieczność bezsensownego wydzielania kodu do funkcji. Bo jak pisałem wcześniej, dla mnie wygodniejsze jest nie mieć masy kilkulinijkowych funkcji, skoro wspólny kod mogę wsadzić na spód funkcji i sobie do niego skoczyć za pomocą goto.

Ot po prostu czasem proste goto robi robotę i nie wymaga mnożenia funkcji.

1

Z mojego doświadczenia w C goto używane do dowolnego skoku w funkcji zaciemnia kod. Wyjątkiem są ‚płaskie’ skoki wewnątrz funkcji, tzn. na tym samym poziomie struktury, np. skoki na koniec do obsługi błędów, oraz wyskok z wielu pętli tuż za te pętle, ponieważ nie można do tego celu użyć break.
Użyłem kiedyś tylko jeden raz goto do skoku wstecz w kodzie i za każdym razem od nowa domyślam się, o co mi chodziło, ale tego teraz nie da się zastąpić czymś innym.

Natomiast ciekawy i wymagający rozwinięcia jest argument: dlaczego używamy języków wyższego poziomu, w których nie ma bezpośredniego odwzorowania skoków asemblerowych. Każdy z takich języków ma określone konstrukcje składniowe, które pozwalają, wydaje się, zrobić jedno zadanie bez mieszania kodu z innymi czynnościami, a kompilator albo maszyna wirtualna dopiero to optymalizuje do kodu wykonywanego jednocześnie lub równolegle. Dlatego mamy na przykład pętlę, która iteruje po wszystkich elementach, a przychodzi instrukcja break, która to wyliczanie przerywa w sposób niskopoziomowy. Można wyliczać tylko z wybranego zbioru elementów, tak by były tylko te elementy, które są potrzebne. I w tym jest właśnie konflikt pomiędzy podejściem z goto, które przerywa niskopoziomowo, a konstrukcjami języka wysokiego poziomu. Kwestia, czy optymalne jest podejście wysokopoziomowe? Na pewno wierniej składniowo wyraża zadanie.

2
furious programming napisał(a):

Ot po prostu czasem proste goto robi robotę i nie wymaga mnożenia funkcji.

Po co Ci w ogóle funkcje, skoro wszystko można ogarnąć goto?

0
hauleth napisał(a):

C jest ułomne

cease-your-heresy-warhammer40k.gif

1
overcq napisał(a):

Wyjątkiem są ‚płaskie’ skoki wewnątrz funkcji, tzn. na tym samym poziomie struktury, np. skoki na koniec do obsługi błędów, oraz wyskok z wielu pętli tuż za te pętle, ponieważ nie można do tego celu użyć break.

Nigdzie nie twierdziłem, że goto używam do każdego skoku — podobnie jak Ty, używam go w sytuacjach wyjątkowych, w których ma to sens (bo skraca kod, nie wymaga jego podziału na mniejsze funkcje). I te dwa przykłady które podałeś, są właśnie takimi sytuacjami, w których goto jest sensownym rozwiązaniem.

Użyłem kiedyś tylko jeden raz goto do skoku wstecz w kodzie i za każdym razem od nowa domyślam się, o co mi chodziło, ale tego teraz nie da się zastąpić czymś innym.

Mhm. Czyli płaski skok w dół np. do cleanupu jest dla Ciebie czytelny i zrozumiały, ale taki sam płaski skok, tyle że do góry, już zrozumiały nie jest i nie potrafisz zrozumieć przepływu sterowania. Trudno mi w to uwierzyć, szczególnie, że mogę to ekstrapolować do continute i break. Mam uwierzyć, że rozumiesz pętlę w której jest break (wyskok z pętli), ale gubisz się gdy widzisz continue?

Poza tym nie ma czegoś takiego jak „tego teraz nie da się zastąpić czymś innym” — nie chcesz używać goto do skoku w górę, to użyj pętli. Albo podziel sobie kod na mniejsze funkcje, tak jak poprzednicy sugerowali. Rozwiązań jest trochę.

Natomiast ciekawy i wymagający rozwinięcia jest argument: dlaczego używamy języków wyższego poziomu, w których nie ma bezpośredniego odwzorowania skoków asemblerowych.

Ależ są — tym właśnie są break i continue, skokami bezwarunkowymi w konkretne miejsce kodu. Tak samo jak bezpośrednim odwzorowaniem zakończenia wykonywania kodu funkcji jest return (albo pascalowy exit).


Riddle napisał(a):
furious programming napisał(a):

Ot po prostu czasem proste goto robi robotę i nie wymaga mnożenia funkcji.

Po co Ci w ogóle funkcje, skoro wszystko można ogarnąć goto?

Ty też nie zdajesz sobie sprawy z tego, że takimi pytaniami gwałcisz logikę? Takich pytań się nie zadaje, bo są bezsensowne — goto służy do regulowania przepływu sterowania, a funkcje do wydzielania fragmentów kodu wielokrotnego użytku, operującego na dowolnych danych dostarczonych w parametrach.

0

@Riddle

W momencie w którym zawołasz goto to kontrola przepływu skacze do innego miejsca, nie zostaje nic zwrócone z funkcji, zmienna do której jest przypisany jej wynik pozostaje w takim samym stanie jak przed jej wywołaniem, dalszy flow idzie z innego miejsca.

No to w takim razie przykład (zakładając, że C ma międzyproceduralne skoki).

extern goto x; /* Tak mogłaby wyglądać składniowo deklaracja etykiety. */

int a () {
        goto x;
        }

void b () {
        x:
                return;
        }

void c () {
        int n;
        n = a (); /* I co teraz - jaką wartość uzyska n? */
        }

Po co Ci w ogóle funkcje, skoro wszystko można ogarnąć goto?

Jezu... Wywołanie procedury zachowuje adres powrtotu na stosie, dzięki czemu można wywoływać procedurę z różnych miejsc programu, i zawsze wracać do odpowiedniego miejsca. Skok to po prostu skok, nie zachowuje żadnego adresu powrotu, używany, gdy nie ma takiej potrzeby.

@hauleth

Podobnie z pętlami jak masz:

loop:

// …

if (cond) goto loop;

To nie widzę zalety nad:

do {
  // …
} while (cond);

Tak prosty przypadek jest "pokryty" instrukcją while, której jak najbardziej należy tu użyć, ale istnieją bardziej złożone przepływy sterowania, dla których zostaje goto.

1

Jeśli dla Ciebie skakanie po funkcjach jest łatwiejsze do debugowania niż skakanie po linijkach w obrębie ciała jednej funkcji no to faktycznie jest to kwestia subiektywna. Bo dla mnie im mniej wywołań tym mniej rzeczy do śledzenia i pamiętania, więc jeśli mam do wyboru w danym przypadku skorzystanie z goto lub wydzielenia kilku linijek do kilku funkcji (nieważne czy zagnieżdżonych czy nie), to biorę goto.

Nie chciałem się udzielać w tym wątku, ale klauzula sumienia nie pozwoliła mi być cicho, gdy zobaczyłem powyższe.

To jest totalne pomieszanie pojęć.

Funkcja jest wydzielonym blokiem kodu, który ogarnia jakieś określone zadanie. Na jej początku możesz np. ustawić wartość jakichś zmiennych, na końcu coś zwrócić czy posprzątać.

Z kolei jakiś kilkudziesiecio czy kilkuset liniowy twór/blok, w środek którego się wstrzeliwujesz, w żaden sposób nie zapewnia tego, co daje funkcja. Wbijasz do jakiejś linii i musisz pamiętać, żeby sobie wszystko przygotować - bo przecież mamy jeden globalny blok kodu, a co za tym idzie - wszystkie zmienne mają globalny zakres i czort wie, kto i kiedy grzebał w ich wartościach. Musisz pamiętać, żeby w określonym momencie przy użyciu kolejnego goto wrócić... Tylko w sumie to gdzie? Funkcja po zakończeniu wraca do miejsca, z którego była wywołana. A tutaj jest gorzej.

Dla mnie (tak, jak piszesz - kwestia subiektywna) skoki po funkcjach są logiczne i czytelne, a zabawa w konika polnego, który sobie skacze po liniach to jeden z objawów spaghetti.

1
cerrato napisał(a):

To jest totalne pomieszanie pojęć.

Ale kto miesza te pojęcia? Mnie nie musisz tłumaczyć czym są funkcje i jak się ich używa, ani też czym są skoki (bez)warunkowe i do czego służą. Poza tym ja nie teoretyzuję, a opieram swoje zdanie na istniejącym kodzie (swoim w Pascalu i cudzym w C), z którym mam do czynienia codziennie, od bardzo dawna.

Z kolei jakiś kilkudziesiecio czy kilkuset liniowy twór/blok, w środek którego się wstrzeliwujesz, w żaden sposób nie zapewnia tego, co daje funkcja.

Tak by było, gdybym do wnętrza funkcj wskakiwał nie wiadomo skąd — np. z innego modułu, z innej funkcji.

Wbijasz do jakiejś linii i musisz pamiętać, żeby sobie wszystko przygotować - bo przecież mamy jeden globalny blok kodu, a co za tym idzie - wszystkie zmienne mają globalny zakres i czort wie, kto i kiedy grzebał w ich wartościach.

Wbijam do jakiejś linii po inicjalizacji zmiennych lokalnych, więc zawsze z góry wiadomo jakie dane w tych zmiennych siedzą. Natomiast skok do wczejszego fragmentu nie powoduje w magiczny sposób wykoślawienia tych danych, więc również zawsze wiadomo na jakich danych powtórka będzie operowała. Jest to dokładnie ten sam schemat co w przypadku dowolnych pętli, które operują na jakichś zmiennych nie będących iteratorami, tyle że zamiast ciała pętli mam prosty goto do konkretnej etykiety.

Dla mnie (tak, jak piszesz - kwestia subiektywna) skoki po funkcjach są logiczne i czytelne, a zabawa w konika polnego, który sobie skacze po liniach to jeden z objawów spaghetti.

Nie skacze sobie po liniach jak konik polny, a realizuje konkretne założenia algorytmiczne. Spaghetti można napisać w dowolnym języku, z goto i bez. Zdumiewające jest to, że krótkie lokalne skoki w obrębie kilkudziesięciu linii są postrzegane jako spaghetti, a takie samo skakanie, tyle że po dziesiątkach jednorazowych mikro-funkcji jest uznawane za czytelną, dobrą praktykę.

1

O small-functions anti-pattern gadaliśmy już tu

Kiedy gruboziarnisty kod jest lepszy?

:D

4
furious programming napisał(a):

na istniejącym kodzie (swoim w Pascalu i cudzym w C), z którym mam do czynienia codziennie, od bardzo dawna.

Wyrazy najgłębszego współczucia, ale to nie zmienia faktu, że jeśli mamy nowoczesny język, który posiada inne mechanizmy, to goto jest zupełnie niepotrzebne i jedyne co wprowadza to zbędny chaos.

Wbijasz do jakiejś linii i musisz pamiętać, żeby sobie wszystko przygotować - bo przecież mamy jeden globalny blok kodu, a co za tym idzie - wszystkie zmienne mają globalny zakres i czort wie, kto i kiedy grzebał w ich wartościach.

Wbijam do jakiejś linii po inicjalizacji zmiennych lokalnych, więc zawsze z góry wiadomo jakie dane w tych zmiennych siedzą.

Ale przecież wartość tej zmiennej mogła się w międzyczasie zmienić, więc tak nie zawsze a jedynie domyślasz się na podstawie tego jaki przepływ myślisz, że będzie (co może lub nie być zgodne ze stanem faktycznym).

Spaghetti można napisać w dowolnym języku, z goto i bez.

Tak, ale język może to czasem utrudniać lub ułatwiać.

Zdumiewające jest to, że krótkie lokalne skoki w obrębie kilkudziesięciu linii są postrzegane jako spaghetti, a takie samo skakanie, tyle że po dziesiątkach jednorazowych mikro-funkcji jest uznawane za czytelną, dobrą praktykę.

Nie powiedziałbym, że za dobrą praktykę, ale to zależy od kontekstu i tego jak są te funkcje nazwane. Dodatkowo zaleta funkcji jest taka, że każda funkcja to osobny scope (co już zdążyliśmy tutaj zauważyć) przez co nie musisz trzymać w pamięci całego stanu, by zrozumieć jak funkcja działa, a jedynie stan jaki przekazujesz w argumentach (zakładam nieużywanie zmiennych globalnych do sterowania przepływem). Dodatkowo jak masz funkcje, to łatwiej jest testować czy dana funkcja robi to co chcesz, a w przypadku goto ciężej jest testować subkomponenty takiej funkcji, bo (również, jak sam zauważyłeś) skoki do wnętrza funkcji "z zewnątrz" to coś czego raczej unikamy.

1

Teraz w sumie pomyślałem, że może furious programming dlatego lubi goto w swoich aplikacjach, bo pisze głównie w Delphi/Pascal; a tam nie ma returna. Funkcje zwracają wynik poprzez przypisanie do Result, w stylu zmiennej. Jeśli pisalibyśmy całe życie w takim języku (bez returnów), to w sumie podejście w którym samemu się robi takie skoki może być koncepcyjnie spójne (zakładając że nigdy nie zrobimy bezsensownego jumpa, o których ostrzegał Dijkstra w swojej książce - co furious programming mówi że ich unika).

1
Riddle napisał(a):

Teraz w sumie pomyślałem, że może furious programming dlatego lubi goto w swoich aplikacjach, bo pisze głównie w Delphi/Pascal; a tam nie ma returna.

Też piszę w językach gdzie nie ma return, albo nawet jak jest to go wyłączam w linterze (multiple return to też goto). Tyle, że goto nie używam - argument troche się nie klei.
(Btw ostatnio pisałem troche w golangu - to chyba największy koszmar pod względem bajzlu returnowego - nie dość, że multiple return dopuszcza to jeszcze ma nazwane argumenty return - czyli połączenie tego z pascala z tym z javy) -> ale taki design języka sprawia, że debugger się faktycznie przydaje. (Twórcy płatnych IDE lubią to).

0
jarekr000000 napisał(a):
Riddle napisał(a):

Teraz w sumie pomyślałem, że może furious programming dlatego lubi goto w swoich aplikacjach, bo pisze głównie w Delphi/Pascal; a tam nie ma returna.

Też piszę w językach gdzie nie ma return

W jakich więc językach piszesz?

1
Riddle napisał(a):

W jakich więc językach piszesz?

Haskell
(w kodzie haskella często zobaczysz return, ale to normalna funkcja, nie jest to żadna instrukcja/słowo kluczowe, taki myk do monad)
Scala, Kotlin - niby jest return - taki jak w javie, ale można go zabronić linterem :-) i zrobić tak, żeby ostatnie wyrażenie w funkcji (a najlepiej jedyne) było zwracaną wartością

(pomijam inne niszowce - Nix, Dhall)

0
jarekr000000 napisał(a):
Riddle napisał(a):

Teraz w sumie pomyślałem, że może furious programming dlatego lubi goto w swoich aplikacjach, bo pisze głównie w Delphi/Pascal; a tam nie ma returna.

Też piszę w językach gdzie nie ma return

jarekr000000 napisał(a):
Riddle napisał(a):

W jakich więc językach piszesz?

Haskell
(w kodzie haskella często zobaczysz return, ale to normalna funkcja, nie jest to żadna instrukcja/słowo kluczowe, taki myk do monad)
Scala, Kotlin - niby jest return - taki jak w javie, ale można go zabronić linterem :-) i zrobić tak, żeby ostatnie wyrażenie w funkcji (a najlepiej jedyne) było zwracaną wartością

(pomijam inne niszowce - Nix, Dhall)

slsy napisał(a):

W FP nie miasz returnów

Widzę że się nie wyraziłem odpowiednio.

Oczywiście że nie chodzi mi o wystąpienie słowa return, a ni o to czy funkcje zwracają wiele wartości czy jedną. Całkowicie umknął wam sens tego co chciałem napisać.

Zacytuję mój post:

Riddle napisał(a):

...bo pisze głównie w Delphi/Pascal; a tam nie ma returna. Funkcje zwracają wynik poprzez przypisanie do Result, w stylu zmiennej.

Sensem wypowiedzi jest Result, w stylu zmiennej. Może to moja wina, że nie nacechowałem poprawnie tego zdania, ale to jest to na co chciałem zwrócić uwagę. Nie chodzi o brak returna samego w sobie, tylko istnienie tego podejścia z Result. W Pascalu zwrócenie wartości z funkcji wykonuje się poprzez przypisanie wartości do Result która zachowuje się jak zmienna - w językach FP nie ma takiego mechanizmu.

1

Sens mojej wypowiedzi był taki, że w wielu jezykach (java, C#) zamiast GOTO używa się multiple return. To dużo mniejsze bagno, ale nadal bagno.

Dodatkowo dochodzą specyficzne zboczenia czyli używanie wyjątków, do sterowania flow - to też bagno (choć istotnie mniejsze od goto).

0

@jarekr000000:

(w kodzie haskella często zobaczysz return, ale to normalna funkcja, nie jest to żadna instrukcja/słowo kluczowe, taki myk do monad)

a co robi?

0
WeiXiao napisał(a):

@jarekr000000:

(w kodzie haskella często zobaczysz return, ale to normalna funkcja, nie jest to żadna instrukcja/słowo kluczowe, taki myk do monad)

a co robi?

To nie jest instrukcja więc NIC nie robi. Zmienia typ wyrażenia.

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