pub fn main() {
let res = [
String::from("Fruit"),
String::from("Vegetable"),
String::from("Oils"),
];
let mut name: &String;
for (index, row) in res.iter().enumerate() {
if index == 0 {
name = row;
}
if name != row {
println!("Not the same as first: {}", row);
}
}
}
The compiler complains:
Compiling playground v0.0.1 (/playground)
error[E0381]: borrow of possibly uninitialized variable: `name`
--> src/main.rs:17:12
|
17 | if name != row {
| ^^^^ use of possibly uninitialized `name`
error: aborting due to previous error
But because of the guard clause, name will always be assigned before access. Strange that compiler is aborting.
PS: I understand the code could be changed to assign name at declaration as below. But the example is deliberately toned down and it still feels compiler is unduly restricting a use case.
Rust can't always prove that a variable is initialized so it may fail in some cases that could work.
But in this case, let's imagine that enumerate starts at 1. Then your code is no longer correct. Because of this possibility Rust will not compile your code (to force you to make your code more robust).
The solution is to give a default value for name.
let name = String::new();
llet mut name = &name;
tldr: Rust does a conservative analysis to check if variables are initialized, and your program is too complex for it to reliably check
Still need to give a default. Or you can use Option to provide a default and unwrap it when you are sure that it is initialized.
In Rust, if the memory correctness of a program depends on runtime values, then Rust will require you to use unsafe, or use a workaround that usually incurs some runtime penalty.
Think about it this way.
The code the compiler forces you to write is not only easier to prove correct for the compiler but also for fellow humans looking at the code.
While you are correct in saying that the value index is run-time dependent but, for example, in the code posted in the OP, why is it difficult for compiler to infer that the assignment will always be present?
let x;
if true {
x = some_value;
}
drop(x); // possibly uninitialized
So as you can see, Rust makes no attempt to do branch analysis. This is also why your code does not compile.
In general we can fix this like so,
let x = None;
if true {
x = Some(some_value);
}
let x = x.unwrap(); // once you are sure that variables are initialized.
drop(x); // possibly uninitialized
While I can understand the thinking here, yet it would feel more appropriate if the compiler emit a warning instead of an error where it is not confident of the issue.
Warnings are for safe cases when there is the possibility of logical error, AFAIK. If there is a possibility for unsafety - this always will be an error, even if this possibility is entirely due to the fact that the flow is not checked deep enough.
I get that if there is a possibility, the compiler will default to an error because of it's safety first approach. And that is good! But in the specific example above (or the simple one mentioned by @RustyYato later in the thread) there is no possibility. Just that the compiler lacks the chops to infer that and projects it's conclusions on the populace under the guise of their own safety.
10/10 rustc will run for the President!
Instead, of forcing to delve into unsafe blocks, rustc can afford levels of safety. Alternatively, just like linters allow the programmer to skip/suppress certain warnings, it can allow some errors to be suppressed/skipped by the compiler (for specific lines/blocks) by using appropriate annotations. This is unlike unsafe blocks which will loose all other guards of safety.
While C/C++ compilers treat programmers as fire-resistant species, rustc treats us as pyrophoric ones. As always, the reality is somewhere in between.
'unsafe' causes the Rust front-end of the compiler to accept some things that it would otherwise reject. It cannot cause the LLVM back-end to not scramble your code if you in fact do something that is UB.
My understanding is that unsafe will accept all the things which it would otherwise reject. Instead, we could allow the programmer to hint the compiler on what class of safety issues it should accept while continuing to enforce other ones.
You can still use safe Rust by using an Option, now LLVM is not smart enough to optimize out the Option in your case (because it is a bit complex to analyze) but it will optimize my super simple case.
If it’s hard for the compiler to prove in general, then it’s in general hard for the next programmer to prove. And compilers work much more quickly than humans. If these kinds of cases are deemed generally unworthy of the effort it would take to analyze them deeply, I wonder if they’re not also a code smell.
Do you have any other examples of this behavior that might convince people that there’s really a flaw here? Given that you are aware of the workaround and that it’s of equal complexity to the code you want to write, it seems a bit contrived.
How unsafe actually works is closer to your second notion. All rules of Rust remain in place in unsafe blocks, and the borrow checker works the same way there for example. You only gain some new capabilities, mostly related to raw pointers.
With that said, this is not a situation where Unsafe is needed. I'd first extract the first element with next, then run the rest of the iterator in the loop.
Nothing immediate but will follow-up if I come across more. Though my hunch is that most scenarios will have work-around(s) involving trade-offs which a broader audience may deem acceptable.