Rust-Datenmodellierung: Structs, Enums & Robustes Error Handling
Erstellt: 06.11.2026 Zuletzt aktualisiert: 07.11.2026
Executive Summary: Klassische objektorientierte Programmiersprachen stützen sich auf Vererbungshierarchien, Klassen und ungeprüfte Laufzeit-Exceptions – was Kontrollflüsse oft verschleiert und zu allgegenwärtigen
NullPointerExceptionsführt. Rust bricht radikal mit diesem Paradigma. Statt Vererbung setzt Rust auf Komposition über Vererbung auf Basis Algebraischer Datentypen (ADTs): Structs (Produkttypen), die zusammengehörige Felder mit deterministischem Speicherlayout bündeln, und Enums (Summentypen), die sich gegenseitig ausschließende Zustände mit reichhaltigen Nutzdaten (Payloads) abbilden. Zudem verbannt Rust Null-Referenzen durchOption<T>und modelliert fehlschlagbare Operationen explizit überResult<T, E>. Dieses Kapitel bildet Teil 1.2 (Grundlagen): Analyse von Struct-Varianten und Byte-Alignment, Implementierung von Methoden und Konstruktoren, meisterhafter Musterabgleich (Pattern Matching), robuste Fehlerfortpflanzung mit dem?-Operator sowie die Realisierung eines absturzsicheren Webserver-Access-Logparsers.
-
Structs & Speicherlayout
Konstruiere klassisch benannte Structs, Tupel-Structs (Newtype-Pattern) und speicherlose Unit-Structs mit optimiertem CPU-Cache-Alignment.
-
Methoden &
impl-Blöcke
Implementiere Instanzmethoden mit
&self,&mut selfund konsumierendemselfsowie assoziierte Konstruktor-Funktionen (Self::new). -
Algebraische Enums & Pattern Matching
Modelliere komplexe Zustandsautomaten mit nutzlasttragenden Summentypen, erschöpfenden
match-Ausdrücken, Match Guards und modernerlet else-Syntax. -
Option<T>,Result<T, E>&?
Eliminiere Null-Pointer-Bugs für immer, trenne behebbare Fehler von unrettbaren Panics und leite Fehler elegant mit dem
?-Operator weiter.
1. Structs: Produkttypen im Systementwurf
In der Typentheorie ist ein Produkttyp ein zusammengesetzter Datentyp, dessen Zustandsraum dem kartesischen Produkt seiner Einzelfelder entspricht. In Rust werden Produkttypen über struct realisiert. Die Sprache bietet drei Struct-Varianten für unterschiedliche Architekturmuster:
- Klassisch benannte Structs: Explizite Feldnamen für maximale Lesbarkeit und selbstdokumentierende Schnittstellen.
- Tupel-Structs: Positionsbasierte Felder ohne Namen, ideal für schlanke Wrapper und das typsichere Newtype-Pattern.
- Unit-Like Structs: Structs gänzlich ohne Felder, die 0 Byte Speicher auf dem Stack belegen (Zero-Sized Types / ZST), prädestiniert für Trait-Marker und Compile-Time-Zustandsmaschinen.
// 1. Klassisch benanntes Struct
#[derive(Debug, Clone, PartialEq)]
pub struct NetworkInterface {
pub name: String,
pub mtu: u32,
pub is_up: bool,
}
// 2. Tupel-Struct (Newtype-Pattern für strikte Domain-Typsicherheit)
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Port(pub u16);
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Megabytes(pub f64);
// 3. Unit-Like Struct (Zero-Sized Type: belegt exakt 0 Byte im RAM!)
#[derive(Debug, Copy, Clone)]
pub struct PromiscuousModeMarker;
Speicher-Alignment und Struct-Padding
Standardmäßig verwendet der Rust-Compiler (rustc) das Repräsentationsmodell repr(Rust). Im Gegensatz zu C (wo Felder strikt in Deklarationsreihenfolge im Speicher liegen) sortiert rustc Struct-Felder automatisch um, um CPU-Padding-Lücken zu minimieren und Speicherzugriffe optimal an Cache-Lines auszurichten.
Unoptimierte C-Deklarationsreihenfolge (40 Bytes Gesamtgröße durch Padding):
[ u8: 1 Byte ] [ 7 Bytes Padding ] [ u64: 8 Bytes ] [ bool: 1 Byte ] [ 7 Bytes Padding ] [ String: 24 Bytes ]
Optimierte rustc-Feldanordnung (32 Bytes Gesamtgröße):
[ String: 24 Bytes ] [ u64: 8 Bytes ] [ u8: 1 Byte ] [ bool: 1 Byte ] [ 6 Bytes trailing Padding ]
[!TIP] Für direkte C-FFI-Kompatibilität oder speicherabgebildete Hardware-Register (MMIO) erzwingt
#[repr(C)]oder#[repr(packed)]eine feste, nicht umgeordnete Bytedarstellung exakt nach C-Standard.
2. Methoden und assoziierte Funktionen (impl)
In Rust sind Datenstruktur und Verhalten strikt entkoppelt. Methoden und Konstruktoren werden innerhalb von impl-Blöcken definiert.
Die Empfänger-Semantik: &self, &mut self und self
Der erste Parameter einer Funktionssignatur bestimmt die Eigentums- und Zugriffsrechte:
| Empfänger-Signatur | Ownership- / Borrow-Semantik | Typischer Anwendungsfall |
+| :--- | :--- | :--- |
| &self | Geteilte Lese-Referenz: Beliebig viele parallele Aufrufer erlaubt. | Getter, Statusabfragen, Validierung, Serialisierung. |
| &mut self | Exklusive Schreib-Referenz: Genau ein exklusiver Aufrufer hat Schreibzugriff. | Modifikation interner Puffer, Zähler, Zustandsänderungen. |
| self | Ownership-Transfer (Konsumierung): Das Ursprungsobjekt wird zerstört/übertragen. | Builder-Pattern-Finalizer, Zustandsübergänge (State Machine). |
pub struct LinuxService {
name: String,
pid: Option<u32>,
restart_count: u32,
}
impl LinuxService {
/// Assoziierte Konstruktor-Funktion (Kein self-Parameter, Aufruf via LinuxService::new)
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
pid: None,
restart_count: 0,
}
}
/// Shared Borrow (&self): Reine Leseabfrage
pub fn status(&self) -> (&str, bool) {
(&self.name, self.pid.is_some())
}
/// Mutable Borrow (&mut self): Verändert den internen Zustand
pub fn start(&mut self, assigned_pid: u32) {
self.pid = Some(assigned_pid);
self.restart_count += 1;
}
/// Konsumierender Empfänger (self): Wandelt das Objekt um und zerstört das Original
pub fn decommission(self) -> String {
format!("Dienst '{}' wurde endgültig außer Betrieb genommen.", self.name)
// 'self' wird hier am Ende des Gültigkeitsbereichs via Drop deallokiert
}
}
3. Algebraische Enums: Modellierung exklusiver Zustände
In traditionellen Programmiersprachen sind Enums meist bloße Integer-Konstanten. In Rust sind Enums vollwertige Summentypen (Tagged Unions / Discriminated Unions). Jede Variante kann völlig eigenständige Datentypen als Nutzlast tragen.
#[derive(Debug, Clone)]
pub enum NetworkEvent {
/// Unit-Variante: Keine Nutzdaten
LinkDown,
/// Tupel-Variante: Trägt IP-Adresse und Port
ConnectionEstablished(std::net::IpAddr, u16),
/// Struct-Variante: Benannte Felder für komplexe Telemetrie
PacketReceived {
interface: String,
bytes_transferred: usize,
checksum_valid: bool,
},
/// Gekapselter Fehlerzustand
FatalError(String),
}
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph DataModeling["Rust Algebraische Datentypen (ADT)"]
direction TB
subgraph ProductType["Produkttyp: Struct"]
direction TB
ST1["struct ServerConfig<br/>Alle Felder existieren zeitgleich"]
ST2["host: String (24 Byte auf Stack)<br/>port: u16 (2 Byte + 6B Padding)<br/>tls: bool (1 Byte + 7B Padding)"]
ST3["Gesamtspeichergröße =<br/>Summe der Felder + Padding (32 Byte)"]
ST1 --> ST2 --> ST3
end
subgraph SumType["Summentyp: Enum"]
direction TB
EN1["enum NetworkEvent<br/>Exakt EINE Variante ist aktiv"]
EN2["LinkDown: Unit<br/>ConnectionEstablished: Tupel<br/>PacketReceived: Struct-Payload"]
EN3["Total Memory Size =<br/>Tag-Diskriminante (1-8B) +<br/>Größte Varianten-Nutzlast + Padding"]
EN1 --> EN2 --> EN3
end
end
Niche Filling und Null Pointer Optimization (NPO)
Eine herausragende Compiler-Optimierung von rustc ist die Null Pointer Optimization (NPO). Für Typen, bei denen Bitmuster 0x0 ungültig ist (z. B. Referenzen &T, Box<T> oder std::num::NonZeroU64), nutzt der Compiler den Null-Pointer 0x0, um die None-Variante von Option<T> darzustellen.
use std::mem::size_of;
// Beide belegen auf einer 64-Bit-Architektur exakt 16 Byte!
assert_eq!(size_of::<&str>(), 16);
assert_eq!(size_of::<Option<&str>>(), 16); // Null Overhead! Kein zusätzliches Tag-Byte!
4. Pattern Matching: Präziser Kontrollfluss
Rusts match-Ausdruck erzwingt zur Compile-Zeit Vollständigkeit (Exhaustiveness): Jeder denkbare Fall muss behandelt werden, wodurch unvorhergesehene Randfälle mathematisch unmöglich werden.
Vollständiges match, Guards und Destrukturierung
fn process_event(event: &NetworkEvent) -> String {
match event {
NetworkEvent::LinkDown => "Physische Netzwerkverbindung unterbrochen.".to_string(),
NetworkEvent::ConnectionEstablished(ip, port) if ip.is_loopback() => {
format!("Lokale Loopback-Verbindung auf Port {port}")
}
NetworkEvent::ConnectionEstablished(ip, port) => {
format!("Externer Peer verbunden: {ip}:{port}")
}
NetworkEvent::PacketReceived { bytes_transferred, checksum_valid: true, .. } => {
format!("{bytes_transferred} authentifizierte Bytes verarbeitet.")
}
NetworkEvent::PacketReceived { checksum_valid: false, .. } => {
"Beschädigter Frame erkannt; Paket verworfen.".to_string()
}
NetworkEvent::FatalError(err) => format!("Kernel-Panic-Alarm: {err}"),
}
}
Ergonomische Syntax: if let, while let und let else
Wenn nur eine einzelne Variante von Interesse ist, bietet Rust schlanke Konstrukte:
let raw_config: Option<u16> = Some(8080);
// 1. if let: Ausführung nur bei Übereinstimmung
if let Some(port) = raw_config {
println!("Dienst wird an Port {port} gebunden");
}
// 2. let else (Rust 1.65+): Idiomatischer Guard mit zwingendem Abbruch
fn parse_port_or_bail(opt: Option<u16>) -> u16 {
let Some(port) = opt else {
// Der else-Block MUSS divergieren (return, break, continue oder panic!)
println!("Port-Konfiguration fehlt; verwende Standardwert 80");
return 80;
};
port
}
5. Beseitigung von Null: Das Option<T>-Enum
Der britische Informatiker Sir Tony Hoare bezeichnete seine Erfindung der null-Referenz im Jahr 1965 rückblickend als seinen „Billion-Dollar Mistake“. Rust verhindert Null-Pointer-Dereferenzierungsfehler, indem es schlicht kein null-Schlüsselwort besitzt. Abwesende Werte werden explizit über Option<T> modelliert:
Funktionale Kombinatoren: Schluss mit unwrap()
In Produktionscode sollte der direkte Aufruf von .unwrap() vermieden werden. Stattdessen verketten wir funktionale Kombinatoren:
let raw_timeout: Option<&str> = Some("4500");
// Wandle Some(&str) -> Some(u64) -> addiere 500ms Puffer
let calculated_timeout: u64 = raw_timeout
.and_then(|s| s.parse::<u64>().ok()) // Parst String zu Result, konvertiert zu Option
.map(|ms| ms + 500) // Transformiert inneren Wert, falls Some
.unwrap_or(3000); // Fallback-Standardwert, falls None
assert_eq!(calculated_timeout, 5000);
6. Robuste Fehlerarchitektur: Result<T, E> und ?
Rust unterscheidet Laufzeitprobleme strikt in zwei Kategorien:
- Nicht behebbare Fehler (
panic!): Invarianten-Verletzungen, Array-Zugriffe außerhalb der Grenzen oder interne Softwarefehler. Löst Stack-Unwinding (oder sofortigen Abbruch) aus und bereinigt den Speicher deterministisch via RAIIDrop. - Behebbare Fehler (
Result<T, E>): Vorhersehbare betriebliche Ereignisse (Netzwerk-Timeouts, fehlende Dateien, ungültige Benutzereingaben, fehlerhafte Protokoll-Header).
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph ErrorHandling["Rust Duale Fehlerarchitektur"]
direction TB
E1["Operation oder Syscall-Ausführung"]
E1 --> C1{"Fehler aufgetreten?"}
C1 -- Nein --> S1["Ok(T) oder Some(T)<br/>Reguläre Pipeline wird fortgesetzt"]
C1 -- Ja --> C2{"Behebbarer Fehler?"}
C2 -- Ja --> R1["Result::Err(E)<br/>Expliziter Fehlerwert"]
R1 --> R2["Weiterleitung via ?-Operator<br/>Musterabgleich oder lokale Reparatur"]
R2 --> R3["Graceful Degradation oder<br/>Strukturierter Logeintrag"]
C2 -- Nein --> U1["Unrettbarer Systemfehler<br/>Bug, Out of Bounds, Invariantenbruch"]
U1 --> U2["panic! wird ausgelöst"]
U2 --> U3["Stack Unwinding & RAII Drop<br/>oder Abbruch ohne Speicherkorruption"]
end
Der ?-Operator (Try Operator)
Der ?-Operator entpackt Ok(T) im Erfolgsfall oder führt ein sofortiges return Err(From::from(e)) aus der aufrufenden Funktion durch:
use std::fs::File;
use std::io::{self, Read};
fn read_kernel_hostname() -> Result<String, io::Error> {
let mut file = File::open("/proc/sys/kernel/hostname")?; // Gibt io::Error bei Fehlschlag zurück
let mut hostname = String::new();
file.read_to_string(&mut hostname)?; // Gibt io::Error bei Fehlschlag zurück
Ok(hostname.trim().to_string())
}
7. Entwurf professioneller Domain-Fehlertypen
In professionellen Systemen sind reine Textfehler (Err("failed")) ein Anti-Pattern. Stabile Software erfordert stark typisierte Fehler-Enums:
use std::fmt;
use std::error::Error;
#[derive(Debug, PartialEq, Eq)]
pub enum ServerConfigError {
IoFailure(String),
InvalidPort { requested: u32, max: u16 },
MissingMandatoryKey(&'static str),
}
impl fmt::Display for ServerConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IoFailure(msg) => write!(f, "I/O-Subsystemfehler: {msg}"),
Self::InvalidPort { requested, max } => {
write!(f, "Port {requested} überschreitet erlaubtes Maximum {max}")
}
Self::MissingMandatoryKey(key) => write!(f, "Erforderlicher Konfigurationsschlüssel fehlt: {key}"),
}
}
}
impl Error for ServerConfigError {}
[!NOTE] In größeren Cargo-Projekten automatisiert das Standard-Crate
thiserrordie Implementierung vonDisplayundErrorüber prozedurale Makros (#[derive(Error)]), währendanyhowin Anwendungs-Binaries für unkomplizierte Fehleraggregation mit Kontext-Traces genutzt wird.
8. Hands-On Systems Lab: web_log_parser.rs
Wir implementieren einen produktionsreifen, absturzsicheren Apache/Nginx Webserver-Logparser. Er nutzt klassische Structs, Summentyp-Enums, eigene Domain-Fehler und lückenloses Pattern Matching.
use std::fmt;
use std::error::Error;
use std::str::FromStr;
/// HTTP-Anfragemethoden (Summentyp)
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
impl FromStr for HttpMethod {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GET" => Ok(HttpMethod::Get),
"POST" => Ok(HttpMethod::Post),
"PUT" => Ok(HttpMethod::Put),
"DELETE" => Ok(HttpMethod::Delete),
"PATCH" => Ok(HttpMethod::Patch),
"HEAD" => Ok(HttpMethod::Head),
"OPTIONS" => Ok(HttpMethod::Options),
other => Err(ParseError::UnknownHttpMethod(other.to_string())),
}
}
}
/// HTTP-Status-Kategorien
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HttpStatusCategory {
Success, // 2xx
Redirection, // 3xx
ClientError, // 4xx
ServerError, // 5xx
Unknown,
}
/// Strukturierter Web-Logdatensatz (Produkttyp)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
pub client_ip: String,
pub timestamp: String,
pub method: HttpMethod,
pub path: String,
pub status_code: u16,
pub response_bytes: u64,
}
impl LogRecord {
/// Bestimmt Statuskategorie über Bereichsabgleich (Range Matching)
pub fn status_category(&self) -> HttpStatusCategory {
match self.status_code {
200..=299 => HttpStatusCategory::Success,
300..=399 => HttpStatusCategory::Redirection,
400..=499 => HttpStatusCategory::ClientError,
500..=599 => HttpStatusCategory::ServerError,
_ => HttpStatusCategory::Unknown,
}
}
}
/// Stark typisierter Domain-Fehler
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
EmptyLine,
InsufficientFields { found: usize, expected: usize },
MalformedRequestSection,
UnknownHttpMethod(String),
InvalidStatusCode(String),
InvalidByteCount(String),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyLine => write!(f, "Logzeile war leer oder enthielt nur Whitespace"),
Self::InsufficientFields { found, expected } => {
write!(f, "Unzureichende Spaltenanzahl: {found} gefunden, mindestens {expected} erwartet")
}
Self::MalformedRequestSection => write!(f, "Format des HTTP-Anfrageabschnitts ist ungültig"),
Self::UnknownHttpMethod(m) => write!(f, "Unbekannte HTTP-Methode: '{m}'"),
Self::InvalidStatusCode(s) => write!(f, "HTTP-Statuscode kann nicht als Zahl geparst werden: '{s}'"),
Self::InvalidByteCount(b) => write!(f, "Byte-Größe der Antwort kann nicht geparst werden: '{b}'"),
}
}
}
impl Error for ParseError {}
/// Reiner Parser für kombinierte Apache/Nginx-Logzeilen
pub fn parse_log_line(line: &str) -> Result<LogRecord, ParseError> {
let trimmed = line.trim();
if trimmed.is_empty() {
return Err(ParseError::EmptyLine);
}
// Anführungszeichen trennen Prefix, HTTP-Request und Suffix
let parts: Vec<&str> = trimmed.split('"').collect();
if parts.len() < 3 {
return Err(ParseError::MalformedRequestSection);
}
// 1. Client-IP und Timestamp aus Prefix extrahieren
let prefix_tokens: Vec<&str> = parts[0].split_whitespace().collect();
if prefix_tokens.is_empty() {
return Err(ParseError::InsufficientFields { found: 0, expected: 1 });
}
let client_ip = prefix_tokens[0].to_string();
let timestamp = parts[0]
.find('[')
.and_then(|start| parts[0].find(']').map(|end| &parts[0][start + 1..end]))
.unwrap_or("UNBEKANNTER_ZEITSTEMPEL")
.to_string();
// 2. Request-Abschnitt parsen: "GET /api/v1/telemetry HTTP/1.1"
let request_tokens: Vec<&str> = parts[1].split_whitespace().collect();
if request_tokens.len() < 2 {
return Err(ParseError::MalformedRequestSection);
}
let method = HttpMethod::from_str(request_tokens[0])?;
let path = request_tokens[1].to_string();
// 3. Statuscode und Bytegröße aus Suffix parsen
let suffix_tokens: Vec<&str> = parts[2].split_whitespace().collect();
if suffix_tokens.len() < 2 {
return Err(ParseError::InsufficientFields {
found: suffix_tokens.len(),
expected: 2,
});
}
let status_code = suffix_tokens[0]
.parse::<u16>()
.map_err(|_| ParseError::InvalidStatusCode(suffix_tokens[0].to_string()))?;
let response_bytes = match suffix_tokens[1] {
"-" => 0, // HTTP 304 oder leerer Antwortkörper
raw => raw
.parse::<u64>()
.map_err(|_| ParseError::InvalidByteCount(raw.to_string()))?,
};
Ok(LogRecord {
client_ip,
timestamp,
method,
path,
status_code,
response_bytes,
})
}
fn main() {
println!("=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITEKTUR) ===\n");
let raw_logs = [
r#"192.168.1.105 - - [07/Nov/2026:12:30:45 +0000] "GET /api/v1/telemetry HTTP/1.1" 200 4826"#,
r#"10.0.0.12 - - [07/Nov/2026:12:30:46 +0000] "POST /api/v1/auth HTTP/1.1" 401 128"#,
r#"172.16.0.5 - - [07/Nov/2026:12:30:47 +0000] "DELETE /admin/config HTTP/1.1" 403 89"#,
r#"192.168.1.200 - - [07/Nov/2026:12:30:48 +0000] "GET /static/bundle.js HTTP/1.1" 304 -"#,
r#"10.0.0.99 - - [07/Nov/2026:12:30:49 +0000] "PROPFIND /webdav/docs HTTP/1.1" 405 210"#,
r#"192.168.1.105 - - [07/Nov/2026:12:30:50 +0000] "GET /api/v2/metrics HTTP/1.1" 500 512"#,
r#"FEHLERHAFTE_ZEILE_OHNE_ANFUEHRUNGSZEICHEN"#,
];
let mut total_requests = 0;
let mut total_bytes = 0;
let mut client_errors = 0;
let mut server_errors = 0;
let mut rejected_lines = 0;
for (idx, line) in raw_logs.iter().enumerate() {
match parse_log_line(line) {
Ok(record) => {
total_requests += 1;
total_bytes += record.response_bytes;
match record.status_category() {
HttpStatusCategory::ClientError => client_errors += 1,
HttpStatusCategory::ServerError => server_errors += 1,
_ => {}
}
println!(
"[{:>2}] OK | {:<6?} | {:<3} ({:<11?}) | {:<20} | {:>5} B",
idx + 1,
record.method,
record.status_code,
record.status_category(),
record.path,
record.response_bytes
);
}
Err(err) => {
rejected_lines += 1;
eprintln!("[{:>2}] ERR | Zeile abgewiesen: {err}", idx + 1);
}
}
}
println!("\n--- TELEMETRIE-AGGREGATIONS-ZUSAMMENFASSUNG ---");
println!("Gültige Anfragen gesamt: {total_requests}");
println!("Gesamtes Datenvolumen: {total_bytes} Bytes");
println!("Client 4xx-Fehler: {client_errors}");
println!("Server 5xx-Fehler: {server_errors}");
println!("Verworfene Zeilen: {rejected_lines}");
}
Kompilierung & Ausführung des Labs
Kompiliere und starte das eigenständige Labor direkt mit rustc:
Beispielhafte Terminal-Ausgabe:
=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITEKTUR) ===
[ 1] OK | Get | 200 (Success ) | /api/v1/telemetry | 4826 B
[ 2] OK | Post | 401 (ClientError) | /api/v1/auth | 128 B
[ 3] OK | Delete | 403 (ClientError) | /admin/config | 89 B
[ 4] OK | Get | 304 (Redirection) | /static/bundle.js | 0 B
[ 5] ERR | Zeile abgewiesen: Unbekannte HTTP-Methode: 'PROPFIND'
[ 6] OK | Get | 500 (ServerError) | /api/v2/metrics | 512 B
[ 7] ERR | Zeile abgewiesen: Format des HTTP-Anfrageabschnitts ist ungültig
--- TELEMETRIE-AGGREGATIONS-ZUSAMMENFASSUNG ---
Gültige Anfragen gesamt: 5
Gesamtes Datenvolumen: 5555 Bytes
Client 4xx-Fehler: 2
Server 5xx-Fehler: 1
Verworfene Zeilen: 2
Nächste Schritte
Nachdem zusammengesetzte Typen, algebraische Enums und das Fehlermodell gefestigt sind, widmen wir uns im nächsten Kapitel dem dynamischen Speicher, zusammenhängenden Heap-Strukturen und UTF-8-Zeichenketten:
-
Teil 2.1: Collections, Strings & Slices
Meistere
Vec<T>, String-Slices&str, Heap-Reallokationskosten,BTreeMapvs.HashMapund Zero-Cost-Iterator-Pipelines.