Rust ist eine junge und ehrgeizige Systemprogrammiersprache. Es implementiert die automatische Speicherverwaltung ohne Garbage Collector und anderen Aufwand für die Ausführung. Darüber hinaus wird die Standardsprache in der Rust-Sprache verwendet, es gibt beispiellose Regeln für den Zugriff auf veränderbare Daten und die Lebensdauer der Verbindungen wird ebenfalls berücksichtigt. Dies ermöglicht ihm die Gewährleistung der Speichersicherheit und erleichtert die Multithread-Programmierung aufgrund des Mangels an Datenrasen.

All dies ist bereits jedem bekannt, der die Entwicklung moderner Programmiertechnologien zumindest ein wenig verfolgt. Aber was ist, wenn Sie kein Systemprogrammierer sind und nicht viele Multithread-Codes in Ihren Projekten enthalten, Sie aber dennoch von der Leistung von Rust angezogen werden? Erhalten Sie zusätzliche Vorteile aus der Verwendung in Anwendungen? Oder alles, was er Ihnen zusätzlich geben wird, ist ein harter Kampf mit dem Compiler, der Sie dazu zwingt, das Programm so zu schreiben, dass es ständig den Regeln der Sprache für Ausleihe und Besitz folgt?
Dieser Artikel hat ein Dutzend nicht offensichtlicher und nicht besonders beworbener Vorteile der Verwendung von Rust gesammelt, die Ihnen hoffentlich bei der Entscheidung über die Wahl dieser Sprache für Ihre Projekte helfen werden.
1. Die Universalität der Sprache
Trotz der Tatsache, dass Rust als Sprache für die Systemprogrammierung positioniert ist, eignet es sich auch zur Lösung von Problemen auf hoher Ebene. Sie müssen nicht mit Rohzeigern arbeiten, es sei denn, Sie benötigen sie für Ihre Aufgabe. Die Standard-Sprachbibliothek hat bereits die meisten Typen und Funktionen implementiert, die möglicherweise für die Anwendungsentwicklung erforderlich sind. Sie können auch problemlos externe Bibliotheken verbinden und verwenden. Das Typsystem und die verallgemeinerte Programmierung in Rust ermöglichen die Verwendung von Abstraktionen auf ziemlich hohem Niveau, obwohl OOP in der Sprache nicht direkt unterstützt wird.
Schauen wir uns einige einfache Beispiele für die Verwendung von Rust an.
Ein Beispiel für die Kombination von zwei Iteratoren zu einem Iterator über Elementpaare:
let zipper: Vec<_> = (1..).zip("foo".chars()).collect(); assert_eq!((1, 'f'), zipper[0]); assert_eq!((2, 'o'), zipper[1]); assert_eq!((3, 'o'), zipper[2]);
Ausführen
Hinweis: Ein Aufruf des Formatnamens name!(...)
ist ein Aufruf eines Funktionsmakros. Die Namen solcher Makros in Rust enden immer mit einem Symbol !
damit sie von Funktionsnamen und anderen Bezeichnern unterschieden werden können. Die Vorteile der Verwendung von Makros werden unten erläutert.
Ein Beispiel für die Verwendung der externen regex
Bibliothek zum Arbeiten mit regulären Ausdrücken:
extern crate regex; use regex::Regex; let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); assert!(re.is_match("2018-12-06"));
Ausführen
Ein Beispiel für die Implementierung des Add
für die eigene Point
, um den Additionsoperator zu überladen:
use std::ops::Add; struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2;
Ausführen
Ein Beispiel für die Verwendung eines generischen Typs in einer Struktur:
struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 };
Ausführen
Auf Rust können Sie effiziente Systemdienstprogramme, große Desktopanwendungen, Microservices, Webanwendungen (einschließlich des Client-Teils, da Rust in Wasm kompiliert werden kann) und mobile Anwendungen schreiben (obwohl das Sprachökosystem in dieser Richtung noch schlecht entwickelt ist). Diese Vielseitigkeit kann für Teams mit mehreren Projekten von Vorteil sein, da Sie in vielen verschiedenen Projekten dieselben Ansätze und dieselben Module verwenden können. Wenn Sie daran gewöhnt sind, dass jedes Werkzeug für seinen engen Anwendungsbereich ausgelegt ist, versuchen Sie, Rust als Werkzeugkasten mit der gleichen Zuverlässigkeit und Bequemlichkeit zu betrachten. Vielleicht haben Sie genau das vermisst.
2. Praktische Tools für das Build- und Abhängigkeitsmanagement
Dies wird eindeutig nicht angekündigt, aber viele bemerken, dass Rust über eines der besten Build- und Abhängigkeitsmanagementsysteme verfügt, die derzeit verfügbar sind. Wenn Sie in C oder C ++ programmiert haben und die Frage der schmerzlosen Verwendung externer Bibliotheken für Sie ziemlich akut war, ist die Verwendung von Rust mit dem Build-Tool und dem Cargo-Abhängigkeits-Manager eine gute Wahl für Ihre neuen Projekte.
Neben der Tatsache, dass Cargo Abhängigkeiten für Sie herunterlädt und deren Versionen verwaltet, Ihre Anwendungen erstellt und ausführt, Tests ausführt und Dokumentation generiert, kann es auch mit Plugins für andere nützliche Funktionen erweitert werden. Beispielsweise gibt es Erweiterungen, mit denen Cargo die veralteten Abhängigkeiten Ihres Projekts ermitteln, eine statische Analyse des Quellcodes durchführen, Clientteile von Webanwendungen erstellen und erneut bereitstellen kann und vieles mehr.
Die Cargo-Konfigurationsdatei verwendet die benutzerfreundliche und minimale Toml-Markup-Sprache, um die Projekteinstellungen zu beschreiben. Hier ist ein Beispiel für eine typische Konfigurationsdatei von Cargo.toml
:
[package] name = "some_app" version = "0.1.0" authors = ["Your Name <you@example.com>"] [dependencies] regex = "1.0" chrono = "0.4" [dev-dependencies] rand = "*"
Im Folgenden sind drei typische Befehle für die Verwendung von Cargo aufgeführt:
$ cargo check $ cargo test $ cargo run
Mit ihrer Hilfe wird der Quellcode auf Kompilierungsfehler, die Zusammenstellung des Projekts und den Start von Tests sowie die Zusammenstellung und den Start des Programms zur Ausführung überprüft.
3. Eingebaute Tests
Das Schreiben von Komponententests in Rust ist so einfach und unkompliziert, dass Sie es immer wieder tun möchten. :) Oft ist es einfacher, einen Komponententest zu schreiben, als die Funktionalität auf andere Weise zu testen. Hier ist ein Beispiel für Funktionen und Tests für sie:
pub fn is_false(a: bool) -> bool { !a } pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod test { use super::*; #[test] fn is_false_works() { assert!(is_false(false)); assert!(!is_false(true)); } #[test] fn add_two_works() { assert_eq!(1, add_two(-1)); assert_eq!(2, add_two(0)); assert_eq!(4, add_two(2)); } }
Ausführen
Die Funktionen im test
, die mit dem Attribut #[test]
, sind Komponententests. Sie werden parallel ausgeführt, wenn der cargo test
aufgerufen wird. Das bedingte Kompilierungsattribut #[cfg(test)]
, das das gesamte Modul mit Tests kennzeichnet, führt dazu, dass das Modul nur kompiliert wird, wenn die Tests ausgeführt werden, aber nicht in die normale Assembly gelangt.
Es ist sehr praktisch, die Tests im selben Modul wie die zu testende Funktion zu platzieren, indem Sie einfach das test
Submodul hinzufügen. Wenn Sie Integrationstests benötigen, platzieren Sie Ihre Tests einfach im Testverzeichnis im Stammverzeichnis des Projekts und verwenden Sie Ihre Anwendung darin als externes Paket. Ein separates test
und Anweisungen zur bedingten Kompilierung müssen in diesem Fall nicht hinzugefügt werden.
Spezielle Beispiele für Dokumentationen, die als Tests ausgeführt werden, verdienen besondere Aufmerksamkeit, dies wird jedoch nachstehend erörtert.
Integrierte Leistungstests (Benchmarks) sind ebenfalls verfügbar, jedoch noch nicht stabil. Daher sind sie nur in Compiler-Night-Assemblys verfügbar. In stabilem Rust müssen Sie für diese Art von Tests externe Bibliotheken verwenden.
4. Gute Dokumentation mit aktuellen Beispielen
Die Standard-Rust-Bibliothek ist sehr gut dokumentiert. Die HTML-Dokumentation wird automatisch aus dem Quellcode mit Markdown-Beschreibungen in den Dock-Kommentaren generiert. Darüber hinaus enthalten die Dokumentkommentare im Rust-Code Beispielcode, der ausgeführt wird, wenn Tests ausgeführt werden. Dies stellt die Relevanz der Beispiele sicher:
Die Dokumentation
Hier ist ein Beispiel für die Verwendung der as_bytes
Methode vom Typ String
let s = String::from("hello"); assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());
wird beim Teststart als Test ausgeführt.
Darüber hinaus ist es für Rust-Bibliotheken üblich, Beispiele für ihre Verwendung in Form kleiner unabhängiger Programme zu erstellen, die sich im examples
im Stammverzeichnis des Projekts befinden. Diese Beispiele sind ebenfalls ein wichtiger Bestandteil der Dokumentation und werden auch während des Testlaufs kompiliert und ausgeführt. Sie können jedoch unabhängig von den Tests ausgeführt werden.
5. Intelligenter automatischer Abzug von Typen
In einem Rust-Programm können Sie den Ausdruckstyp nicht explizit angeben, wenn der Compiler ihn basierend auf dem Verwendungskontext automatisch ausgeben kann. Dies gilt nicht nur für Orte, an denen Variablen deklariert werden. Schauen wir uns ein Beispiel an:
let mut vec = Vec::new(); let text = "Message"; vec.push(text);
Ausführen
Wenn wir die Typanmerkungen anordnen, sieht dieses Beispiel folgendermaßen aus:
let mut vec: Vec<&str> = Vec::new(); let text: &str = "Message"; vec.push(text);
Das heißt, wir haben einen Vektor von String-Slices und eine Variable vom Typ String-Slice. In diesem Fall ist die Angabe von Typen jedoch völlig redundant, da der Compiler sie selbst ausgeben kann (unter Verwendung der erweiterten Version des Hindley-Milner- Algorithmus). Die Tatsache, dass vec
ein Vektor ist, wird bereits durch den Typ des Rückgabewerts von Vec::new()
, aber es ist noch nicht klar, welcher Typ seiner Elemente sein wird. Die Tatsache, dass der text
ein String-Slice ist, ist dadurch verständlich, dass ihm ein Literal dieses Typs zugewiesen ist. Somit wird nach vec.push(text)
die Art der vec.push(text)
offensichtlich. Beachten Sie, dass der Typ der vec
Variablen vollständig durch ihre Verwendung im Ausführungsthread und nicht in der Initialisierungsphase bestimmt wurde.
Ein solches System der Typinferenz eliminiert das Rauschen des Codes und macht ihn so präzise wie den Code in einer dynamisch typisierten Programmiersprache. Und das unter strikter statischer Typisierung!
Natürlich können wir das Tippen in einer statisch typisierten Sprache nicht vollständig loswerden. Das Programm muss Punkte haben, an denen die Objekttypen garantiert bekannt sind, damit diese Typen an anderen Stellen angezeigt werden können. Solche Punkte in Rust sind Deklarationen von benutzerdefinierten Datentypen und Funktionssignaturen, in denen man nur die verwendeten Typen angeben kann. Sie können jedoch mithilfe der allgemeinen Programmierung "Metavariablen von Typen" eingeben.
6. Mustervergleich an Variablendeklarationspunkten
let
Operation
let p = Point::new();
nicht wirklich darauf beschränkt, nur neue Variablen zu deklarieren. Was sie tatsächlich tut, ist, den Ausdruck rechts vom Gleichheitszeichen mit dem Muster links abzugleichen. Und neue Variablen können als Teil der Stichprobe eingeführt werden (und nur so). Schauen Sie sich das folgende Beispiel an, und es wird Ihnen klarer:
let Point { x, y } = Point::new();
Ausführen
Die Destrukturierung wird hier durchgeführt: Ein solcher Vergleich führt die Variablen x
und y
, die mit dem Wert der x
und y
Felder des Objekts der Punktstruktur initialisiert werden, der durch Aufrufen von Point::new()
. Gleichzeitig ist der Vergleich korrekt, da der Typ des Ausdrucks rechts dem Punktmuster vom Typ Point
links entspricht. Auf ähnliche Weise können Sie beispielsweise die ersten beiden Elemente eines Arrays verwenden:
let [a, b, _] = [1, 2, 3];
Und noch viel mehr zu tun. Das Bemerkenswerteste ist, dass solche Vergleiche an allen Stellen durchgeführt werden, an denen neue Variablennamen in Rust eingegeben werden können, nämlich: in der match
let
, if let
, while let
if let
, in der Kopfzeile der for
Schleife, in den Argumenten von Funktionen und Abschlüssen. Hier ist ein Beispiel für die elegante Verwendung des Pattern Matching in einer for
Schleife:
for (i, ch) in "foo".chars().enumerate() { println!("Index: {}, char: {}", i, ch); }
Ausführen
Die enumerate
, die für den Iterator aufgerufen wird, erstellt einen neuen Iterator, der nicht über die Anfangswerte, sondern über Tupel iteriert und "Ordnungsindex, Anfangswert" paart. Jedes dieser Tupel während der Iteration des Zyklus wird dem angegebenen Muster (i, ch)
, wodurch die Variable i
den ersten Wert vom Tupel erhält - den Index und die Variable ch
- den zweiten, dh das Zeichen der Zeichenfolge. Weiter im Körper der Schleife können wir diese Variablen verwenden.
Ein weiteres beliebtes Beispiel für die Verwendung eines Musters in einer for
Schleife:
for _ in 0..5 {
Hier ignorieren wir einfach den Wert des Iterators unter Verwendung des _
Musters. Weil wir die Iterationsnummer im Hauptteil der Schleife nicht verwenden. Das gleiche kann zum Beispiel mit einem Funktionsargument gemacht werden:
fn foo(a: i32, _: bool) {
Oder beim Matching in einer match
Anweisung:
match p { Point { x: 1, .. } => println!("Point with x == 1 detected"), Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"), _ => (),
Ausführen
Der Mustervergleich macht den Code sehr kompakt und ausdrucksstark und ist in der match
im Allgemeinen unersetzlich. Der match
ist ein Operator für die vollständige variable Analyse, sodass Sie nicht versehentlich vergessen können, einige der möglichen Übereinstimmungen für den darin enthaltenen analysierten Ausdruck zu überprüfen.
7. Syntaxerweiterung und benutzerdefiniertes DSL
Die Rostsyntax ist begrenzt, hauptsächlich aufgrund der Komplexität des in der Sprache verwendeten Typsystems. Beispielsweise hat Rust keine benannten Funktionsargumente oder Funktionen mit einer variablen Anzahl von Argumenten. Mit Makros können Sie diese und andere Einschränkungen umgehen. Rust hat zwei Arten von Makros: deklarative und prozedurale. Mit deklarativen Makros haben Sie nie die gleichen Probleme wie mit Makros in C, da sie hygienisch sind und nicht auf der Ebene der Textersetzung, sondern auf der Ebene der Ersetzung im abstrakten Syntaxbaum funktionieren. Mit Makros können Sie Abstraktionen auf der Ebene der Sprachsyntax erstellen. Zum Beispiel:
println!("Hello, {name}! Do you know about {}?", 42, name = "User");
Zusätzlich zu der Tatsache, dass dieses Makro die syntaktischen Funktionen zum Aufrufen der "Funktion" zum Drucken einer formatierten Zeichenfolge erweitert, wird in seiner Implementierung auch überprüft, ob die Eingabeargumente zur Kompilierungszeit und nicht zur Laufzeit mit der angegebenen Formatzeichenfolge übereinstimmen. Mithilfe von Makros können Sie eine präzise Syntax für Ihre eigenen Designanforderungen eingeben, DSL erstellen und verwenden. Hier ist ein Beispiel für die Verwendung von JavaScript-Code in einem Rust-Programm, das in Wasm kompiliert wird:
let name = "Bob"; let result = js! { var msg = "Hello from JS, " + @{name} + "!"; console.log(msg); alert(msg); return 2 + 2; }; println!("2 + 2 = {:?}", result);
Makro js!
definiert im stdweb
Paket und ermöglicht es Ihnen, vollwertigen JavaScript-Code in Ihr Programm einzubetten (mit Ausnahme von Zeichenfolgen und Operatoren in einfachen Anführungszeichen und Operatoren, die nicht mit einem Semikolon vervollständigt sind) und Objekte aus dem Rust-Code mit der Syntax @{expr}
.
Makros bieten enorme Möglichkeiten, die Syntax von Rust-Programmen an die spezifischen Aufgaben eines bestimmten Themenbereichs anzupassen. Sie sparen Zeit und Aufmerksamkeit bei der Entwicklung komplexer Anwendungen. Nicht durch Erhöhen des Laufzeitaufwands, sondern durch Erhöhen der Kompilierungszeit. :) :)
8. Automatische Generierung von abhängigem Code
Rusts prozedurale Ableitungsmakros werden häufig verwendet, um Merkmale und andere Codegenerierungen automatisch zu implementieren. Hier ist ein Beispiel:
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] struct Point { x: i32, y: i32, }
Da alle diese Typen ( Copy
, Clone
, Debug
, Default
, PartialEq
und Eq
) aus der Standardbibliothek für den i32
der i32
Struktur implementiert sind, kann ihre Implementierung automatisch für die gesamte Struktur als Ganzes angezeigt werden. Ein weiteres Beispiel:
extern crate serde_derive; extern crate serde_json; use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 };
Ausführen
Hier werden unter Verwendung der Deserialize
und Deserialize
aus der serde
Bibliothek für die Point
Struktur automatisch Methoden für deren Serialisierung und Deserialisierung generiert. Anschließend können Sie eine Instanz dieser Struktur an verschiedene Serialisierungsfunktionen übergeben, z. B. indem Sie sie in eine JSON-Zeichenfolge konvertieren.
Sie können Ihre eigenen prozeduralen Makros erstellen, die den benötigten Code generieren. Oder verwenden Sie die vielen bereits erstellten Makros anderer Entwickler. Makros ersparen dem Programmierer nicht nur das Schreiben von Boilerplate-Code, sondern haben auch den Vorteil, dass Sie nicht verschiedene Codeabschnitte in einem konsistenten Zustand halten müssen. Wenn beispielsweise der Punktstruktur ein drittes Feld z
hinzugefügt wird, müssen Sie nichts anderes tun, um die Serialisierung korrekt durchzuführen, wenn Sie ableiten verwenden. Wenn wir selbst die notwendigen Merkmale für die Serialisierung von Point
implementieren, müssen wir sicherstellen, dass diese Implementierung immer mit den neuesten Änderungen in der Struktur von Point
übereinstimmt.
9. Algebraischer Datentyp
Einfach ausgedrückt ist ein algebraischer Datentyp ein zusammengesetzter Datentyp, der eine Vereinigung von Strukturen darstellt. Formal ist es eine Typensumme von Produkttypen. In Rust wird dieser Typ mit dem Schlüsselwort enum
definiert:
enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), }
Der Typ eines bestimmten Werts einer Variablen vom Typ Message
kann nur einer der in Message
aufgelisteten Strukturtypen sein. Dies ist entweder eine einheitliche feldlose Quit
Struktur, eine der ChangeColor
oder Write
ChangeColor
mit namenlosen Feldern oder die übliche Move
Struktur. Ein traditioneller Aufzählungstyp kann als Sonderfall eines algebraischen Datentyps dargestellt werden:
enum Color { Red, Green, Blue, White, Black, Unknown, }
Mithilfe des Mustervergleichs können Sie herausfinden, welcher Typ in einem bestimmten Fall tatsächlich einen Wert angenommen hat:
let color: Color = get_color(); let text = match color { Color::Red => "Red", Color::Green => "Green", Color::Blue => "Blue", _ => "Other color", }; println!("{}", text); ... fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s), }; }
Ausführen
In Form von algebraischen Datentypen implementiert Rust so wichtige Typen wie Option
und Result
, die verwendet werden, um den fehlenden Wert bzw. das korrekte / fehlerhafte Ergebnis darzustellen. So wird Option
in der Standardbibliothek definiert:
pub enum Option<T> { None, Some(T), }
Rust hat keinen Nullwert, genau wie die nervigen Fehler eines unerwarteten Aufrufs. Stattdessen wird Option
verwendet, wenn es wirklich notwendig ist, die Möglichkeit eines fehlenden Werts anzugeben:
fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), }
Ausführen
Der algebraische Datentyp ist ein leistungsstarkes und ausdrucksstarkes Werkzeug, das die Tür zur typgesteuerten Entwicklung öffnet. Ein kompetent geschriebenes Programm in diesem Paradigma ordnet die meisten Überprüfungen der Richtigkeit seiner Arbeit dem Typensystem zu. Wenn Ihnen in der täglichen industriellen Programmierung ein wenig Haskell fehlt, kann Rust Ihr Outlet sein. :) :)
10. Einfaches Refactoring
Das in Rust entwickelte strikte statische Typsystem und der Versuch, während der Kompilierung so viele Überprüfungen wie möglich durchzuführen, führen dazu, dass das Ändern und Umgestalten des Codes recht einfach und sicher wird. Wenn das Programm nach den Änderungen kompiliert wurde, bedeutet dies, dass nur logische Fehler übrig blieben, die nicht mit der Funktionalität zusammenhängen, deren Überprüfung dem Compiler zugewiesen wurde. In Kombination mit der einfachen Hinzufügung von Komponententests zur Testlogik führt dies zu ernsthaften Garantien für die Zuverlässigkeit von Programmen und zu einer Erhöhung des Vertrauens des Programmierers in den korrekten Betrieb seines Codes nach Änderungen.
Vielleicht ist das alles, worüber ich in diesem Artikel sprechen wollte. Natürlich hat Rust viele andere Vorteile sowie eine Reihe von Nachteilen (etwas Feuchtigkeit in der Sprache, Mangel an vertrauten Programmiersprachen und „nicht literarische“ Syntax), die hier nicht erwähnt werden. Wenn Sie etwas darüber zu erzählen haben, schreiben Sie in die Kommentare. Versuchen Sie Rust im Allgemeinen in der Praxis. Und vielleicht überwiegen seine Vorteile für Sie alle seine Mängel, wie es in meinem Fall passiert ist. Und schließlich erhalten Sie genau die Werkzeuge, die Sie lange Zeit benötigt haben.