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

Instrukcja goto

0
hauleth napisał(a):

Ale pewnie będziesz miał problem z tym, że nie wywołuję tutaj funkcji "z palca" bo przecież te funkcje mogą robić "dowolne rzeczy" jak opisałeś (mimo iż ich nazwy oczywiście wskazują na czyszczenie po poprzednich wywołaniach).

Nie będę miał z tym problemu — jedyne o co poprosiłem to o wzięcie każdego z przykładów z artykułu (jest ich sześć) i przeportowanie ich na inne języki, tak aby te porty działały zgodnie z oryginałem, ale można używać dowolnych ficzerów języków.

Tak więc jeśli ww. funkcje do_something, init_stuff i prepare_stuff są idiomatycznie napisane i zwracają sensowne typy (jak ww. MutexGuard) to mamy wszystko ładnie zwolnione/wyczyszczone/cofnięte/whatever bez ingerencji programisty.

A jeśli zwracają coś innego?

BTW: funkcje do_something, init_stuff i prepare_stuff zwracają wartość logiczną, ew. kod błędu, który musi być zerowy, aby wykonywanie dalszych instrukcji było możliwe. Jeśli zwrócą wartość niezerową, to należy posprzątać i wyjść z funkcji. Chodzi tutaj wyłącznie o wykrycie problemu i o odpowiednią reakcję na niego — nieważne czy zwracają coś, czy walą wyjątkiem.

Żadne goto czy inny mechanizm nie jest tutaj potrzebny, bo do_something(u32) -> Result<Guard, Error> ładnie nam się tym zajmie […]

Istotą mojej propozycji było podanie kodu, który działaniem będzie zgodny z przykładami z artykułu. Jeśli guard się zajmie, to pasuje pokazać jak się zajmie.

[…] bez konieczności naszej pracy. Jeśli chcesz "manualnie" wywołać czyszczenie to można napisać własny GenGuard, który to zrobi.

Pisanie własnych guardów to jednak jest praca, którą trzeba wykonać samodzielnie. ;)

0

W C w praktyce często widziałem mix rozwiazania z artykułu i 1 bloku skoku.

int* foo(int bar)
{
    int* return_value = NULL;

    bool flag_1 = false;
    bool flag_2 = false;
    bool flag_3 = false;

    flag_1 = do_something(bar);
    if (!flag_1) {
        goto out;
    }
    
    flag_2 = init_stuff(bar);
    if (!flag_2) {
        goto out;
    }

    flag_3 = prepare_stuff(bar);
    if (!flag_3) {
        goto out;
    }

    return_value = do_the_thing(bar);

out:
    if (flag_3) clean_stuff();
    if (flag_2) destroy_stuff();
    if (flag_1) undo_something();

    return return_value;
}
2
furious programming napisał(a):

Tak więc jeśli ww. funkcje do_something, init_stuff i prepare_stuff są idiomatycznie napisane i zwracają sensowne typy (jak ww. MutexGuard) to mamy wszystko ładnie zwolnione/wyczyszczone/cofnięte/whatever bez ingerencji programisty.

A jeśli zwracają coś innego?

BTW: funkcje do_something, init_stuff i prepare_stuff zwracają wartość logiczną, ew. kod błędu, który musi być zerowy, aby wykonywanie dalszych instrukcji było możliwe. Jeśli zwrócą wartość niezerową, to należy posprzątać i wyjść z funkcji.

Napisałem o idiomatycznym Ruscie. W Ruscie masz typ Result<S, E>, który zwraca Ci wartość lub błąd. Czyli dokładnie to co chcesz, ale zamiast bawić się w goto używasz RAII, które sprząta za ciebie w przypadku sukcesu.

A jeśli zwracają coś innego?

Jeśli chcesz się uprzeć przy nienaturalnym zwracaniu boola z funkcji, bo "tak jest w C" to wtedy można zrobić to tak:

#[derive(Default)]
struct GenGuard<F: FnOnce()>(Option<F>);

impl<F: FnOnce()> Drop for GenGuard<F> {
    fn drop(&mut self) {
        if let Some(func) = self.0.take() {
            func()   
        }
    }
}

fn defer<F: FnOnce()>(func: F) -> GenGuard<F> { GenGuard(func) }

Playground

To przy okazji pokazuje jak wielkim problemem jest napisanie generycznego guarda o czym piszesz w

Pisanie własnych guardów to jednak jest praca, którą trzeba wykonać samodzielnie. ;)

Ww. guard zadziała dla dowolnej funkcji na podobnej zasadzie co defer w Go czy Zigu, a nie wymaga on żadnego goto i jest IMHO zdecydowanie naturalniejszy dla programisty, bo całość inicjalizacji i "ubicia" dzieje się w jednym miejscu a nie jest porozrzucane po funkcji. Więcej, możemy nawet zaimplementować metodę na Guardzie, która pozwoli nam go "wyłączyć":

impl<F: FnOnce()> GenGuard<F> {
    fn cancel(self) { self.0.take() }
}

I to wszystko. Nasza funkcja już się nie wykona. Co więcej, nie da się wywołać przypadkowo guard.cancel() dwa razy, bo kompilator nam na to nie pozwoli.

Wtedy piszemy to trochę mniej naturalnie, bo tak:

fn foo(bar: i32) -> Option<i32> {
    let _guard1 = if do_something(bar) {
      defer(undo_something)
    } else { return None }

  // …
}

Tylko nikt o zdrowych zmysłach tak tego nie pisze, bo to nie ma sensu. Mamy konstrukty językowe, które służą do tego by to można było zapisać łatwiej i z punktu użytkownika i z punktu projektanta API. Zwłaszcza, że skoro do_something nie zwraca żadnej wartości (zgodnie z oryginalnym kodem) oraz nie jest oznaczone jako unsafe (zakładam, że nie używa unsafe wewnątrz by modyfikować zmienne statyczne) to znaczy, że zwraca wartość, którą trzeba gdzieś przypisać. Nie przypisuje jej nigdzie, więc ta wartość nie jest widać istotna i może zostać zniszczona od razu.

Więc mamy rozwiązanie nie tylko bardziej naturalne, prostsze w ogarnięciu (bo nie trzeba skakać na początek i koniec funkcji by rzeczy ogarnąć), a do tego bardziej elastyczne, bo pozwala nam "dynamicznie" anulować wywołanie. Technicznie mamy też np. możliwość zrobienia tak, by dodawać więcej funkcji do naszego guarda w stylu guard.before(do_other_stuff) lub guard.after(do_another_stuff). goto nie dość, że jest mniej elastyczne, to do tego wymaga zdecydowanie więcej uwagi od programisty.


Co do wychodzenia z zagnieżdżonych pętli, to już podawałem przykład wcześniej, ale zgodnie z życzeniem przepiszę to jeszcze raz używając Twojego przykładu:

