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_str
as 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.