Rust uses definitive assignment, so sometimes it makes sense to write the code like this:
let x;
...
x = 92
...
Today I realized that this pattern extends nicely for cases where you want to fill a bunch of variables:
let (s, e, attrs); // <- The trick, can define the whole bunch at once.
if self.attr_pos == self.text.attrs.len() {
s = self.pos;
e = self.text.string.len();
attrs = Attrs::default();
} else {
let (attr_start, attr_end) = self.text.attrs[self.attr_pos].range;
if attr_start > self.pos {
s = self.pos;
e = attr_start;
attrs = Attrs::default();
} else {
s = attr_start;
e = attr_end;
attrs = self.text.attrs[self.attr_pos].attrs.clone();
self.attr_pos += 1;
}
}
self.pos = e;
Some((&self.text.string[s..e], attrs))
I've personally found the assignment-after-declaration pattern to be useful for getting a temporary value inside a conditional block to live longer, since the lifetime is based on the position of the let statement. For example, I recently wrote something similar to this:
use std::{
fs::File,
io::{self, prelude::*},
};
pub enum Example {
Foo,
Bar,
Baz(usize),
}
pub fn write_enum(value: Example) -> io::Result<()> {
let buf: String;
let bytes = match value {
Example::Foo => b"foo\n",
Example::Bar => b"bar\n",
Example::Baz(x) => {
buf = format!("baz {x}\n");
buf.as_bytes()
}
};
let mut file = File::create("example.txt")?;
file.write_all(bytes)
}
By declaring buf in the outer block, we can unify the runtime String buffer with the compile-time &'static [u8] literals, so we can use a simple &[u8] instead of a full Cow<[u8]>. (Effectively, we're letting the compiler-generated drop flag act as an ad-hoc enum.)
You might have been trying let x, y; and not let (x, y); - the former is "multiple independent variables", but the latter is essentially the syntax for tuple destructuring, just, well, without the tuple itself.