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 bool
a 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.