Rusts subtilste Syntax
(zkrising.com)Rusts let und const
letwird verwendet, um neue Variablen zu deklarieren- In der Form
let PAT = EXPR;und mächtiger, als es aussieht - In Kombination mit Pattern Matching bietet es praktische Funktionen
let (a, b) = (5, 10);let maybe_string: Option<String> = ..;let Some(value) = maybe_string else { panic!("die horribly")};
- In der Form
constsind Konstanten, die zur Compile-Zeit berechnet und direkt in den kompilierten Code eingebettet werdenconst MY_VAR: &str = "heyyyyyyyy man";const SECRET: i32 = 0x1234;- In der Form
const IDENT: TYPE = EXPR;, der Typ muss angegeben werden und Patterns können nicht verwendet werden
Was daran verwirrend ist
constkann unabhängig von der Deklarationsreihenfolge verwendet werden (Hoisting)
// kompiliert, obwohl X nach Y definiert ist
const Y: i32 = X + X;
const X: i32 = 5;
- Kann auch innerhalb von Funktionen deklariert werden, und selbst dort funktioniert Hoisting
fn oh_boy() -> i32 {
return X;
const X: i32 = 5;
// ^ kompiliert und funktioniert. Keine Warnung!
}
- Wenn man mit Programmierern arbeitet, die aus JavaScript kommen und gerade erst Rust lernen, ist das eine großartige Funktion, um sie aus dem Konzept zu bringen
- Das ist eine harmlose Folge einer großartigen Funktion, aber jetzt schreiben wir die schädliche Folge auf
Rusts match
// let PAT = EXPR;
let x = 5;
// hier ist `x` ein Pattern. Es wird geprüft, ob `5` in `x` hineingelegt werden kann
// dieses Pattern matcht immer -- man kann 5 immer in eine Variable namens `x` legen
// Nicht jedes Pattern muss unbedingt matchen. Zum Beispiel:
let (5, x) = (a, b);
// hier matcht der Ausdruck das Pattern nur dann, wenn a == 5 ist
//
// Das nennt man ein "refutable" Pattern
//
// Bei einer `let`-Deklaration muss der Fall behandelt werden, dass ein refutable Pattern "abgelehnt" wird:
let (5, x) = (a, b) else { panic!() };
//
// ...sonst könnte man Variablen haben, die "nur unter bestimmten Bedingungen existieren", und das ist nicht gut
- Schauen wir uns also
matchan. Was istmatch?
// `match` ist eine Liste von Patterns und den Aktionen, die ausgeführt werden, wenn sie matchen
//
// match EXPR {
// PAT => EXPR
// PAT => EXPR
// ..
// }
match (a, b) {
(5, x) => {
// wenn (a,b) mit (5,x) matcht, wird dieser Block ausgeführt
},
(x, 5) => {
// genauso: wenn (a,b) mit (x, 5) matcht..
},
(x, y) => {
// und das hier ist ein Pattern, das "alles abfängt", genau wie `let (x,y) = (a,b)` funktioniert
}
}
Lasst uns Schmerzen verursachen
- Es macht Spaß, Leute zu verwirren, aber wie wäre es mit vollständigem Elend und echten Bugs?
- Für mich ist das Rusts subtilste Syntax:
- Die interessanteste Zeile in diesem Text: Rusts subtilste Syntax ist, dass Konstanten selbst Patterns sind
- Diese Syntax bringt rund um Matching einige nette Ergonomics mit:
let input: i32 = ..;
const GOOD: i32 = 1;
const BAD: i32 = 2;
match input {
// das prüft, ob input == GOOD ist, weil GOOD eine Konstante ist
GOOD => println!("input was 1"),
// das prüft, ob input == BAD ist, weil BAD eine Konstante ist.
BAD => println!("input was 2"),
// das definiert otherwise = input und matcht immer...
otherwise => println!("input was {otherwise}"),
}
Konstanten in Großbuchstaben zu schreiben ist aber nur eine Konvention. Der Compiler gibt lediglich eine Warnung aus, wenn man es nicht tut.
const good: i32 = 1;
const bad: i32 = 2;
match input {
// hm...
good => {},
bad => {},
otherwise => {},
}
Jetzt haben wir drei Zweige, die gleich aussehen, aber was sie tun, hängt davon ab, ob Konstanten mit diesen Namen existieren!
Gehen wir noch einen Schritt weiter. Was passiert unten?
const GOOD: i32 = 1;
match input {
// Tippfehler...
GOD => println!("input was 1"),
otherwise => println!("input was not 1")
}
Hier gibt es zwar eine Compiler-Warnung, aber dieser Code wird immer input was 1 ausgeben
Oder etwas realistischer:
// ups, versehentlich diesen Import auskommentiert oder gelöscht
// use crate::{SOME_GL_CONSTANT, OTHER_THING}
// oh nein!
match value {
SOME_GL_CONSTANT => ..,
OTHER_THING => ..,
_ => ..,
}
Das verwirrt Leute. Besonders dann, wenn sie versuchen, mit Enums etwas Elegantes zu machen.
enum MyEnum {
A, B, C
}
// normalerweise schreibt man es so
match value {
MyEnum::A => ..,
MyEnum::B => ..,
MyEnum::C => ..,
}
// aber man kann auch so schreiben
use MyEnum::*;
match value {
A => {},
B => {},
C => {}
}
// und wenn man dann MyEnum ändert...
enum MyEnum { A, B, D, E };
use MyEnum::*;
// das kompiliert immer noch!
match value {
A => {},
B => {},
C => {},
}
// `C` ist jetzt ein Pattern, das "alles abfängt", weil nichts namens `C` mehr im Scope ist.
// Man macht also `let C = value`, und das matcht immer!!!
Clippy hat viele Regeln, die davor warnen, so etwas zu tun, weil es die Leute ständig verwirrt.
Aber es kann noch verwirrender werden:
// x irrefutably an 5 binden...
let x = 5;
// ...Moment mal...
const x: i32 = 4;
Dieser Code kompiliert nicht, weil const x ein Pattern ist, Konstanten gehoistet werden und der Code nun so ausgewertet wird:
let 4 = 5;
// error[E0005]: refutable pattern in local binding
// --> src/main.rs:3:5
// |
// 3 | let x = 5;
// | ^
// | |
// | pattern `i32::MIN..=3_i32` and `5_i32..=i32::MAX` not covered
// | not covered because the matched value is of type `i32`, interpreted as a constant pattern rather than a new variable
// | help: introduce a variable instead: `x_var`
// |
// = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
"expr ist gleich 4" ist kein irrefutables Match und behandelt den Fall nicht, dass es nicht zutrifft
Alle um euch herum verärgern
// Angenommen, `maybe` ist ein Option<&str>. Es könnte ein Text sein oder None.
let maybe_username: Option<&str> = ..;
// Das ist ein übliches Rust-Pattern für ein einzeiliges Match. Wenn es mit Some(..) matcht, können wir etwas mit dem String tun.
if let Some(username) = maybe_username {
// also läuft dieser Code, wenn username existiert...
return username.to_uppercase();
}
// aber Moment... jetzt läuft der Code nur noch, wenn er mit Some("hey") matcht
const username: &str = "hey";
Die Kombination aus Konstanten-Hoisting und der Tatsache, dass Konstanten Patterns sind, erlaubt es euch, rätselhaften Rust-Code zu schreiben
Das ist kein echtes Problem
- Realistisch gesehen kann das nur deshalb verwirrend sein, weil man
let UPPERCASEundconst lowercaseschreiben kann - Wenn Variablen, die mit Großbuchstaben beginnen, ein Lint-Fehler wären, würde die Verwirrung gar nicht erst entstehen
- weil man beim Versuch, auf Enum-Varianten oder Konstanten zu matchen, nicht versehentlich etwas binden könnte
- Aber um das klarzustellen: Das ist nur eine lustige Eigenart der Sprache
macro_rules! f {
($cond: expr) => {
if let Some(x) = $cond {
println!("i am some == {x}!");
} else {
println!("i am none");
}
}
}
fn main() {
f!(Some(100));
{
f!(Some(100));
return;
const x: i32 = 5;
}
}
3 Kommentare
Eigentlich ist das kein großes Problem, denn in den meisten Entwicklungsumgebungen gibt es einen Language Server,
der all das ableitet und anzeigt.
rust-analyzer, die Grundlage des Language Servers von RustRover, ist nämlich ein ziemlich leistungsfähiges Tool.
Im Grunde ist das so ein Text, der Dark Patterns sammelt, wie es sie in jeder Sprache gibt.
Das hier kann Verwirrung stiften!
So in etwa fühlt sich der Artikel an.
Krass … irgendwie. Wie will Rust damit wohl umgehen?