Raw strings, escapes and the literal prefixes¶
Level: 101 → 201 · working knowledge
One line: A handful of prefixes decide what the compiler does with the characters between your quotes — none, r, b, c, and the br / cr combinations — and the difference between "C:\temp\new" and r"C:\temp\new" is a tab and a newline you never typed.
let typed = r"C:\temp\new"; // 11 bytes: exactly the characters you see
let cooked = "C:\temp\new"; // 9 bytes: \t became a tab, \n a newline
Every literal, in one table¶
| you type | you get | |
|---|---|---|
"…" |
&'static str ↗ |
UTF-8 text, supporting the three escapes below |
"\n\r\t\0\\" |
the common escapes — and \" for a quote |
|
"\x36" |
one ASCII byte, \x00–\x7F only |
|
"\u{7fff}" |
any Unicode scalar value, up to six hex digits | |
r"…" |
&'static str |
raw: no escape above is interpreted |
r#"…"# |
raw, and the text may contain " |
|
c"…" |
&'static CStr ↗ |
NUL-terminated, for calling C — since 1.77 |
cr"…", cr#"…"# |
the raw combination of the two above | |
b"…" |
&'static [u8; N] ↗ |
ASCII source, and no UTF-8 promise at all |
br"…", br#"…"# |
the raw combination again | |
b'x' |
u8 ↗ |
one byte, an ASCII byte literal |
'🦀' |
char ↗ |
one Unicode scalar value, always four bytes wide |
The same twelve, run:
Verified output of raw_strings_and_escapes.rs — regenerated by tools/run_examples.py, never hand-typed.
THE TWELVE LITERALS
you type you get it holds
"hello" &'static str "hello"
"a\tb\nc\0d\\e" &'static str "a\tb\nc\0d\\e"
"\x36" &'static str "6"
"\u{7fff}" &'static str "翿" = U+7FFF
r"C:\temp\new" &'static str "C:\\temp\\new"
r#"he said "hi""# &'static str "he said \"hi\""
c"linker" &'static CStr "linker" [108, 105, 110, 107, 101, 114, 0]
cr#"C:\a"b"# &'static CStr "C:\\a\"b"
b"Hi" &'static [u8; 2] [72, 105]
br"\n" &'static [u8; 2] [92, 110] <- backslash, n
b'A' u8 65
'🦀' char '🦀' = U+1F980, 4 bytes of UTF-8, 4 bytes as a char
SAME ELEVEN KEYSTROKES, TWO DIFFERENT STRINGS
"C:\temp\new" 9 bytes "C:\temp\new"
r"C:\temp\new" 11 bytes "C:\\temp\\new"
COUNTING THE # UNTIL THE TEXT FITS
r#"a " quote"# "a \" quote"
r##"a "# sequence"## "a \"# sequence"
\x STOPS AT 7F IN TEXT, REACHES FF IN BYTES
"\x7F" "\u{7f}" the highest a str will take
b"\xF5" [245] a byte string owes UTF-8 nothing
A LITERAL SPANS LINES UNLESS YOU END ONE WITH \
no backslash "one\n two"
backslash "one two" <- newline AND the indent are eaten
DISPLAY RENDERS THE ESCAPE, DEBUG PRINTS IT BACK
{} a b
{:?} "a\tb"
r is the one you reach for¶
Every backslash in a Windows path, a regex, or a chunk of embedded JSON is an escape the compiler will try to honour. "C:\temp\new" compiles cleanly and is wrong — \t and \n are real characters now, and the string is two bytes shorter than it looks. r"…" turns the escapes off wholesale.
The # count is a delimiter you widen until the text fits:
let quote = r#"he said "hi""#; // the text contains "
let awkward = r##"a "# sequence"##; // the text contains "#
Nothing dedents a multi-line literal, so a raw string is indented the way you wrote it. Ending a line with \ eats the newline and the leading whitespace of the next line — which is how a long message stays one line of output while staying readable in the source.
\x is ASCII in text, anything in bytes¶
\x stops at \x7F in a str, because above that a single byte is not valid UTF-8 on its own. In a byte string it goes to \xFF, since b"…" promises nothing about encoding — that is what it is for.
fn main() {
let high = "\xF5"; // does not compile
let polish = b"ł"; // does not compile either
let nul = c"a\0b"; // nor this
}
error: out of range hex escape
--> escapes.rs:2:18
|
2 | let _high = "\xF5";
| ^^^^ must be a character in the range [\x00-\x7f]
|
= help: if you want to write a byte literal, use `b'\xF5'`
= help: if you want to write a Unicode character, use `'\u{F5}'`
error: non-ASCII character in byte string literal
--> escapes.rs:3:21
|
3 | let _polish = b"ł";
| ^ must be ASCII
|
help: if you meant to use the UTF-8 encoding of 'ł', use \xHH escapes
|
3 - let _polish = b"ł";
3 + let _polish = b"\xC5\x82";
|
error: null characters in C string literals are not supported
--> escapes.rs:4:19
|
4 | let _nul = c"a\0b";
| ^^
The third one is the C string type showing through the syntax: a CStr ends at its NUL, so it cannot contain one. "a\0b" is a perfectly ordinary three-character str — the refusal belongs to c"…", not to Rust. Which promise each type makes is six kinds of string.
What you see when you print one¶
An escape is resolved at compile time, so by run time there is no \t left to print — there is a tab. Display writes that tab; Debug writes the escape back, quotes included, which is why {:?} is the one to reach for when the question is what is actually in this string.
If you are coming from another language¶
Python. Almost the whole table transfers, prefix for prefix — r"…", b"…", rb"…", \x41, \u1234, \N{…} — and two differences bite.
| Python | Rust | |
|---|---|---|
r"C:\temp\new" |
raw string | r"C:\temp\new" — same idea, same spelling |
r"ends with\" is a syntax error |
Python's raw strings still cannot end in a backslash | r"ends with\" is fine; the # count, not the backslash, is what closes it |
"""…""" for multi-line |
Rust needs no triple quote — every literal already spans lines | "one\ntwo" written across two lines |
b"\xF5" is bytes, "\xF5" is str 'õ' |
\xNN in a Python str means code point NN |
"\xF5" does not compile — \x is a byte, and a str is UTF-8 |
textwrap.dedent |
trimming indentation from a block literal | no dedent in std; end lines with \ or trim yourself |
| — | a NUL-terminated literal for FFI | c"…", which Python has no syntax for |
ABAP. A string template |…| interprets \|, \{ and \\, and a text literal '…' interprets nothing at all — so 'C:\temp\new' in ABAP already behaves like Rust's r"C:\temp\new", and the surprise runs the other way: what needs no thought in ABAP needs the r prefix here. ABAP has no byte-string literal; a raw byte sequence is an X literal of hex digits (X'C582'), which is the same information as b"\xC5\x82" written as data rather than as text.
See also¶
- RFC 69 — how Rust got
b'A'— where thebrows came from, and the three questions the proposal left open - Meet the
char—'🦀', and why it is four bytes - Meet the byte —
b'A'as a number - Six kinds of string — what
&CStrand&[u8]promise &'static str— what the plain literal's lifetime meansDebugvsDisplay— which one shows you the escape- String literals ↗ — the Reference's own list
- STRINGS.md — the map this page sits on
- Strings: links, books and videos
Po polsku¶
Z tej tabeli dwa wiersze dotyczą polskiego tekstu bezpośrednio. \x kończy się na \x7F, czyli dokładnie na ASCII, a każda polska litera diakrytyczna leży wyżej — "\xF3" się nie skompiluje, ó zapisuje się jako "\u{F3}", a ł jako "\u{142}". W łańcuchu bajtowym b"…" jest jednocześnie luźniej i ciaśniej: \xNN sięga tam do \xFF, ale wpisanie ł wprost jest błędem (non-ASCII character in byte string literal), bo b"…" to &[u8; N] bez żadnej obietnicy UTF-8 — i właśnie po to istnieje. Kompilator podpowiada wtedy b"\xC5\x82", czyli te same dwa bajty, którymi ł jest zakodowane w UTF-8.
Surowy łańcuch r"…" przydaje się natomiast tam, gdzie ukośnik ma zostać ukośnikiem: "C:\temp\nowe" to w rzeczywistości C:, tabulator, emp, nowa linia i owe, podczas gdy r"C:\temp\nowe" jest tym, co widać.
Szukaj po polsku: surowe łańcuchy znaków · sekwencje ucieczki · polskie znaki diakrytyczne w kodzie · rust raw string literal · rust byte string literal b""