I'm learning rust, and I thought I understood how ownership works, but I'm confused by this (playground):
use regex::Regex;
use std::path::Path;
pub fn file_number(path: &Path) -> Option<(&str, u32)> {
let rexp = Regex::new(r"^(?<prefix>.*[^\d]|[^\d]*)(?<digits>\d+)$").unwrap();
let name = path.to_str().unwrap();
let Some(parts) = rexp.captures(name) else {
return None;
};
let number: u32 = parts["digits"].parse().unwrap();
let part = parts.get(1).unwrap();
let prefix = &name[part.start()..part.end()];
Some((prefix, number))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_number() {
let prefix: &str;
{
let path = Path::new("testing0123");
let number: u32;
(prefix, number) = file_number(path).unwrap();
assert_eq!(number, 123);
}
assert_eq!(prefix, "testing");
}
}
Where is the data prefix is pointing to in the test function? Why is it still valid after the Path struct is out of scope?
It seems like this should fail, but it runs successfully.
You're starting from a string literal here: "testing0123" is a &'static str (a reference to data that is valid for the lifetime of the program) because it's just stored as data in the compiled binary, so all the later variables which borrow from it can also be 'static.
If you start with a string from any other source, instead of a literal, then this will indeed fail as you expect.
Thanks for you reply, but I don't understand. I created a Path struct from the string literal, then got a slice from the Path's internal structure. Was the compiler able somehow to determine that Path was using a reference to my string literal? I expected a compiler error about a borrow from out of scope, or something.
Path is essentially a wrapper for a [u8] byte slice â it behaves like one in referencing and slicing. You didn't create a Path struct; you created a &'static Path from an &'static str, by calling Path::new().
You got a reference to a smaller Path-slice from a bigger one. That reference also gets to stay 'static.
The key is that both of these steps didn't allocate anything new to refer to. They created different references to the same memory. Therefore, since you started with &'static, the result can be &'static.
Note that new returns a reference, not a Path instance. The lifetime parameter tells the compiler that the reference returned is valid as long as the input reference is; if the input is &'static str then the output is &static Path. But where exactly is the referenced Path? Well, that's a trick question; the answer is "nowhere". Because Path is just a [1] wrapper around an OsStr, and because every str is a valid OsStr, it's valid to just coerce a &'a str into a &'a Path, and that's exactly what happens inside new:
unsafe { &*(s.as_ref() as *const OsStr as *const Path) }
Don't try this at home unless you're extremely sure of what you're doing.
The fn signature determines the relationship between the lifetime of the inputs (params) and outputs (return value). The lifetime annotations are elided but we can add them manually with their inferred values:
The input and output lifetimes are therefore equal. Lifetime annotations are generic type parameters, so their actual value is chosen by the caller. You chose 'static when you passed a &'static Path param (where its lifetime was similarly taken from the &'static str). Therefore, the output return value is (&'static str, u32).
Thanks. I guess I was thinking of Path as something more complex.
I went back and tried it creating the Path from a String that also went out of scope, and I got the compiler complaint I was expecting.