Consider the following code for reading a file:
use std::{fs::File, io::Read};
use std::any::type_name_of_val;
fn main() {
let path: &str = "foo.txt";
let mut file: File = match File::open(&&&path) {
Ok(f) => f,
Err(reason) => panic!("Could not open file: {path}, reason: {reason}"),
};
let mut data: String = String::new();
match file.read_to_string(&mut data) {
Ok(size) => println!("Read {size} bytes, content {data}"),
Err(reason) => panic!("Could not read file: {path}, reason: {reason}"),
}
println!("file {:?}", path); // These two lines return the same value: "foo.txt"
println!("file {:?}\n", &path); // even when this line is a reference to &str
println!("type {:?}", type_name_of_val(path));
println!("type {:?}", type_name_of_val(&path));
println!("type {:?}", type_name_of_val(&&path));
println!("type {:?}", type_name_of_val(&&&path));
}
Output:
> cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/read_file`
Read 35 bytes, content line 1
line 2
line 3
line 4
line 5
file "foo.txt"
file "foo.txt"
type "str"
type "&str"
type "&&str"
type "&&&str"
Question Why is this line valid?
File::open(&&&path)
I tried using File::open(path) (I presumed this is the right approach), but I noticed that using & multiple times still works.
I found this answer, but not sure if it applies to my example:
The compiler is able to dereference the argument of
push_stras much as necessary, sos1.push_str(s2)works just as well ass1.push_str(&&&&&s2).
Creating a reference to a value is always valid. Whether that value is temporary or itself a reference. So &&&expr will construct a reference to a reference to a reference to the value of expr. So that alone won't cause a compilation error.
There are two main ways that &T, &&T, &&&T, etc may all behave the same:
Recursive Trait Implementations
In your case with File::open, it will accept any value that implements AsRef<Path>. If we look for generic implementations on AsRef, you'll see it is implemented for references if the referenced type also implements AsRef. And since in the case of &&&path, that next type is also a reference and so on until it gets to the AsRef<Path> for str implementation.
This is a fairly common pattern on traits (if able) in the standard library and even in third-party if seeking a good degree of generality. You also see it in action with the println! since Debug is also for &T where T: Debug.
Deref Coercion
This doesn't appear in your code but an example like this also works:
fn handle_data(data: &[u8]) {
// do something with data
}
fn main() {
let data = vec![1, 2, 3];
handle_data(&&&data);
}
This works because Rust will perform an implicit coercion from &T to &U if T implements Deref. Since references (tautologically) implement Deref, that has the same outcome of collapsing multiple &&&s into one. This also integrates with more types like above &Vec<u8> is coerced to &[u8] just like a &String coerces to a &str.
Deref coercion isn't always applied - crucially it only works when the compiler knows the target type - but it is very common.