'outer: for i in 1..5 {
    println!("outer iteration (i): {i}");

    for j in 1..5 {
        println!("inner iteration (j): {j}");

        if j >= 3 { break; }
        if i >= 2 { break 'outer; }
    }
}

Zwykłe nazwane pętle, bez dodawania nowej konstrukcji do języka.


Istotą mojej propozycji było podanie kodu, który działaniem będzie zgodny z przykładami z artykułu.

No i tutaj trochę leży pies pogrzebany, bo tak się składa, że w Ruscie czy innych językach te przykłady często będą napisane zupełnie inaczej. Przykład z maszyną stanów:

enum StateMachine {
  A,
  B,
  C,
}

impl StateMachine {
    fn step(self, input: u8) -> Result<Self, ()> {
        use StateMachine::*; // By nie powtarzać się z `StateMachine::` cały czas
        
        match (self, input) {
            (A, b'x') => Ok(Some(B)),
            (A, b'y') => Ok(Some(C)),
            (A, b'z') => Ok(Some(A)),
            
            (B, b'x') => Ok(Some(B)),
            (B, b'y') => Ok(Some(A)),
            (B, 0) => Ok(None),
            
            (C, b'x') => Ok(Some(C)),
            (C, b'z') => Ok(Some(B)),

            _ => Err(())
        }
    }
}

fn match(data: &[u8]) -> Result<(), ()> {
    let mut state = StateMachine::A;
    
    for &c for data {
        match state.step(c)? {
          Some(next_state) => state = next_state,
          None => return Ok(())
        }
    }

    Err(())
}

Jak widać zamiast zestawu switch i goto można mieć pattern matching od razu na całej strukturze, co IMHO jest zdecydowanie bardziej czytelne niż czytanie każdego przejścia osobno. Zwłaszcza, że często dostaniemy to nie jako diagram, a raczej jako tabelę w postaci:

Stan wejście Nowy Stan
A x B
A y C
A z A
B x B
B y A
B 0 End
C x C
C z B

I w takiej formie często to jest łatwiej przeczytać jeśli zapiszemy to jak wyżej.

0
hauleth napisał(a):

Napisałem o idiomatycznym Ruscie. W Ruscie masz typ Result<S, E>, który zwraca Ci wartość lub błąd. Czyli dokładnie to co chcesz, ale zamiast bawić się w goto używasz RAII, które sprząta za ciebie w przypadku sukcesu.

Jeśli Twój przykład działa tak samo co ten z C, to super. O to właśnie chodziło, aby pokazać jak się takie problemy rozwiązuje w innych językach, uwzględniając ich możliwości.

Jeśli chcesz się uprzeć przy nienaturalnym zwracaniu boola z funkcji, bo "tak jest w C" to wtedy można zrobić to tak:

Nie, absolutnie. Kontrprzykłady powinny tylko działać tak samo jak te z artykułu, ale mogą wyglądać dowolnie. W końcu chodzi tutaj o pokazanie implementacji konkretnego algorytmu w innych językach, nie o imitowanie w nich języka C.

To przy okazji pokazuje jak wielkim problemem jest napisanie generycznego guarda o czym piszesz w […]

Nie pisałem, że jest to problem, a że jest to dodatek do napisania własnoręcznie, skoro raz napisałeś, że nie wymaga do naszej pracy, a potem, że trzeba guarda napisać. Chodziło mi jedynie o to, abyś podał kompletny przykład implementacji, a nie pół kodu, pół opisu słownego.

Ww. guard zadziała dla dowolnej funkcji na podobnej zasadzie co defer w Go czy Zigu, a nie wymaga on żadnego goto i jest IMHO zdecydowanie naturalniejszy dla programisty, bo całość inicjalizacji i "ubicia" dzieje się w jednym miejscu a nie jest porozrzucane po funkcji.

Zgoda, jednak pamiętaj, że moja prośba dotyczyła przykładów w popularnych językach, tych których używacie na codzień, a nie w jakimś Zigu, który nawet stabilnej wersji się jeszcze nie doczekał i o którym w ogóle mało kto słyszał.

Mamy konstrukty językowe, które służą do tego by to można było zapisać łatwiej i z punktu użytkownika i z punktu projektanta API.

I właśnie temu służy moja propozycja, czyli pokazania jak to się robi w innych językach.

Zwykłe nazwane pętle, bez dodawania nowej konstrukcji do języka.

Elegancko.

No i tutaj trochę leży pies pogrzebany, bo tak się składa, że w Ruscie czy innych językach te przykłady często będą napisane zupełnie inaczej.

To nie pies jest pogrzebany, a czytanie ze zrozumieniem. Prosiłem o port logiki tych przykładów, nie składni. ;)

2
furious programming napisał(a):

BTW: funkcje do_something, init_stuff i prepare_stuff zwracają wartość logiczną, ew. kod błędu, który musi być zerowy, aby wykonywanie dalszych instrukcji było możliwe. Jeśli zwrócą wartość niezerową, to należy posprzątać i wyjść z funkcji. Chodzi tutaj wyłącznie o wykrycie problemu i o odpowiednią reakcję na niego — nieważne czy zwracają coś, czy walą wyjątkiem.

Ale przecież w tym przykładzie dokładnie to tak nie działa i na tym trik polega, jeśli nie widziałeś tego w wersji z goto, to w wersjach alternatywnych jest to jasne.
Sprzątamy niezależnie od tego co zwróci do_something.
Co zresztą świadczy o tym, że źle ją napisano (nie wiemy jak - ale ze sposobu sprzątania to wynika). A źle ją napisano, żeby patoprzykładem uzasadnić goto.
Przez co w prawie każdym języku ten przykład będzie wyglądać nieidealnie - bo klasyczne try with resource nie zadziała.

To będzie sensowne, merytoryczne podejście do poruszanego problemu. Nie tylko rozwieje wszelkie wątpliwości, ale też pozwoli porównać różne języki i ich funkcjonalność, co będzie znacznie bardziej wartościowe niż rzucanie na lewo i prawo pustych stwierdzeń. Podejdźcie do tego jak do zwykłego FizzBuzzu, pokażcie jakimi ficzerami dysponujecie w językach, z którymi pracujecie.

To klasyczna manipulacją, (zasadniczo oryginalna strona z przykładami) w którą dałem się złapać - tworzone jest sztucznie dziwaczne środowisko, w którym zdefiniowane wcześniej funkcje są naprawdę źle popisane, po to żeby uzasadnić, że w końcowym, małym fragmencie kodu goto wyjdzie całe na biało. A przez udziwnienia wyszło nadal nieczytelnie, skoro ludzie mają problem ze zrozumieniem tego kodu.

Gdyby zadanie było normalnie sformułowane np.: mamy pliki a,b,c - każdy zawiera liczbę naturalną w formacie dziesiętnym, ... napisz funkcję która - to okazałoby się, że żadnej gimnastyki nie trzeba, i mało któremu programiście nawet c goto do głowy przyjdzie*.

