char in Rust ist nicht der char aus C oder C++. Während dort char ein 1-Byte-Integer ist, ist Rust's char ein 4-Byte Unicode-Scalar-Value — ein einzelnes Zeichen im Sinne von Unicode, das Emojis, CJK-Schriften und beliebige andere Code-Points darstellen kann. Dieser Artikel klärt, was ein Unicode-Scalar genau ist, wie sich char von u8-Bytes in UTF-8-Strings unterscheidet, welche Methoden auf char verfügbar sind und wo die typischen Fallen liegen.
Was char ist
Ein char repräsentiert genau einen Unicode-Scalar-Value. Das ist eine 21-Bit-Zahl im Bereich 0x0000 bis 0x10FFFF, außer den Surrogate-Code-Points (0xD800 bis 0xDFFF, die nur für UTF-16-Encoding intern verwendet werden).
Speichermäßig belegt char 4 Bytes (32 Bit) — auch wenn nur 21 Bit benötigt werden. Der Compiler nutzt die unbenutzten Bit-Pattern für Niche-Optimization (etwa bei Option<char>).
fn main() {
let a: char = 'A';
let b: char = 'ß';
let c: char = '中';
let d: char = '🦀';
let e: char = '\n'; // Newline
let f: char = '\u{1F980}'; // 🦀 als Escape
println!("{a} {b} {c} {d}");
println!("Size of char: {}", std::mem::size_of::<char>()); // 4
}'A' ist ein einzelnes Anführungszeichen-Paar — Rust unterscheidet streng zwischen char ('...') und String ("...").
char vs. Byte vs. UTF-8
Der häufigste Fehler bei Einsteigern: char mit Byte zu verwechseln.
let c: char = 'A';
let b: u8 = b'A'; // Byte-Literal, nicht char
println!("size_of::<char>: {}", std::mem::size_of::<char>()); // 4
println!("size_of::<u8>: {}", std::mem::size_of::<u8>()); // 1
// Konvertierung
let als_u32: u32 = 'A' as u32; // 65
let zurueck = char::from_u32(65); // Some('A')In einem UTF-8-String belegt ein Zeichen 1 bis 4 Bytes — ein char ist die dekodierte Form, nicht die kodierte. '🦀' ist als char 4 Bytes (Unicode-Scalar 0x1F980), in UTF-8 dargestellt sind es 4 Bytes (0xF0 0x9F 0xA6 0x80).
Mehr zur Unterscheidung im Strings-Kapitel.
Escape-Sequenzen
In char- und String-Literalen kennt Rust folgende Escapes:
| Escape | Bedeutung |
|---|---|
\n | Newline (LF) |
\r | Carriage Return |
\t | Tab |
\\ | Backslash |
\' | Apostroph (in char-Literal) |
\" | Doppelte Anführung (in String) |
\0 | NULL |
\x7F | ASCII-Byte als Hex (max 0x7F) |
\u{1F980} | Unicode-Scalar als Hex (bis 6 Hex-Ziffern) |
let nl = '\n';
let backslash = '\\';
let apostroph = '\'';
let null = '\0';
let ascii = '\x41'; // 'A'
let unicode = '\u{2764}'; // ❤\x erlaubt nur 1-Byte-ASCII (max \x7F). Für höhere Zeichen brauchst du \u{...}.
char-Methoden
Die Standard-Bibliothek bietet eine reiche Auswahl:
'A'.is_alphabetic(); // true
'5'.is_numeric(); // true
'A'.is_ascii(); // true
'ß'.is_ascii(); // false
'A'.is_ascii_alphabetic(); // true
'A'.is_uppercase(); // true
'a'.is_lowercase(); // true
' '.is_whitespace(); // true
'5'.is_ascii_hexdigit(); // true
'A'.is_ascii_hexdigit(); // true (A-F)
'G'.is_ascii_hexdigit(); // false
'A'.is_control(); // false
'\n'.is_control(); // true'A'.to_lowercase().to_string(); // "a"
'a'.to_uppercase().to_string(); // "A"
'ß'.to_uppercase().to_string(); // "SS" — sz-Sonderfall
'5'.to_digit(10); // Some(5)
'a'.to_digit(16); // Some(10) — Hex
'Z'.to_digit(36); // Some(35)to_lowercase und to_uppercase geben einen Iterator zurück, nicht direkt einen char. Der Grund: manche Zeichen ändern beim Case-Switch ihre Länge ('ß' → "SS"). Mit to_string() oder collect() fasst man den Iterator zusammen.
Iteration über String-chars
In Rust ist ein String kein Array von chars, sondern eine UTF-8-Byte-Sequenz. Um über die Zeichen zu iterieren, gibt es chars():
fn main() {
let s = "Hallo, 🦀!";
for c in s.chars() {
println!("{c}");
}
// H, a, l, l, o, ,, ' ', 🦀, !
let count = s.chars().count(); // 9 — Anzahl Zeichen
let bytes = s.len(); // 12 — Anzahl Bytes
// Index-Zugriff auf chars geht NICHT direkt:
// let dritter = s[2]; // Fehler — s ist UTF-8, kein char-Array
let dritter = s.chars().nth(2); // Some('l')
}Wichtig: s.len() zählt Bytes, nicht chars. Für die Anzahl Zeichen gibt es chars().count() — was eine ganze Iteration kostet, aber notwendig ist (denn UTF-8 hat variable Byte-Längen pro Zeichen).
Der Strings-Artikel im nächsten Kapitel geht darauf im Detail ein.
Konvertierung char ↔ u32
let c = 'A';
let n: u32 = c as u32; // 65 — direkt mit as
let n2 = u32::from(c); // 65 — über From-Trait
// Auch zu kleineren Integer-Typen, aber gefährlich:
let small: u8 = '🦀' as u8; // truncated! 0x80 (nicht 0x1F980)as u8 mit einem char außerhalb des ASCII-Bereichs schneidet ab. Wer ASCII-spezifische Operationen macht, sollte vorher is_ascii() prüfen.
u32 zu char
let n: u32 = 65;
let c = char::from_u32(n); // Some('A')
let c2 = char::from_u32(0xD800); // None — Surrogate-Range
let c3 = char::from_u32(0x110000); // None — über Unicode-Maximum
let safe = char::from_u32(n).unwrap_or('?');char::from_u32 ist fallible — gibt Option<char> zurück, weil nicht jede u32 ein gültiger Unicode-Scalar ist (Surrogates und Werte über 0x10FFFF sind verboten).
Wer sicher ist, dass der Wert gültig ist: char::from_u32(n).unwrap() oder die unsichere Variante unsafe { char::from_u32_unchecked(n) } (nur in heißen Pfaden, mit Borgfalt).
Vergleiche und Ordnung
char implementiert sowohl Eq als auch Ord — Vergleiche funktionieren über den numerischen Unicode-Code-Point:
println!("{}", 'a' == 'a'); // true
println!("{}", 'a' < 'b'); // true (Code-Point 97 < 98)
println!("{}", 'A' < 'a'); // true (65 < 97)
println!("{}", 'A' < 'ß'); // true (65 < 223)Die Sortierung erfolgt also nach Unicode-Code-Point, nicht nach „natürlichem" Alphabet. 'ä' (228) sortiert nach 'z' (122), nicht zwischen 'a' und 'b'. Wer locale-bewusst sortieren will, braucht eine Crate wie unicode-segmentation oder icu.
char in Patterns
Patterns mit char-Bereichen sind sehr nützlich:
fn klassifiziere(c: char) -> &'static str {
match c {
'0'..='9' => "Ziffer",
'a'..='z' => "Kleinbuchstabe ASCII",
'A'..='Z' => "Großbuchstabe ASCII",
' ' | '\t' | '\n' => "Whitespace",
_ => "Anderes",
}
}'0'..='9' ist inklusiver Range — alle char-Werte zwischen '0' und '9' (inclusive). Sehr kompakt für Klassifikationen.
Praxis: char-Manipulation in echtem Code
Slug aus Überschrift generieren
URL-Slugs aus Artikel-Titeln zu erzeugen ist Standard in jedem Blog, CMS oder Wiki:
fn slugify(titel: &str) -> String {
let mut slug = String::with_capacity(titel.len());
let mut last_dash = true; // Doppelt-Bindestriche vermeiden
for c in titel.chars() {
let umgewandelt = c.to_ascii_lowercase();
if umgewandelt.is_ascii_alphanumeric() {
slug.push(umgewandelt);
last_dash = false;
} else if !last_dash {
slug.push('-');
last_dash = true;
}
}
// Trailing-Bindestrich entfernen
if slug.ends_with('-') { slug.pop(); }
slug
}
fn main() {
assert_eq!(slugify("Hello, World!"), "hello-world");
assert_eq!(slugify(" Rust ist cool "), "rust-ist-cool");
}Beachte: to_ascii_lowercase() (direkt einen char zurück) — nicht to_lowercase() (gibt einen Iterator zurück). Für reines ASCII reicht die einfachere Variante.
Passwort-Stärke prüfen
#[derive(Debug)]
struct Staerke {
lang_genug: bool,
hat_ziffer: bool,
hat_grossbuchstabe: bool,
hat_sonderzeichen: bool,
}
fn pruefe_passwort(pw: &str) -> Staerke {
let mut s = Staerke {
lang_genug: pw.chars().count() >= 12,
hat_ziffer: false,
hat_grossbuchstabe: false,
hat_sonderzeichen: false,
};
for c in pw.chars() {
if c.is_ascii_digit() { s.hat_ziffer = true; }
if c.is_uppercase() { s.hat_grossbuchstabe = true; }
if !c.is_alphanumeric() { s.hat_sonderzeichen = true; }
}
s
}chars().count() für Zeichen-Anzahl — pw.len() würde Bytes zählen, was bei Umlauten oder Emojis irreführend wäre. Ein Passwort wie "sehr äußert sicher!" hat 19 chars aber 22 Bytes.
Tokenizer-Klassifikation
Beim Schreiben eines Calc-/Expression-Parsers oder Tokenizers ist char-Pattern-Matching das zentrale Werkzeug:
#[derive(Debug, PartialEq)]
enum Kind { Digit, Letter, Op, Ws, Other }
fn klassifiziere(c: char) -> Kind {
match c {
'0'..='9' => Kind::Digit,
'a'..='z' | 'A'..='Z' | '_' => Kind::Letter,
'+' | '-' | '*' | '/' | '=' => Kind::Op,
c if c.is_whitespace() => Kind::Ws,
_ => Kind::Other,
}
}
fn main() {
for c in "x = 42 + 7".chars() {
println!("{c:?} -> {:?}", klassifiziere(c));
}
}Range-Patterns auf chars ('0'..='9') und Guard-Patterns (c if c.is_whitespace()) zusammen — das ist eines der schönsten Pattern-Matching-Beispiele in Rust.
Besonderheiten
char ist NICHT ein UTF-8-Byte.
Häufigste Verwechslung: in C ist char ein 8-Bit-Integer. In Rust ist char ein 32-Bit-Unicode-Scalar. Wer durch einen UTF-8-String iterieren will, hat zwei Optionen: s.bytes() für die rohen Bytes (u8) oder s.chars() für die dekodierten Zeichen (char). Beides ist legitim — je nachdem, was man will.
Strings und chars haben unterschiedliche Längen.
"🦀".len() ist 4 (Bytes), "🦀".chars().count() ist 1. Manche Sprachen lassen das verschwimmen — Rust trennt es streng. Wer die „sichtbare Zeichen-Anzahl" will (Grapheme), braucht die Crate unicode-segmentation, weil selbst chars nicht ausreichen: ein Emoji mit Hautfarben-Modifier ist ein Grapheme, aber zwei chars.
Kein Index-Zugriff auf Strings — auch nicht für chars.
s[2] ist ein Compile-Fehler. Der Grund: UTF-8 hat variable Byte-Längen, also könnte [2] mitten in einem Multi-Byte-Zeichen landen. Wer wirklich das n-te Zeichen will: s.chars().nth(n) — Kostenpunkt O(n). Daher: wenn man oft auf Zeichen-Indices zugreifen muss, lieber das Ganze einmal in Vec<char> konvertieren.
to_lowercase liefert einen Iterator, keinen char.
Weil manche Zeichen beim Case-Switch ihre Länge ändern. 'ß'.to_uppercase() liefert einen Iterator über 'S', 'S'. Mit to_string() oder collect::<String>() fasst man die Iteration zu einem String zusammen. Pragmatisch: für rein ASCII-Daten gibt es to_ascii_lowercase() / to_ascii_uppercase(), die direkt einen char zurückgeben.
char::from_u32 ist fallible — und sollte es sein.
Surrogate-Code-Points (0xD800-0xDFFF) sind reserviert für UTF-16-Encoding und dürfen nicht als Unicode-Scalar erscheinen. Werte über 0x10FFFF existieren in Unicode nicht. Beides macht from_u32 zu einer fallible Konvertierung. Wer das umgeht (z. B. mit unsafe from_u32_unchecked), riskiert Undefined Behavior.
Char-Vergleich ist Code-Point-Vergleich.
'ä' > 'z' ist true, weil Code-Point 228 > 122. Für „natürliche" Sortierung (locale-aware) brauchst du externe Crates. unicode-collation oder icu_collator machen das richtig — gerade wichtig für UI-Anzeigen mit Benutzer-Eingaben.
char hat eine Niche, die Option auf 4 Bytes hält.
Da nicht alle 32-Bit-Pattern legale chars sind (Surrogates verboten, Werte ab 0x110000 verboten), kann der Compiler None von Option<char> in einem nicht-legalen Bit-Pattern verstecken. Option<char> belegt deshalb genau 4 Bytes — nicht 8.
Char-Ranges in Patterns sind inklusiv.
'a'..='z' matcht alle chars von 'a' bis 'z' einschließlich. Die exklusive Form 'a'..'z' existiert in Patterns nicht — nur in Ranges für Iteratoren. Wer alle Buchstaben außer dem letzten will, muss explizit auflisten oder den exklusiven Endpoint setzen.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – The Character Type
- Rust Reference – Textual types
- std::char – Methoden-Doku
- Unicode Standard
- The crate
unicode-segmentation