Returning default value from function when result is error

后端 未结 3 2001
不思量自难忘°
不思量自难忘° 2021-01-23 02:51

Is there something similar to the ? shortcut which instead of returning a result from a function when there is an error, returns a predefined value?

Basical

相关标签:
3条回答
  • 2021-01-23 03:03

    There is no such utility, but you can always write a macro:

    macro_rules! return_if_err {
        ( $to_test:expr, $default:expr ) => (
            if $to_test.is_err() {
                return $default;
            }
        )
    }
    
    fn pop_or_default(mut v: Vec<i32>) -> i32 {
        let result = v.pop();
        return_if_err!(result.ok_or(()), 123);
    
        result.unwrap()
    }
    
    fn main() {
        assert_eq!(pop_or_default(vec![]), 123);
    }
    

    You cannot return from an outer scope from a closure.

    0 讨论(0)
  • 2021-01-23 03:09

    I would create an inner function that uses a Result. This allows you to use the question-mark operator for the various error messages / default values that you'd like to return. You can then call the inner function and take either the success value or error value:

    fn index() -> String {
        fn inner() -> Result<String, String> {
            let t1 = some_func("pass").map_err(|_| "No parameters named pass")?;
            let t2 = some_func2(t1).map_err(|_| "A second error")?;
            let t3 = some_func3(t2).map_err(|_| "A third error")?;
            Ok(t3)
        }
    
        match inner() {
            Ok(v) => v,
            Err(v) => v,
        }
    }
    

    There's an unstable feature called try blocks that promises to make this slightly less bulky:

    #![feature(try_blocks)]
    
    fn index() -> String {
        let v = try {
            let t1 = some_func("pass").map_err(|_| "No parameters named pass")?;
            let t2 = some_func2(t1).map_err(|_| "A second error")?;
            some_func3(t2).map_err(|_| "A third error")?
        };
    
        match v {
            Ok(v) => v,
            Err(v) => v,
        }
    }
    
    0 讨论(0)
  • 2021-01-23 03:14

    This is kind of possible, but usually not a good idea, especially not in your example (I will explain that later).

    You cannot easily return a String and make ? return a default value, but you can define your own string type and implement std::ops::Try for it. Note that Try is still unstable!

    Let's see how this would work:

    // Just wrap a string
    struct StringlyResult {
        s: String,
    }
    
    // Convenience conversion 
    impl From<String> for StringlyResult {
        fn from(s: String) -> Self {
            Self { s }
        }
    }
    
    // The impl that allows us to use the ? operator
    impl std::ops::Try for StringlyResult {
        type Ok = String;
        type Error = String;
    
        fn into_result(self) -> Result<Self::Ok, Self::Error> {
            if self.s == "No parameters named pass" {
                Err(self.s)
            } else {
                Ok(self.s)
            }
        }
    
        fn from_error(s: Self::Error) -> Self {
            if s != "No parameters named pass" {
                panic!("wat");
            }
            Self { s }
        }
    
        fn from_ok(s: Self::Ok) -> Self {
            if s == "No parameters named pass" {
                panic!("wat");
            }
            Self { s } 
        }
    }
    

    With that we can implement index() like this:

    fn index() -> StringlyResult {
        let temp = some_func("pass")
            .map_err(|_| "No parameters named pass")?; 
        try_decrypt_data(&temp).into()
    }
    

    (Complete code on the Playground)

    So yes, the Try trait enables users to use the ? operator with their own types.


    However, as presented in your example, this is a terrible idea. You probably already noticed the "wat" sections in my code above. The problem is that your OK-type already exhausts the whole type (all instances of the type are valid OK-instances).

    Consider a function get_file_size() -> u64. Now this function can fail (i.e. it cannot determine the file size). You couldn't just return 0 to signal a failure occurred. How would the caller of your function distinguish between an environment in which the function cannot determine the file size and an environment where there the file is actually 0 bytes large?

    Similarly, how would the caller of your function distinguish the situation in which an error occurred and the situation in which the decrypted text is literally "No parameters named pass"? The caller can't! And that's bad.

    Notice that there is something similar, which is not as bad, but still not really idiomatic in Rust: get_file_size() -> i64. Here, we could return -1 to signal a failure. And this is less bad because -1 can never be a valid file size! (in other words, not all instances of your type are valid OK-instances). However, in this case it is still super easy to forget to check for errors. That's why in Rust, we always want to use Result.


    To make error handling easier, consider using the crate failure. With that, you can easily use strings as error messages without sacrificing type safety or sanity of your program. Example:

    use failure::{Error, ResultExt};
    
    fn index() -> Result<String, Error> {
        let temp = some_func("pass")
            .context("No parameters named pass")?; 
        Ok(try_decrypt_data(&temp)?)
    }
    
    0 讨论(0)
提交回复
热议问题