A to nie koniec, bo pozostaje proble, po czym oceniać rozwiązania. Nie mamy obiektywnej metryki czytelności z tego co wiem.
Zawsze może pojawić się jasio, który stwierdzi - a dla mnie goto czytelniejsze i co teraz?

Mierzymy linie kodu? bez sensu w większości języków. Ilość znaków ascii? (robimy golfa - to nawet fajne, ale czytelnie raczej nie będzie).
Cyclomatic complexity?
Halstead complexity?
Maintainability index (https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metrics-maintainability-index-range-and-meaning?view=vs-2022 )

Wszystkie dają się nadużyć i prowadzą do innego typu "golfa". (Pomimo tego, że uważam, że o ile nie są użyte w celach konkursowych to całkiem ładnie pokazują które fragmenty kodu czyta się łatwiej, a które trudniej).

1
jarekr000000 napisał(a):
furious programming napisał(a):

BTW: funkcje do_something, init_stuff i prepare_stuff zwracają wartość logiczną, ew. kod błędu, który musi być zerowy, aby wykonywanie dalszych instrukcji było możliwe. Jeśli zwrócą wartość niezerową, to należy posprzątać i wyjść z funkcji. Chodzi tutaj wyłącznie o wykrycie problemu i o odpowiednią reakcję na niego — nieważne czy zwracają coś, czy walą wyjątkiem.

Ale przecież w tym przykładzie dokładnie to tak nie działa i na tym trik polega, jeśli nie widziałeś tego w wersji z goto, to w wersjach alternatywnych jest to jasne.
Sprzątamy niezależnie od tego co zwróci do_something.

Przecież dokładnie to napisałem w tym cytacie: „Jeśli zwrócą wartość niezerową, to należy posprzątać i wyjść z funkcji.”

Co zresztą świadczy o tym, że źle ją napisano (nie wiemy jak - ale ze sposobu sprzątania to wynika). A źle ją napisano, żeby patoprzykładem uzasadnić goto.

Ze sposobu sprzątania wynika, że ta funkcja realizuje proces podzielony na kilka kroków, z czego każdy krok może walnąć błędem, przez co wcześniej (bezbłędnie) wykonane kroki muszą zostać cofnięte. To że ktoś zaimplementował to w formie jednej funkcji foo i użył goto, nie oznacza, że jest źle napisana. Takie przynajmniej jest moje zdanie.

To klasyczna manipulacją, (zasadniczo oryginalna strona z przykładami) w którą dałem się złapać - tworzone jest sztucznie dziwaczne środowisko, w którym zdefiniowane wcześniej funkcje są naprawdę źle popisane, po to żeby uzasadnić, że w końcowym, małym fragmencie kodu goto wyjdzie całe na biało.

Ten artykuł nie pokazuje, że goto jest najlepsze, a to, że jego użycie jest uzasadnione w języku C, co poparte jest nie tylko przykładowymi szablonami, ale też snippetami z realnych projektów (np. z kernela Linuksa). I w większości przypadków faktycznie goto jest sensownym i czytelnym rozwiązaniem.

Chyba że moją propozycję nazywasz manipulacją?

Zaproponowałem podanie kontrprzykładów w innych językach, bo prawdziwości rozważań teoretycznych nie potwierdzi ten, kto danego języka nie zna. Kod jest jednoznaczym pokazem możliwości implementacji podobnego problemu, rozwiewającym wszelkie wątpliwości. Pierwszy przykład — ten ze sprzątaniem — można użyć wyobraźni i sobie zilustrować w poniższy sposób:

  • jest dany proces (funkcja foo),
  • proces dzieli się na trzy kroki wykonywane sekwencyjnie,
  • każdy krok może spowodować błąd, który wymusi przerwanie procesu,
  • w przypadku błędu, coś trzeba będzie sfinalizować.

To nie jest schemat wyssany z tyłka, oderwany od rzeczywistości, a praktyczny problem mający wiele rozwiązań, zależnie od języka. Poprosiłem więc o zilustrowanie implementacji w tych innych językach, co jest jak najbardziej sensownym podejściem do tematu i potwierdzeniem, że w językach które goto nie mają, da się to rozwiązać w sposób krótki i czytelny, mimo wszystko (nieważne czy w formie jednej funkcji czy wielu, czy z RAII/GC/pattern-matchingiem itd. itp.). I to samo z każdym innym przykładem.

Gdybym chciał się uciec do manipulacji, to bym już dawno napisał, że potraficie tylko teoretyzować, przerzucać się pustymi frazesami i buzzwordami, bo mało kto będzie wiedział o co wam chodzi, a kontrprzykładów w tych nowoczesnych językach (w tym tych bez goto) nie pokazujecie, by wstydzicie się kupy, którą musielibyście wyprodukować, aby zaimplementować podobną logikę. Ale tak nie napisałem (i nie napiszę), bo to by było nieprawdziwe, niesprawiedliwe i niemerytoryczne.

A przez udziwnienia wyszło nadal nieczytelnie, skoro ludzie mają problem ze zrozumieniem tego kodu.

Te przykłady są tak proste, że nikt nie powinien mieć żadnego problemu z ich zrozumieniem.

1

No to wersja w GOLang:

func foo(bar int) (return_value int) {
	defer undo_something()
	if !do_something(bar) {
		return
	}
	defer destroy_stuff()
	if !init_stuff(bar) {
		return
	}
	defer clean_stuff()
	if !prepare_stuff(bar) {
		return
	}
	return_value = do_the_thing(bar)
	return
}

https://go.dev/play/p/RH46NsNMmrV

Nie wnikam czy czytelniejsze, ale bez goto

1

Po pierwsze to akurat podałem przykład.

Po drugie to jednak nadal uważam, że ten akurat snippet jest absurdalny.

Normalne jest, że jak inicjalizuje jakiś zasób to albo się to udaje i wtedy na nim pracuje ( a potem sprzątam). Albo się nie udaje i wtedy nie ma czego sprzątać.
Tu są jakieś koziołki - czemu mam sprzątać jak się nie udało, po co ktoś tak zrypał funkcję do_something?

Odpowiednik w javie miałby taki zapis:

Optional<Integer> foo(int bar)  {
 try (
   var sth = open_something(); 
   var stuff = init_something(sth);
   var prepared = prepare_stuff(stuff)
  ) {
     return do_the_thing(bar, prepared);
  }
  return Optional.empty(); // jak ja nie cierpie tych multiple returnów (ale java jest chora)
}

Tylko, że tutaj open_something jak się nie uda - to od razu sam zamyka zasób, nie ma czego sprzątać, podobnie pozostałe funkcje.
Do tego te funkcje zwracają zasób typu AutoClosable (to już specyfika javy), pozwalająca ładnie to zamknąć w przypadku "błędów".

