14 Punkte von xguru 2024-11-05 | 3 Kommentare | Auf WhatsApp teilen

Rusts let und const

  • let wird 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")};
  • const sind Konstanten, die zur Compile-Zeit berechnet und direkt in den kompilierten Code eingebettet werden
    • const 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

  • const kann 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 match an. Was ist match?
// `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 UPPERCASE und const lowercase schreiben 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

 
sunrabbit 2024-11-05

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.

 
sunrabbit 2024-11-05

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.

 
kayws426 2024-11-05

Krass … irgendwie. Wie will Rust damit wohl umgehen?