Kekeの日記

エンジニア、読書なんでも

Rustの複数の生存期間パラメータでハマった

問題となったソースコード

RustでPathcargoを使って、いろいろファイル操作をしていました。

  • app.rs
let input_path_str = matches.value_of("input").unwrap();
let output_path_str = matches.value_of("output");

let (input_path, output_path) = utils::get_output_file_name(input_path_str, output_path_str);
  • helper.rs
pub fn get_output_file_name(input_path_str: &str, output_file_str: Option<&str>) -> (&Path, &Path) {
    let output_path: &Path;
    
    let input_path = Path::new(input_path_str);
    
    if let Some(output_file_str) = output_file_str {;
        output_path = Path::new(output_file_str);
        return (input_path, output_path);
    }

    ...


    return (input_path, output_path);
}

すると以下のようなエラーが出るのです。

error[E0106]: missing lifetime specifier
 --> src/utils.rs:7:86
  |
7 | pub fn get_output_file_name(input_path_str: &str, output_file_str: Option<&str>) -> (&Path, &Path) {
  |                                                                                      ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `input_path_str` or `output_file_str`

error[E0106]: missing lifetime specifier
 --> src/utils.rs:7:93
  |
7 | pub fn get_output_file_name(input_path_str: &str, output_file_str: Option<&str>) -> (&Path, &Path) {
  |                                                                                             ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `input_path_str` or `output_file_str`

本記事では、一旦、問題を簡略化して考えようと思います。

問題

問題としては

  • 生存期間をコンパイラができていないコードを書いている

です。簡略化して以下のようなソースコードを考えましょう。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

longest関数は以下のように定義されてあります。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

これも同様のエラーがでるのですが、何が悪いのでしょう。

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`

これはxyのどっちの生存期間をもつのがわからないというエラーが出ます。

if文によって分岐したものはxも返しうるし、yも返す可能性があります。

関数として外に出しているからなおさらわかりにくいと思います。

例えば以下のようになっていたらどうでしょう。

fn main() {
    let string1 = "hoge"
    let result
    
     {
          let string2 = "piyo"
          result = longest(string1, string2);
     }

     println!("result in {}", result}
}

この場合は生存期間は明らかにstring1string2は違います。

もしlongest関数に渡されても、戻り値はどっちの生存期間であるかはわかりません。

解決策

問題はxかyかの生存期間がわからないことにつきます。

つまり、明示的に指定すればいいので生存期間パラメータを使います。 これによって生存期間aが生きる限りは関数の戻り値が生きることを指定します。

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }

生存期間パラメータがジェネリクスで指定しなければいけないのは、必然だと思います。

このように明示的に生存期間パラメータをつけることをmake signatureといいます。

longent関数が呼び出された時にRustはaにあたる生存期間が短いxyを探し出します。

これは

fn main() {
    let string1 = "hoge"
     {
          let string2 = "piyo"
          let result = longest(string1, string2);
          println!("result in {}", result}
     }
}

のときにうまくいってました。 しかし、以下のようにしたらどうでしょう。

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

errorがでます。これはprintlnが呼び出されるときに生存期間パラメータよりstring2も生存しておかないといけないからです。

このときは以下の様に対処することができます。

    fn longest<'a>(x: &'a str, y: str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }

このようにすることで生存期間によって制約を課すことなく、実行できました。

まとめ

Rustに入門すると生存期間についてつまづいて、そのままやめてしまう人が多いと思います。

私自身、入門した頃は非常に難しく感じていましたが、C言語と比べるとかなり安全なコードがかけるので学習コストより恩恵の方が多いように思います。