0

Właściwie w Pythonie powinno też to zadziałać, przy założeniu, że te metody to context managery, które czyszczą swoje dane. Imo taki zagnieżdzony kod i tak jest czytelniejszy niż ten przykład z GOTO bo tam trzeba się cofać żeby zrozumieć logikę a tutaj idziesz od góry do dołu

def foo(bar: int):
    with do_something(bar) as value:
        if not value:
            return None

        with init_stuff(bar) as value:
            if not value:
                return None

            with prepare_stuff(bar) as value:
                if not value:
                    return None

    return do_the_thing(bar)

0
jarekr000000 napisał(a):

Normalne jest, że jak inicjalizuje jakiś zasób to albo się to udaje i wtedy na nim pracuje ( a potem sprzątam). Albo się nie udaje i wtedy nie ma czego sprzątać.
Tu są jakieś koziołki - czemu mam sprzątać jak się nie udało, po co ktoś tak zrypał funkcję do_something?

Dlatego że ten przykład z artykułu służy tylko do zilustrowania problemu — funkcje typu do_something czy prepare_stuff służą głównie do pokazania, że coś trzeba zrobić, wykonać jakiekolwiek instrukcje. Równie dobrze te funkcje w tym przykładzie można by zamienić na komentarze i szablon pozostanie taki sam. I w sumie pod tym szablonem w tym artykule jest podany życiowy przykład (z kernela), który już interpretacji nie podlega.

Nie sądziłem, że tyle problemów wyniknie z tak bzdurnych przykładów. :D

0

No to weźmy przykład z jajka i zapiszmy go w Ruscie (krzywe tłumaczenie, bo nie za bardzo wiem co dokładnie i jak to robią funkcje, które tam są):

enum Error {
    NoMemory,
    CannotIOMapResource(Resource),
    // Etc. nie chce mi się teraz reszty możliwych błędów analizować
}

fn audio_clock_probe(pdev: &mut platform::Device) -> Result<(), Error> {
    let priv = Devm::new(&pdev.dev)?;

    pdev.lock.init(); // Nie wiem co to tutaj robi, to raczej nie jest idiomatyczny Rust, ale dodałem by być zgodnym
    
    pdev.set_driver_data(&mut priv);

    pdev.mmio_base = devm::platform::Resource::ioremap(&priv)?;

    let _guard = pdev.dev.enable_runtime();
    pdev.dev.clock_create()?;
    pdev.dev.clock_add("audio")?;

    priv.register_clocks(&mut pdev.dev)?;

    Ok(())
}

Coś w ten deseń. Na pewno gdzieś się popieprzyłem z tym jak będzie podawany odpowiednia struktura i borrow checker będzie krzyczał, ale idea sprzątania jest jak najbardziej zachowana. Zero jakichkolwiek goto i nawet nie ma potrzeby na nie, bo wszystko załatwia nam RAII.

0

macie tu kod z kernela - goto do czyszczenia mutexa i returna.

https://github.com/torvalds/linux/blob/master/fs/bfs/file.c#L64

static int bfs_get_block(struct inode *inode, sector_t block,
			struct buffer_head *bh_result, int create)
{
    blablabla

	/* The rest has to be protected against itself. */
	mutex_lock(&info->bfs_lock);

	/*
	 * If the last data block for this file is the last allocated
	 * block, we can extend the file trivially, without moving it
	 * anywhere.
	 */
	if (bi->i_eblock == info->si_lf_eblk) {
		dprintf("c=%d, b=%08lx, phys=%08lx (simple extension)\n", 
				create, (unsigned long)block, phys);
		map_bh(bh_result, sb, phys);
		info->si_freeb -= phys - bi->i_eblock;
		info->si_lf_eblk = bi->i_eblock = phys;
		mark_inode_dirty(inode);
		err = 0;
		goto out;
	}

	/* Ok, we have to move this entire file to the next free block. */
	phys = info->si_lf_eblk + 1;
	if (phys + block >= info->si_blocks) {
		err = -ENOSPC;
		goto out;
	}

	if (bi->i_sblock) {
		err = bfs_move_blocks(inode->i_sb, bi->i_sblock, 
						bi->i_eblock, phys);
		if (err) {
			dprintf("failed to move ino=%08lx -> fs corruption\n",
								inode->i_ino);
			goto out;
		}
	} else
		err = 0;

	dprintf("c=%d, b=%08lx, phys=%08lx (moved)\n",
                create, (unsigned long)block, phys);
	bi->i_sblock = phys;
	phys += block;
	info->si_lf_eblk = bi->i_eblock = phys;

	/*
	 * This assumes nothing can write the inode back while we are here
	 * and thus update inode->i_blocks! (XXX)
	 */
	info->si_freeb -= bi->i_eblock - bi->i_sblock + 1 - inode->i_blocks;
	mark_inode_dirty(inode);
	map_bh(bh_result, sb, phys);
out:
	mutex_unlock(&info->bfs_lock);
	return err;
}
0

@WeiXiao: ale to dokładnie działa tak jak to co opisaliśmy my, a nie to co opisał autor. Czyli masz:

operation_a();

if (!failable_operation_b(args)) {
  goto handle_b;
}

// stuff

clean_b();
handle_b: // <- tutaj pomijamy czyszczenie `b` jeśli `failable_operation_b` zwróciło błąd
clean_a();

return value;

Natomiast w artykule, który wstawił @furious programming jest kod zapisany tak:

operation_a(&a);

if (!failable_operation_b(&b, args)) {
  goto handle_b;
}

// stuff

handle_b:
clean_b(b); // <- tutaj czyścimy `b` nawet jeśli `failable_operation_b` zwróciło błąd
clean_a(a);

return value;

Więc u Ciebie ten kod ma sens (czyścimy tylko jeśli funkcja) failable_operation_b nie zwróciła błędu (bo jeśli zwraca błąd, to powinna sama po sobie posprzątać).

0

@hauleth: ten pierwszy przykład z artykułu ma tylko obrazować ideę sensowności wykorzystania goto, natomiast praktyczne zastosowanie podano pod tym przykładem (i kilka przez @WeiXiao). Nadal ten schemat będzie miał zastosowanie: wystarczy, że przed pierwszym warunkiem znajdą się instrukcje, których działanie trzeba będzie cofnąć, jeśli pierwszy warunek nie zostanie spełniony. W końcu w tych praktycznych zastosowaniach, w żadnym przykładzie if nie jest wykonywany jako pierwsza instrukcja w funkcji.

To jest mały bubel (ten pierwszy w artykule), mógł być lepiej napisany, jednak miałem nadzieję, że wyobraźni wam nie brakuje i że bez problemu potraficie sobie wyobrazić przykładowe zastosowania podobnego rodzaju. Komentujcie ideę/schemat, nie sam snippet, który tylko w skrócie obrazuje o co w ogóle chodzi.

