On that note, here’s some suggestions on writing more idiomatic code. I’ll start with this version of the code, since I don’t like dealing with the unsafe.
pub struct Conf {
locale: String,
}
static CONFIG: Option<Conf> = None;
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => {
let l = r.locale.to_string();
return l;
}
None => {}
}
return "".to_string();
}
The code if of course slightly weird then since the CONFIG
is never changing, but that’s hopefully easy to ignore; my focus is the body of the get_locale()
function anyways.
First: return EXPR;
in the end of a function is always more idiomatically written just EXPR
.
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => {
let l = r.locale.to_string();
return l;
}
None => {}
}
"".to_string()
}
Then, in this case it’s possible, and also more readable, to transform
match {
… => { …; return },
⁞
… => { …; return },
… => {}, // single non-early-returning branch
}
[some code…]
into
match {
… => { …; return …; },
⁞
… => { …; return …; },
… => {[some code…]},
}
i.e. avoiding the step of leaving the match
for one of the cases.
This is comparable to the more common transformation in other programming languages between
if … {
…:
return …;
}
[some code…]
and
if … {
…:
return …;
} else {
[some code…]
}
Long story short, we get
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => {
let l = r.locale.to_string();
return l;
}
None => {
"".to_string()
}
}
}
In case you wonder how "".to_string()
is still returned from the function even though it isn’t in the last line / final position: The whole function now consists of only a single expression, the match … {…}
expression, and the whole match … {…}
expression is being returned here. The match … {…}
evaluates to the block { "".to_string() }
in the None
case, which evaluates to its final expression "".to_string()
.
Now, return l;
is in the same kind of position as "".to_string()
though, so anther return
became irrelevant:
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => {
let l = r.locale.to_string();
l
}
None => {
"".to_string()
}
}
}
Avoiding the intermediate step of defining l
and immediately using it:
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => {
r.locale.to_string()
}
None => {
"".to_string()
}
}
}
Rewriting the match
using the => EXPR,
style arms instead of => { /* BLOCK */ …}
to save some vertical space (in case you use it, rustfmt
would even do this step for you)
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => r.locale.to_string(),
None => "".to_string(),
}
}
That’s it for control flow for now. Next up, string functions. to_string
on a &str
is for many Rust programmers the less nice-looking option compared to .to_owned()
, though that’s a matter of taste. On literals, some (e.g. the book) also like String::new("literal…")
. For &String
, I think most, if not all, people would prefer .clone()
. And for an empty string it’s nicer to use String::new()
. So we get
pub fn get_locale() -> String {
match &CONFIG {
Some(r) => r.locale.clone(),
None => String::new(),
}
}
Now this looks fine to me. But you could also use Option
’s combinators/methods to use a different style if you like. There’s the map
function that’s useful if most (or all) of your action happens in the Some
case. You cannot just call it like CONFIG.map(…)
here, because it takes an Option
by-value, but transforming to Option<&Conf>
first allows us to use it anyways
pub fn get_locale() -> String {
CONFIG.as_ref().map(…)…
}
The map takes a closure handling the Some
case:
pub fn get_locale() -> String {
CONFIG.as_ref().map(|r| r.locale.clone())…
}
Afterwards, we have Option<String>
. We can remove the Option
layer, giving a fallback for None
, with unwrap_or
:
pub fn get_locale() -> String {
CONFIG.as_ref().map(|r| r.locale.clone()).unwrap_or(String::new())
}
It’s cheap (essentially free) in the case of String::new
, but often, unwrap_or_else
can be useful to avoid function calls / extra work for constructing the fallback, in case the None
case isn’t reached
pub fn get_locale() -> String {
CONFIG.as_ref().map(|r| r.locale.clone()).unwrap_or_else(|| String::new())
}
we don’t need a closure, since String::new
as a function can be named directly; a simplification sometimes called “eta conversion”
pub fn get_locale() -> String {
CONFIG.as_ref().map(|r| r.locale.clone()).unwrap_or_else(String::new)
}
In case of String::new
, since the empty string is also String::default()
, i.e. the default value, you can even use the slightly shorter .unwrap_or_default()
, giving us the final result
pub fn get_locale() -> String {
CONFIG.as_ref().map(|r| r.locale.clone()).unwrap_or_default()
}
though rustfmt would have you re-format this as
pub fn get_locale() -> String {
CONFIG
.as_ref()
.map(|r| r.locale.clone())
.unwrap_or_default()
}
Choose yourself whether you prefer this, or the last match
statement version above. (I myself might actually prefer the match
version here.)