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>).

Rust char-Beispiele
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.

Rust char ≠ byte
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:

EscapeBedeutung
\nNewline (LF)
\rCarriage Return
\tTab
\\Backslash
\'Apostroph (in char-Literal)
\"Doppelte Anführung (in String)
\0NULL
\x7FASCII-Byte als Hex (max 0x7F)
\u{1F980}Unicode-Scalar als Hex (bis 6 Hex-Ziffern)
Rust Escapes in Aktion
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:

Rust Klassifikation
'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
Rust Konvertierung
'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():

Rust Iteration
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

Rust char zu 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

Rust 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:

Rust Ordnung
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:

Rust Pattern-Matching
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:

Rust Slug-Generator
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

Rust Passwort-Validator
#[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:

Rust Mini-Tokenizer
#[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

/ Weiter

Zurück zu Primitive Datentypen

Zur Übersicht