Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different behavior between match and unwrap

I have done a small program, and it shows a weird behavior that I cannot explain. I am using rodio crate to try out some audio stuff.

I have done two programs that, in my opinion, should give the same result.

The first one I use matches to handle errors:

let sink : Option<Sink> = match rodio::OutputStream::try_default() {
        Ok((_, handle)) => {
            match Sink::try_new(&handle) {
                Ok(s) => Some(s),
                Err(_e) => None,
            }
        },
        Err(_e) => None,
};
println!("{}", sink.unwrap().len());

In the last one, I have used unwrap instead.

let (_, handle) = rodio::OutputStream::try_default().unwrap();
let s = Sink::try_new(&handle).unwrap();
println!("{}",s.len());

The first one executes the print statement as expected, while the last one panics in the second unwrap.

It is very strange to me as soon as there is no error propagation, implicit conversions or other things that can explain that. The problem here is not related to the error itself but the difference between the two codes.

like image 542
Gabriel Sartori Avatar asked Sep 30 '21 13:09

Gabriel Sartori


Video Answer


1 Answers

The issue is one of scoping and an implementation detail of rodio: the one critical item here is OutputStream::try_default(), it doesn't really matter how you handle Sink::try_new(&handle) it'll always behave the same, not so try_default, if you match or if let it it'll work fine, if you unwrap it it'll fail.

But why would that be, the two should be equivalent. The answer is in the details of rodio, specifically of OutputStreamHandle:

pub struct OutputStreamHandle {
    mixer: Weak<DynamicMixerController<f32>>,
}

So an OutputStreamHandle (OSH thereafter) only holds a weakref to a DynamicMixerController, to which the OutputStream (OS thereafter) holds a strong reference.

This means the OSH only "works" as long as the OS is alive.

let (_, handle) = OutputStream::try_default().unwrap();

does not name the OS, so doesn't hold onto it, it's immediately dropped, the Arc is released and the OSH holds onto nothing, and the sink is unhappy.

How can the other one work then? Because

    if let Ok((_, handle)) = OutputStream::try_default() {
        let sink = Sink::try_new(&handle).unwrap();
        println!("match {}", sink.len());
    }

is really rather

{
    let _var = OutputStream::try_default();
    if let Ok((_, handle)) = _var {
        let sink = Sink::try_new(&handle).unwrap();
        println!("match {}", sink.len());
    }
}

so the match/if let itself is keeping the Result alive, which is a blessing here (but causes issues just the next question over).

Since the Result is kept alive, the tuple is kept alive, the OutputStream is kept alive, the Arc is kept alive, and thus the OSH has a working mixer, which the sink can make use of.

You can fix the issue of the second version by binding the OutputStream to a "proper" name e.g.

let (_stream, handle) = OutputStream::try_default().unwrap();

prefixing a name with _ will create the binding for real, but will suppress the "unused variable" warning.

FWIW being careful around this sort of things is incredibly important around RAII types whose values you "don't need", most commonly mutexes protecting code as opposed to data:

let _ = m.lock().unwrap();

doesn't do anything, the mutex guard is not kept alive, so the lock is immediately released. In that case, you'd rather

let _lock = m.lock().unwrap():

or even better

let lock = m.lock().unwrap();
...
drop(lock);
like image 56
Masklinn Avatar answered Oct 19 '22 21:10

Masklinn