0

No to wtedy masz dokładnie RAII, tak jak podałem wyżej. Masz funkcję, która zwraca typ, który wyczyści co trzeba w momencie jak będzie on niszczony. Przykład który podał @WeiXiao będzie zapisany tak:

fn bfs_get_block(inode: &Inode, block: Sector, bh_result: &mut BufferHead, create: bool) -> Result<(), Error> {
    // bla bla

    let _guard = info.bfs_lock.lock();

    if bi.i_eblock == info.si_lf_eblk {
		// something there
        return Ok(())
    }

    let phys = info.si_lf_eblk + 1;
	if phys + block >= info.si_blocks {
        return Err(Error::NoSpace)
    }

    // Rest of the code

    Ok(())
}

I wszystko samo nam się wyczyści, mutex zostanie zwolniony, etc. Bez jakiegokolwiek goto w okolicy. W C musimy używać goto bo nie ma lepszych rozwiązań, ale jak projektujemy język w XXI wieku, to już jednak da się to rozwiązać bez takich dziwnych konstrukcji.

Przykład jak to by wyglądało w Zigu:

fn bfs_get_block(inode: *Inode, block: Sector, bh_result: *BufferHead, create: bool) !void {
  // bla bla

  info.bfs_lock.lock();
  defer info.bfs_lock.unlock();

  if (bi.i_eblock == info.si_lf_eblk) {
    // something there
    return;
  }

  const phys = info.si_lf_eblk + 1;
  if (phys + block >= info.si_blocks) {
    return BfsError.NoSpace;
  }

  // Rest of code
}

I tak samo zostanie cokolwiek posprzątane po nas, bez zabawy w goto czy inne manualne skoki. Więc tak, goto do obsługi błędów w C ma rację bytu, ale to ze względu na upośledzenie C, a nie ze względu na użyteczność goto.

1

Podsumowując:

