Ferris Talk #10: Constant Fun mit Rust – const fn
In Version 1.61 hat Rust Tricks in Sachen konstanter Code-Evaluierung gelernt. Diese Ausgabe der Kolumne zeigt, was const in Rust kann und was es Neues gibt.
- Rainer Stropek
Egal, von welcher Programmiersprache man kommt, Konstanten gibt es praktisch überall. Rust wäre nicht seit Jahren eine der beliebtesten Programmiersprachen laut Stack Overflow Survey, wenn sie nicht auch im Bereich der Konstanten etwas zu bieten hätte, was man in vielen anderen Sprachen nicht findet. In der kürzlich erschienenen Version 1.61 hat Rust einige Tricks in Sachen Konstanten und konstanter Evaluierung von Code dazugelernt.
Die in nächster Zeit geplanten Versionen werden laut aktuell verfügbarer Nightly weitere Neuerungen in dem Bereich bringen. Das nehmen wir in der Rust-Kolumne zum Anlass, in dieser Ausgabe des Ferris Talk const
und const fn
in Rust vorzustellen und anhand von Beispielen zu zeigen, was es Neues gibt.
In diesem Artikel stehen die Codebeispiele im Mittelpunkt. Damit Interessierte den gezeigten Code ausprobieren und vor allem damit experimentieren können, ist bei jedem Beispiel ein Link angegeben, der den Code im Rust Playground öffnet. Dort lässt er sich ausführen und ändern, selbst wenn Rust noch nicht lokal installiert sein sollte.
Grundlagen zu Konstanten in Rust
Starten wir mit den Grundlagen, die in Rust ähnlich funktionieren wie in anderen Programmiersprachen. Folgender Code legt Konstanten mit verschiedenen Typen an (siehe Rust Playground, Sample 1). Die Zuweisung des Wertes zur Konstanten ANSWER
ist besonders zu beachten. Der Wert lässt sich durch einen konstanten Ausdruck ermitteln. In einem solchen Kontext steht nicht alles zur Verfügung, was Rust zur Laufzeit an Ausdrücken beherrscht. Die konkreten Einschränkungen sind in der Rust-Dokumentation beschrieben.
// A simple constant value
const NEARLY_THE_ANSWER: i32 = 41;
// A constant expression that is evaluated at compile time.
// Note: Not everything is allowed in a constant expression. See
// https://doc.rust-lang.org/reference/const_eval.html#constant-expressions
// for details
const ANSWER: i32 = NEARLY_THE_ANSWER + 1;
// A simple constant string (because of static lifetime elision,
// we don't need 'static)
const TEXT: &str = "the quick brown fox jumps over the lazy dog!";
// A constant array
const NUMBERS: [i32; 5] = [1, 2, 3, 4, 5];
fn main() {
// Constants are inlined at compile time wherever they are used.
println!("The answer is {ANSWER}");
println!("{TEXT}");
println!("Numbers: {:?}", NUMBERS);
// Constants can be declared in any scope, not just global.
const VERSION: &str = "1.2.3";
println!("Version: {}", VERSION);
}
Konstanten sind strukturierte Datentypen
Konstanten in Rust sind nicht auf Basisdatentypen wie Zahlen oder Zeichenketten beschränkt. Auch Strukturen können const
sein, wie das folgende Codebeispiel zeigt (siehe auch Rust Playground, Sample 2).
// Constants in Rust don't need to be basic data types.
// Structs can also be constant.
#[derive(Debug)]
struct Customer<'a> {
name: &'a str,
age: i32,
}
const CUSTOMER: Customer = Customer {
name: "John",
age: 42,
};
fn main() {
println!(
"Customer {} is of age {} ({:?})",
CUSTOMER.name, CUSTOMER.age, CUSTOMER
);
// Note that if you modify a const item, a new temporary
// item is created. The original const item is not modified.
CUSTOMER.age += 1;
}
In Rust können Konstanten auf die Adressen anderer Konstanten verweisen. Das folgende Beispiel demonstriert das, indem es eine Struktur NamedNumbers
definiert und im Rahmen einer Konstante verwendet (siehe Rust Playground, Sample 3). Die Konstante verweist auf andere Konstanten. Zu erwähnen ist dabei, dass beim Verwenden der Struktur im Rahmen der Konstantendeklaration die static
Lifetime nicht explizit anzugeben ist, Rust ergänzt sie im Hintergrund automatisch.
const TEXT: &str = "the quick brown fox jumps over the lazy dog!";
const NUMBERS: [i32; 5] = [1, 2, 3, 4, 5];
// A constant struct.
// Note that constants may refer to the address of other constants.
// The lifetime defaults to 'static (elided).
struct NamedNumbers<'a> {
name: &'a str,
numbers: &'a [i32; 5],
}
const NAMED_NUMBERS: NamedNumbers = NamedNumbers {
name: TEXT,
numbers: &NUMBERS,
};
fn main() {
println!("{TEXT:p}\n{:p}", NAMED_NUMBERS.name);
println!("{}", NAMED_NUMBERS.numbers.iter().sum::<i32>());
}
Konstanten in Traits
Die nächste Besonderheit von Rust ist, dass Konstanten mit Typen assoziiert werden und sogar Teil eines Traits sein können. Im folgenden Beispiel verlangt der Trait HasNumbers
, dass zwei Konstanten existieren (siehe Rust Playground, Sample 4). Für eine davon ist ein überschreibbarer Standardwert vorzugeben.
Empfohlener redaktioneller Inhalt
Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.
Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.
// Constants in traits
trait HasNumbers {
// Note that we have a constant here without a value.
const NUMBERS: [i32; 5];
// Constants in traits can have default values
const LAST_NUMBER: i32 = 5;
}
struct IHaveNumbers {}
impl HasNumbers for IHaveNumbers {
// This is an *associated constant*, as it is associated with a type.
const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveNumbers::LAST_NUMBER];
}
struct IHaveOtherNumbers {}
impl HasNumbers for IHaveOtherNumbers {
// Here we override the default value of the trait.
const LAST_NUMBER: i32 = 6;
const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveOtherNumbers::LAST_NUMBER];
}
fn main() {
println!("{:?}", IHaveNumbers::NUMBERS);
println!("{:?}", IHaveOtherNumbers::NUMBERS);
}
Konstanten mit Destruktoren
Jetzt verlassen wir denjenigen Bereich der Konstanten, der in anderen Programmiersprachen ähnlich ist, und sprechen über weiterführende Fähigkeiten von Rust. Als Erstes geht es um konstante Ausdrücke mit Destruktoren. Ja, richtig gehört: Konstante Ausdrücken können in Rust Destruktoren haben, die an der entsprechenden Stelle im Programm ausgeführt werden, wenn die Konstante verwendet wird. Etwas anders ist das bei statischen Variablen. Ihr Destruktor wird nicht aufgerufen, wenn das Programm bzw. der Thread beendet wird. Das folgende Listing (siehe auch Rust Playground, Sample 5) demonstriert dieses Verhalten.
// Constants can have destructors
struct WillSayGoodbye<'a>(&'a str);
impl<'a> Drop for WillSayGoodbye<'a> {
fn drop(&mut self) {
println!("{}", self.0);
}
}
// Note that destructor on statics will not run on program/thread exit.
static _GOODBYE_IN_ENGLISH: WillSayGoodbye = WillSayGoodbye("Goodbye");
const GOODBYE_IN_GERMAN: WillSayGoodbye = WillSayGoodbye("Auf Wiedersehen");
fn main() {
{
let _goodbye_sayer = GOODBYE_IN_GERMAN;
// Destructor will run at appropriate point when const is used. Therefore,
// this code will print "Auf Wiedersehen" on stdout when var goes out of scope. }
}