C programmers usually start with this mapping:
C if -> Rust if
C while -> Rust while
C for -> Rust for
C break -> Rust break
C continue -> Rust continue
The mapping is mostly right, but a few differences affect everyday code:
ifandwhileconditions must beboolifcan produce a value- Rust has no C-style
for (init; condition; step) - Rust
foriterates over an iterable value loopexpresses an intentional infinite loop and can return a value withbreak value
These are not advanced features. They show up immediately in CLI tools and buffer processing.
if conditions must be bool
C accepts integers, pointers, and expression results as conditions:
if (len) {
handle_data();
}
if (ptr) {
use_ptr(ptr);
}
if (flags & 0x01) {
enable();
}
Rust if conditions must be bool:
if len != 0 {
handle_data();
}
if !ptr.is_null() {
// raw pointer case
}
if flags & 0x01 != 0 {
enable();
}
This is the same rule from the operators article: Rust does not implicitly turn integers into truth values. Conditions must state the test.
In ordinary Rust code, APIs often express the intent directly:
if !data.is_empty() {
handle_data(data);
}
if name.is_empty() {
return Err(ConfigError::EmptyName);
}
This is slightly longer than if (len), but readers do not need to guess what non-zero means.
if can produce a value
C often uses the ternary operator:
int limit = debug ? 64 : 1024;
Rust has no ?:. if itself can produce a value:
let limit = if debug { 64 } else { 1024 };
Both branches must produce the same type:
let limit = if debug { 64 } else { "large" }; // compile error
This works well for simple choices:
let mode = if readonly {
"read"
} else {
"write"
};
If the code only performs actions, it still looks close to C:
if verbose {
eprintln!("reading config");
}
The difference is that Rust if can be both control flow and an expression, depending on how it is used.
while is close to C, but still bool-only
C while:
while (offset < len) {
offset++;
}
Rust is close:
while offset < len {
offset += 1;
}
The condition still must be bool. This C style:
while (len--) {
consume();
}
does not translate directly. A clearer Rust form is:
while len > 0 {
consume();
len -= 1;
}
For read loops, bind intermediate values explicitly:
while offset < data.len() {
let byte = data[offset];
offset += 1;
handle_byte(byte);
}
This is still a familiar loop model. Rust simply avoids packing decrement, assignment, and condition into one expression.
loop means an intentional infinite loop
C often writes infinite loops as:
for (;;) {
poll();
}
or:
while (1) {
poll();
}
Rust uses loop:
loop {
poll();
}
This is more direct than while true: the loop is intentionally infinite until an internal break, return, or error propagation exits it.
loop can also return a value with break:
let packet = loop {
if let Some(packet) = try_read_packet()? {
break packet;
}
};
This fits “keep trying until a result is available.” In C, that often needs a variable declared before the loop and assigned before breaking out.
Rust for is not C’s three-part for
C’s common for form is:
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
Rust has no for (init; condition; step). Rust for iterates over something iterable:
for byte in data {
sum += *byte as u32;
}
When you need an index, use a range:
for i in 0..data.len() {
sum += data[i] as u32;
}
0..data.len() excludes the right bound, like i < len in C.
When you need both index and value, use enumerate:
for (i, byte) in data.iter().enumerate() {
println!("{i}: {byte}");
}
Rust’s default preference is to iterate over the data directly. Indexing is still allowed, but it should not be the default shape for every loop.
Ranges can include or exclude the end
Rust commonly uses two range forms:
0..len // excludes len
0..=last // includes last
They map to common C conditions:
for (i = 0; i < len; i++) // 0..len
for (i = 0; i <= last; i++) // 0..=last
For slices, excluding the right bound is the common case:
for i in 0..data.len() {
handle(data[i]);
}
But if you only need each element, prefer:
for byte in data {
handle(*byte);
}
That removes one class of bounds mistakes and fits Rust’s slice model better.
break and continue are familiar
C break and continue:
for (size_t i = 0; i < len; i++) {
if (data[i] == 0) {
break;
}
if (data[i] == 0xff) {
continue;
}
handle(data[i]);
}
Rust is similar:
for byte in data {
if *byte == 0 {
break;
}
if *byte == 0xff {
continue;
}
handle(*byte);
}
For nested loops, Rust can label the loop to break out of a specific level:
'outer: for row in rows {
for byte in row {
if *byte == 0 {
break 'outer;
}
}
}
This is clearer than using an extra flag or goto for many multi-level exits in C.
match is not just switch with new syntax
C uses switch for multi-way branches:
switch (state) {
case 0:
init();
break;
case 1:
run();
break;
default:
fail();
break;
}
Rust has match:
match state {
0 => init(),
1 => run(),
_ => fail(),
}
At the first layer, it is a stricter multi-way branch. It does not fall through automatically, each arm uses =>, and _ is the default arm.
With enums, match becomes more important: it is not just a switch replacement, it can check that enum states are covered.
Minimal migration table
C if (x) -> Rust if x != 0 / !is_empty() / !is_null()
C ?: -> Rust if ... { ... } else { ... }
C while (cond) -> Rust while cond
C while (len--) -> Rust while len > 0 { len -= 1; ... }
C for (;;) -> Rust loop
C for (i=0; i<len; i++) -> Rust for i in 0..len
C array traversal -> Rust for item in slice
C break / continue -> Rust break / continue
C multi-level exit -> Rust loop label + break 'label
C switch -> Rust match
Rust control flow is not difficult because of syntax. The real shift is that Rust reduces C’s habit of packing assignment, increment, condition, and implicit truthiness into one expression. Write the condition clearly and the traversal target clearly; borrowing and ownership become easier to reason about later.