RAII (C++, Rust, D) > defer (Go, Zig) > try-with-resources (Java / C#) > try+finally (Java) > goto (C)

0
Krolik napisał(a):

Podsumowując:

RAII (C++, Rust, D) > defer (Go, Zig) > try-with-resources (Java / C#) > try+finally (Java) > goto (C)

Functional effects system (zio/cats) > RAII

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

Podsumowując:

RAII (C++, Rust, D) > defer (Go, Zig) > try-with-resources (Java / C#) > try+finally (Java) > goto (C)

Functional effects system (zio/cats) > RAII

Mało osób wie jak tego używać, to z ciekawości bardzo chętnie się dowiem jak ww kod używający goto/defer/RAII będzie wyglądał z wykorzystaniem systemu efektów. Dasz przykład?

0

goto miażdży. :]

procedure TFormSettings.FormCloseQuery(ASender: TObject; var ACanClose: Boolean);
label
  TryValidateSettings;
var
  PathRoot:   String;
  PathEditor: String;
begin
  if FSaving then
  begin
    FSaving := False;

  TryValidateSettings:
    PathRoot   := EditPathRoot.Text;
    PathEditor := EditPathEditor.Text;

    if not ValidatePathRoot(PathRoot)     then begin ACanClose := False; exit; end;
    if not ValidatePathEditor(PathEditor) then begin ACanClose := False; exit; end;

    EditPathRoot.Text   := PathRoot;
    EditPathEditor.Text := PathEditor;

    ModalResult := mrOK;
  end
  else
    if FModified then
    case FontsEditor.Dialogs.Message.Execute({...}, {...}, MB_ICONWARNING or MB_YESNOCANCEL or MB_DEFBUTTON3) of
      mrCancel: ACanClose := False;
      mrNo:     ACanClose := True;
      mrYes:    goto TryValidateSettings;
    end;
end;
procedure TFormMain.MenuItemFileSaveAsClick(ASender: TObject);
label
  TrySaveProject;
begin
  DialogSaveProject.InitialDir := GetCurrentDir();

  if FontsEditor.Project.Stored then
    DialogSaveProject.FileName := ExtractFileName(FontsEditor.Project.FileName)
  else
    DialogSaveProject.FileName := 'new font.bin';

TrySaveProject:
  if DialogSaveProject.Execute() then
    if FontsEditor.Project.SaveToFile(DialogSaveProject.FileName, True) then
    begin
      {...}
    end
    else
      if FontsEditor.Dialogs.Message.Execute({...}, {...}, MB_ICONERROR or MB_YESNO or MB_DEFBUTTON2) = mrYes then
        goto TrySaveProject;
end;
0
furious programming napisał(a):

goto miażdży. :]

...
...

Wygląda jakby TryValidateSettings oraz TrySaveProject mogły być po prostu procedurami.

Przykład podałem poniżej. Jeśli ktoś zacząłby od takiego kodu - właśnie z wydzieloną funkcją - to nie widze powodu żeby ją zamienić z powrotem na goto.

procedure TFormSettings.FormCloseQuery(ASender: TObject; var ACanClose: Boolean);
label
  TryValidateSettings;
begin
  if FSaving then
  begin
    FSaving := False;
    TryValidateSettings();
  end
  else
    if FModified then
    case FontsEditor.Dialogs.Message.Execute({...}, {...}, MB_ICONWARNING or MB_YESNOCANCEL or MB_DEFBUTTON3) of
      mrCancel: ACanClose := False;
      mrNo:     ACanClose := True;
      mrYes:    TryValidateSettings();
    end;
end;

procedure TFormSettings.TryValidateSettings();
var
  PathRoot:   String;
  PathEditor: String;
begin
  PathRoot   := EditPathRoot.Text;
  PathEditor := EditPathEditor.Text;

  if not ValidatePathRoot(PathRoot)     then begin ACanClose := False; exit; end;
  if not ValidatePathEditor(PathEditor) then begin ACanClose := False; exit; end;

  EditPathRoot.Text   := PathRoot;
  EditPathEditor.Text := PathEditor;

  ModalResult := mrOK;
end;
1

Mogłyby być gdyby nie to, że fragment, który wydzieliłeś, nie powinien być nigdzie widoczny — nigdzie prócz w metodzie, w której go umieściłem. Tym właśnie kończy się gotofobia — logika, która publicznie widoczna być nie powinna, a jest upubliczniana tylko po to, by uniknąć użycia goto. Pisząc tutaj „publicznie widoczna”, w tym przypadku mam na myśli jej widoczność dla pozostałych metod klasy.

Mamy trzy rozwiązania:

  1. Wydzielić fragmenty logiki do osobnych metod plus punkt 2.
  2. Zamienić goto na pętlę kontrolowaną flagą logiczną.
  3. Użyć goto aby powtórzyć/pominąć fragment.

Pierwszy przykład ma poważny minus, o którym napisałem na górze — tworzenie metod jednorazowych drastycznie zwiększa liczbę metod w module oraz upublicznia fragmenty logiki, które nie powinny być publiczne (bo nie są uniwersalne). Drugi sposób, czyli kombinacje z pętlami i flagami to typowa patalogia wynikająca z gotofobii, bo tworzy kolejne lokalne zmienne, wydłuża i zaciemnia kod.

goto wyklucza powyższe problemy, a dodatkowo jasno informuje, że nie mamy do czynienia z typową pętlą, a z nietypowym skokiem. Jeśli widzę goto to wiem, że skok ten nie dotyczy klasycznej pętli, a powrotu do specyficznego fragmentu lub skoku w dalszą część w tym samym bloku lub poza nim (czego klasyczna pętla nie wspiera).

Dla mnie goto to niesamowicie wygodne narzędzie.

2

Mozna też odwrócić sprawdzenie warunków:

Procedure TFormSettings.FormCloseQuery(ASender: TObject; var ACanClose: Boolean);
label
  TryValidateSettings;
var
  PathRoot:   String;
  PathEditor: String;
begin
	if FModified then
	case FontsEditor.Dialogs.Message.Execute({...}, {...}, MB_ICONWARNING or MB_YESNOCANCEL or MB_DEFBUTTON3) of
	  mrCancel: ACanClose := False;
	  mrNo:     ACanClose := True;
	  mrYes:    FSaving := True;
	end;

	if FSaving then
	begin
		FSaving := False;

		TryValidateSettings:
		PathRoot   := EditPathRoot.Text;
		PathEditor := EditPathEditor.Text;

		if not ValidatePathRoot(PathRoot)     then begin ACanClose := False; exit; end;
		if not ValidatePathEditor(PathEditor) then begin ACanClose := False; exit; end;

		EditPathRoot.Text   := PathRoot;
		EditPathEditor.Text := PathEditor;

		ModalResult := mrOK;
	end
end;
0
furious programming napisał(a):

Mogłyby być gdyby nie to, że fragment, który wydzieliłeś, nie powinien być nigdzie widoczny — nigdzie prócz w metodzie, w której go umieściłem. Tym właśnie kończy się gotofobia — logika, która publicznie widoczna być nie powinna, a jest upubliczniana tylko po to, by uniknąć użycia goto. Pisząc tutaj „publicznie widoczna”, w tym przypadku mam na myśli jej widoczność dla pozostałych metod klasy.

Nie zarzucaj mi proszę gotofobii - goto jako element kontroli przepływu, w taki sposób jaki Ty go użyłeś w swoim kodzie jest jaknajbardziej okej - jeśli każdy kod z goto by tak wyglądał, to nie miałbym nic przeciwko i bardzo mocno broniłbym tego podejścia. Taki sposób jaki podałeś moim zdaniem jest okej - bo używasz go jako zamiennik pętli i ifów, czyli ten goto nadal działa w ramach scope'u - co jest spoko. Także nie jestem goto-fobem, jeśli jest użyte poprawnie - może być.

Ale - mój problem z goto jaki mam, to to że można go użyć żeby napisać bardzo shit code, i to często niespodziewanie - przez nieudany refaktor, np goto które skacze pomiędzy dwoma niezależnymi pętlami, albo dwoma niezależnymi funkcjami - w Twoim kodzie @furious programming takich nie ma, bo widocznie masz na tyle dyscypliny żeby takich nie pisać. goto przypomina trochę - dla mnie - jazdę bez kasku na motorze - dopóki jeździsz bardzo dobrze, zawsze jesteś uważny i nie popełnisz nigdy błędu; ale pomyl się - no właśnie, katastrofa.

Dla mnie, nawet cień szansy że zostanie dodane do mojego source-code'u goto pomiędzy dwoma scope'ami (np ze środka pętli do innej pętli), albo goto które wrzuca nas w nieskończoną rekurencję skoków bez wyjścia to już jest za duże ryzyko - jestem bardzo ostrożny w kodzie, ale oceniam takie goto jako za dużą szansę na buga - widzę po Twoim kodzie że Ty je akceptujesz, bo np sam zawsze piszesz kod i jesteś pewien że nie dodasz żadnego goto które jest bez sensu. To nie jest też akademickie rozważanie, bo sam zaczynałem od Delphi i sam używałem goto, aż napotkałem buga ciężkiego do znalezienia właśnie przez to że użyłem goto by przeskoczyć pomiędzy nieodpowiednimi elementami.

Jeśli powstałby kompilator który pozwala na goto ale tylko w obrębie jednego scope'u (tak jak w przykładzie @furious programming z TryValidateSettings i uniemożliwiałby skoki łamiące zakres (czyli goto pomiędzy pętlami jest zablokowany, i goto pomiędzy różnymi zakresami leksykalnymi) - to wtedy moim zdaniem byłoby to akceptowalne - bo wyeliminowałoby ryzyko złych goto. Ale z tego co wiem, jeszcze tego nie mamy.

Drugi argument jaki mam - to skalowanie takich rozwiązań. Jeśli napisałbym algorytm na 2000 linijek kodu, i podzieliłbym go powiedzmy na 400 różnych funkcji, każda po 1-5 linijek, to na 100% połapałbym się w takim kodzie i wiedziałbym jak poprawnie go rozwijać dalej. Czy jeśli podzielimy te same 2000 linijek kodu na np 400 różnych goto, to czy również ten kod byłby łatwy do edycji? Wydaje mi się że nie, i z tego powodu argumentowałbym że goto - jeśli miałoby być użyte - to w małych funkcjach. Jeśli pętla jest zbyt verbose użyj sobie goto, jeśli masz funkcje użytą tylko raz - zainline'ują żeby nie była widoczna i wstaw tam goto, niech będzie. Ale jeśli zaczniesz składać większe algorytmy, i te poziomy w których goto skacze stają się większe i większe, wtedy stopień skomplikowania takiej aplikacji roście. Miksowanie ze sobą różnych funkcji daje bardzo dobre rezultaty w kwestii utrzymywania kodu. Miksowanie ze sobą różnych goto to (moim zdaniem) już krok w stronę spaghetii. Więc moim zdaniem funkcje nadają się do ciągłego rozwoju w górę, ale goto powinno zostać użyte w małych miejscach i raczej nie skalują się tak dobrze w górę.

No i trzeci argument, jak piszemy kod funkcjami, to można go przeczytać "z góry na dół", funkcja woła inną funkcje, która z kolei woła jeszcze inną, i ta jeszcze inna woła inną - można czytać ją z pewnym "pędem". Natomiast kod z goto często wymaga "skoków" podczas czytania - nie jest to oczywiście jakaś giga mega nie wiadomo jaka wada, czasem to jest nieuniknione - np z pętlami - ale jeśli ja mam wybór napisać kod czytany "z góry na dół" vs. taki w którym trzeba czytać raz w górę raz w dół, to oczywiście wybiorę to pierwsze.

Czwarty argument to jest odniesienie do Twojego: "Pisząc tutaj „publicznie widoczna”, w tym przypadku mam na myśli jej widoczność dla pozostałych metod klasy." dla mnie to nie jest argument "za goto", tylko przeciwko SRP. Jeśli masz dwie funkcje w kodzie (takie jak podałem w moim przykładzie), i uważasz że są niepotrzebnie publiczne (bo reszta klasy je widzi niepotrzebnie) - i Twoją radą na to jest inline tej funkcji i użycie goto, dla mnie to byłby sygnał żeby wydzielić z tego mniejszą klasę - jeśli te funkcje faktycznie są ze sobą "zwarte" tak że działają razem, a pozostałe funkcje w klasie nie powinny o niej wiedzieć - to moim zdaniem te dwie funkcje należą już do innej klasy i ja bym je wydzielił. Ale oczywiście Ty masz prawo taką funkcję inline'ować i użyć goto.

1
Panczo napisał(a):

Mozna też odwrócić sprawdzenie warunków: […]

Niestety nie można, bo pole FSaving jest zapalane w zdarzeniu kliknięcia w przycisk zapisu:

procedure TFormSettings.ButtonSaveClick(ASender: TObject);
begin
  FSaving := True;
  Close();
end;

Wtedy odpala się zdarzenie OnCloseQuery, które musi zapytać o zapis ustawień tylko jeśli flaga FSaving jest zgaszona i jednocześnie gdy flaga FModified jest zapalona. Dlatego warunek testujący FSaving jest na początku, aby w przypadku kliknięcia w przycisk zapisu, program nie wyświetlał dialogu.

Gdybym nie użył goto, musiałbym wydzielić kod pod etykietą TryValidateSettings do osobnej metody i tę metodę wywołać w dwóch miejscach — w bloku FSaving oraz w casie mrYes. A tak to nie tworzę zbędnych metod, nie duplikuję kodu, maksymalnie upraszczam i skracam logikę.

@Riddle: spokojnie, nie zarzucam Ci gotofobii, a tylko stwierdzam, że strach przed goto generuje obfuskację i uwidacznia logikę, która nie powinna być widoczna. Nie traktujcie też „gotofobii” jako jakiejś niewiarygodnej wady.

0

No ciężko sie poprawia ciało funkcji ze zmiennymi gdzie indziej.

ale:

musi zapytać o zapis ustawień tylko jeśli flaga FSaving jest zgaszona i jednocześnie gdy flaga FModified jest zapalona.

No to w czym problem:

if FModified AND not FSaving then
0
Panczo napisał(a):

No ciężko sie poprawia ciało funkcji ze zmiennymi gdzie indziej.

Pola FSaving i FModified są prywatnymi polami klasy okna (jak sugeruje prefix F*). Flaga FModified jest zapalana wtedy, gdy zawartość którejś kontrolki interfejsu się zmieni (pola edycyjne, checkboxy itd.), natomiast FSaving jest zapalana tylko jeśli wciśnie się przycisk zapisu.

Logika odpowiedzialna za kontrolę zapisu i opcjonalnych dialogów musi być zawarta w OnCloseQuery, dlatego że okno ustawień można zamknąć na wiele sposobów (przycisk Save, przycisk Cancel, przycisk na belce okna, klawisz Esc, skrót Alt+F4 oraz systemowe menu belki okna), a samo zamykanie okna może mieć dwa źródła — rozkaz zapisu ustawień (kliknięcie w przycisk Save) lub zamknięcie okna w celu anulowania zapisu zmian (przycisk Close lub inne podane). Wszystkie te przypadki są obsłużone w snippecie z poprzedniego posta (i działają prawidłowo — wszystkie sprawdziłem).

No to w czym problem:

if FModified AND not FSaving then

W tym, że zbiłeś dwa stany FSaving w jeden, a program musi inaczej reagować na oba stany, zanim przetestuje flagę FModified.

Z tego snippetu, który podałem wyżej, zapewne trudno wam wywnioskować dlaczego on tak wygląda a nie jakoś prościej (czemu warunki są sprawdzane w takiej formie i kolejności), ale tylko taka konfiguracja poprawnie obsługuje zapis ustawień i poprawnie kontroluje to kiedy wyświetlić dodatkowy dialog z zapytaniem.

0
furious programming napisał(a):

@Riddle: spokojnie, nie zarzucam Ci gotofobii, a tylko stwierdzam, że strach przed goto generuje obfuskację i uwidacznia logikę, która nie powinna być widoczna. Nie traktujcie też „gotofobii” jako jakiejś niewiarygodnej wady.

Ten strach przed goto jest zasadny, bo ludzie popełniają błędy - wiem że gdybym rozwijał projekt np 5 lat z goto to prędzej czy później niechcący popełniłbym taki błąd, a chcę tego uniknąć - dlatego zaproponowałem że dobrym wyjściem byłby kompilator który nie pozwala skompilować zbyt "ruchliwych" goto, tak żeby mogły być użyte tylko tak jak w Twoich przykładach.

Jeśli miałbym napisać taką funkcję jak Twoja, w której ta sama logika ma się wykonać kilka razy to użyłbym albo pętli albo wydzielił to do funkcji, z czego raczej wybrałbym funkcję. Odpowiedziałeś wtedy że funkcja jest niepotrzebnie widoczna:

  • Okej, no to możemy zrobić funkcję która nie jest w klasie, a poza nią i dostaje kontrolki jako parametr
  • Możemy też wydzielić te dwie funkcje do osobnej klasy - wtedy te dwie funkcje będą razem.
  • A dodatkowo, to ja nie uważam żeby taka widoczność funkcji była znowu czymś złym. Jeśli miałbym kod na 2000 linijek, i podzielił go na 400 funkcji, z których każda byłaby użyta tylko raz - to zostawiłbym to, nie próbowałbym tych funkcji ograniczać im widoczności.

Ale nie o tym chciałem -

Kiedy widzę tematy takie jak ten: Instrukcja goto To w nich się udzielają dwa rodzaje ludzi: większa grupa to ludzie którzy piszą sobie aplikacje konsolowe i nie lubią funkcji, więc używają goto - nie dlatego że to jest lepsze albo odpowiedniejsze rozwiązanie, tylko po to że "tak im lepiej pisać" (ale nawet nie dlatego że taki kod się lepiej pisze, tylko dlatego że to chyba wymaga mniej sił mózgowych żeby taki program stworzyć, niekoniecznie utrzymać). Druga grupa ludzi, to taka która przeanalizowała różne możliwości, i wybrała goto posługując się powodami, za i przeciw oraz są w tym w jakiś sposób racjonalne. Z tą pierwszą grupą nie ma o czym rozmawiać i należy od razu zgasić takie podejście: "rób funkcje, koniec siadaj" - dlatego że ich podejście i lubienie goto nie opiera się na tym że to jest pomocny element języka programowania, tylko używają go jako "hack" obejścia słabej struktury programu. Ale widać że przykłady i argumenty @furious programming są inne - są merytoryczne, to wybalansowanie kontroli przepływu, funkcji, klas, enkapsulacji, DRY, szczegółów implementacyjnych, czytelności kodu etc. - i to już jest dużo subtelniejsze zagadnenie.

Problemem jest to, że nawet jeśli uda się w debacie dojść do konsensusu, że podejście prezentowane przez @furious programming (czyli ja to rozumiem goto jako zamiennik wydzielania niepotrzebnych funkcji) jest okej i spójne - to może zostać źle zinterpretowane przez pierwszą grupę, która potem zacznie wrzucać goto w randomowe miejsca "bo przecież okaząło się że można".

Z innej beczki, można dodać jeszcze aspekt socjalny:

  • Niektóre biblioteki, języki czy frameworki znane są z tego że są "idioto odporne". Np taki PDO jest bardziej idioto odporny niż mysql, a program jednowątkowe są bardziej idiotoodporne niż te na wątkach. Niektóre biblioteki nawet do UI są bardziej idiotoodporne, w takim React ciężej zrobić podatność na XSS niż w vanilla JS czy jQuery.

I funkcje mają to do siebie, że one właśnie są bardziej idiotoodporne niż goto. Jeśli weźmiemy idiotę-juniora i każemu mu pisać tylko funkcjami, to spodziewam się że taki junior zrobi mniej błędów niż taki który będzie programował samymi goto.

0

Ja całe to podejście z goto i przykład z FormCloseQuery od @furious programming widzę tak:

  • Algorytm zaprogramowany bez goto i bez wydzielenia funkcji będzie musiał mieć duplikację : @Panczo starał się wyeliminować duplikacje odwracając if, ale to chyba się nie udało?
  • Duplikacja jest bardzo niefajna, więc w jakiś sposób powinno się ją usunąć:
    • Da się to zrobić albo pętlą
    • Albo wydzielając funkcję
    • Albo goto
  • No właśnie, i które teraz wybrać.
  • Ja na 100% wybrałbym wydzielenie funkcji albo nawet klasy.
  • Jeśli wydzielimy funkcję, to ona jest używa w jednym miejscu i można powiedzieć że jest "niepotrzebnie widoczna": Rozwiązania na to są trzy:
    • Albo wydzielić funkcje która nie jest metodą w klasie, i przyjmuje rzeczy przez parametry (ale to trochę łamie ankapsulacje)
    • Albo wydzielić to do osobnej klasy - coś co ja bym zrobił
    • Albo dodać to goto

Jeśli ktoś nie widzi nic złego w goto, to wybór może paść na goto. Ale jeśli jednak ktoś widzi coś złego (np. jak ja widzę że goto może potencjalnie wyjść poza zakres leksykalny w wyniku nieumiejętnie napisanego kodu lub nieumiejętnego refaktoru), to wtedy lepiej wybrać klasę IMO - no chyba że nikomuś nie przeszkadza "niepotrzebnie widoczna funkcja", wtedy można funkcję.

0

Pamiętajcie, że Free Pascal, którego dotyczą moje snippety, nie ma takich bajerów jak ”nowoczesne” języki — funkcjonalność związana z przepływem sterowania jest podobna jak w przypadku języka C. Dlatego też bierzcie to pod uwagę, że nie mam do dyspozycji np. defer czy RAII.

Riddle napisał(a):

Ja całe to podejście z goto i przykład z FormCloseQuery od @furious programming widzę tak:

  • Algorytm zaprogramowany bez goto i bez wydzielenia funkcji będzie musiał mieć duplikację : @Panczo starał się wyeliminować duplikacje odwracając if, ale to chyba się nie udało?

Nie udało się, bo zmienił się przepływ sterowania i dialog wyświetla się zawsze, a nie tylko wtedy, gdy zmiany zostały wprowadzone i gdy użytkownik zamknął okno (w celu anulowania zmian i ewakuacji z okna ustawień).

Można by było też wydzielić te fragmenty do funkcji lokalnych, czyli osadzonych wewnątrz ciała metody (coś jak lambdy w C++), ale to tylko spowoduje obniżenie czytelności, zmieni kolejność deklaracji tych fragmentów, nadal konieczna będzie duplikacja (zamiast fragmentów, to wywołań lokalnych funkcji) i nadal nie pozwoli na fall-through (jedna funkcja lokalna będzie wołała inną).

Mam też inne zdarzenia, w których również używam goto, ale etykiet mam trzy i korzystam z fall-through (plus kilka różnych dialogów, w zależności od sytuacji). Uniknięcie goto wydłuży kod, wymusi duplikację (wywołań), wymusi deklarację lokalnych funkcji (lub globalnych, a to niepotrzebnie uwidoczni fragmenty jednorazowej logiki) — no dramat.

0
furious programming napisał(a):

Pamiętajcie, że Free Pascal, którego dotyczą moje snippety, nie ma takich bajerów jak ”nowoczesne” języki — funkcjonalność związana z przepływem sterowania jest podobna jak w przypadku języka C. Dlatego też bierzcie to pod uwagę, że nie mam do dyspozycji np. defer czy RAII.

Riddle napisał(a):

Ja całe to podejście z goto i przykład z FormCloseQuery od @furious programming widzę tak:

  • Algorytm zaprogramowany bez goto i bez wydzielenia funkcji będzie musiał mieć duplikację : @Panczo starał się wyeliminować duplikacje odwracając if, ale to chyba się nie udało?

Nie udało się, bo zmienił się przepływ sterowania i dialog wyświetla się zawsze, a nie tylko wtedy, gdy zmiany zostały wprowadzone i gdy użytkownik zamknął okno (w celu anulowania zmian i ewakuacji z okna ustawień).

Można by było też wydzielić te fragmenty do funkcji lokalnych, czyli osadzonych wewnątrz ciała metody (coś jak lambdy w C++), ale to tylko spowoduje obniżenie czytelności, zmieni kolejność deklaracji tych fragmentów, nadal konieczna będzie duplikacja (zamiast fragmentów, to wywołań lokalnych funkcji) i nadal nie pozwoli na fall-through (jedna funkcja lokalna będzie wołała inną).

Mam też inne zdarzenia, w których również używam goto, ale etykiet mam trzy i korzystam z fall-through (plus kilka różnych dialogów, w zależności od sytuacji). Uniknięcie goto wydłuży kod, wymusi duplikację (wywołań), wymusi deklarację lokalnych funkcji (lub globalnych, a to niepotrzebnie uwidoczni fragmenty jednorazowej logiki) — no dramat.

A co jest nie tak z wydzieleniem kodu do funkcji tak jak napisałem, i potem tych dwóch funkcji do klasy? Wtedy nie mamy problemu z niepotrzebnie widocznymi funkcjami.

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