if / else
if / else
Level 2 — Control Flow & Data Structures Conditional branching;
ifis an expression and can return values.
1. Prerequisites
- Variable — Understanding how to assign the result of an
ifexpression to a variable.
2. Term Category
Rust Control Flow (expression-based branching): While if statements exist in almost every programming language, Rust elevates them by making them expressions (meaning they can return a value directly), eliminating the need for the "ternary operator" (condition ? a : b) found in languages like C, Java, or JavaScript.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Every program needs to make decisions (conditional branching). If a user is an admin, show the admin panel; otherwise, show the standard dashboard.
In older languages, if is purely a statement—an action that executes code. If you wanted to assign a value based on a condition, you had to either:
- Create a mutable variable, then mutate it inside the
ifblock (which is verbose and breaks immutability). - Use a completely different, specialized syntax called the ternary operator (
let status = is_admin ? "Admin" : "User";).
Rust elegantly solves this by making if an expression. In Rust, blocks of code can evaluate to a final value. This means you can assign an entire if / else block directly to a variable. It keeps the language syntax simple (no need for ternary operators) while encouraging you to use safe, immutable variables (let instead of mut).
(2) Reality Metaphor
An if statement is like approaching a fork in a road. You read the sign (the condition). If the sign says "Bridge Out," you take the else path.
Because Rust's if is an expression, it's also like a vending machine. You press a button based on a condition (e.g., "Do I want soda or water?"). The machine evaluates your choice and returns an item directly into your hands (the value assigned to your variable).
(3) Rust Code Examples
Short Snippet (Standard usage)
let health = 45;
// Notice there are NO parentheses around the condition.
if health <= 0 {
println!("Game Over!");
} else if health < 50 {
println!("Warning: Low Health!");
} else {
println!("Looking good.");
}
Fuller Example (Using if as an expression)
fn main() {
let is_vip = true;
// We are assigning the result of the entire `if` block to `entrance_fee`.
// Because we don't put semicolons at the end of `0` and `50`,
// they are RETURNED from the block.
let entrance_fee = if is_vip {
0
} else {
50
};
println!("Your fee is ${}", entrance_fee);
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding If Else Scoping and Lifecycle Rules
The mistake: Assuming If Else instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("if_else_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("if_else_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Putting parentheses around the condition
The mistake: Writing if (x > 5) like you would in C, Java, or JavaScript.
Why it's wrong: Rust does not require (or want) parentheses around the boolean condition. If you include them, the compiler will actually give you a warning telling you to remove them, as they are considered unnecessary visual clutter.
Incorrect:
if (health == 100) { ... } // Compiler Warning!
Fix:
if health == 100 { ... }
Mistake 3: Concurrent Access to If Else Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe If Else instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Financial Order Routing & Dynamic Surcharge Engine
Scenario:
You are building an execution gateway for a low-latency financial exchange. The service must validate incoming order requests and compute dynamic fee quotes based on account tier (VIP, Standard, Guest), account activity status (Active, Suspended), order transaction amount in cents, and real-time exchange load factor (f64).
Requirements:
- Define the data structures:
AccountTier:VIP,Standard,GuestAccountStatus:Active,SuspendedTransactionQuote: containing fieldsbase_fee_cents: u64,surcharge_cents: u64,total_fee_cents: u64,priority: &'static strTransactionError:AccountSuspended,ExceedsLimit,InvalidAmount
- Implement
evaluate_transaction(tier: AccountTier, status: AccountStatus, amount_cents: u64, load_factor: f64) -> Result<TransactionQuote, TransactionError>without using anymutvariable bindings. - Use expression-based
if/elselogic to:- Reject suspended accounts (
AccountStatus::Suspended) or zero amounts (0). - Calculate maximum single order transaction limits:
VIP= 1,000 (100,000 cents),Guest= $100 (10,000 cents). ReturnErr(TransactionError::ExceedsLimit)if exceeded. - Assign base fees:
VIP= 10 cents,Standard= 50 cents,Guest= 200 cents. - Calculate load surcharges: if
load_factor > 0.8, add 20% ofbase_fee_cents; ifload_factor > 0.5, add 10%; otherwise 0 cents. - Assign execution priority:
VIPreceives"HIGH";Standardreceives"NORMAL"ifamount_cents >= 10_000else"LOW";Guestreceives"LOW".
- Reject suspended accounts (
- Provide unit tests using
assert_eq!,assert!,assert_ne!, andmatches!.
Answer
Implementation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountTier {
VIP,
Standard,
Guest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountStatus {
Active,
Suspended,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionQuote {
pub base_fee_cents: u64,
pub surcharge_cents: u64,
pub total_fee_cents: u64,
pub priority: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransactionError {
AccountSuspended,
ExceedsLimit,
InvalidAmount,
}
pub fn evaluate_transaction(
tier: AccountTier,
status: AccountStatus,
amount_cents: u64,
load_factor: f64,
) -> Result<TransactionQuote, TransactionError> {
if matches!(status, AccountStatus::Suspended) {
return Err(TransactionError::AccountSuspended);
}
if amount_cents == 0 {
return Err(TransactionError::InvalidAmount);
}
let max_limit = if matches!(tier, AccountTier::VIP) {
1_000_000
} else if matches!(tier, AccountTier::Standard) {
100_000
} else {
10_000
};
if amount_cents > max_limit {
return Err(TransactionError::ExceedsLimit);
}
let base_fee_cents = if matches!(tier, AccountTier::VIP) {
10
} else if matches!(tier, AccountTier::Standard) {
50
} else {
200
};
let surcharge_cents = if load_factor > 0.8 {
(base_fee_cents as f64 * 0.20) as u64
} else if load_factor > 0.5 {
(base_fee_cents as f64 * 0.10) as u64
} else {
0
};
let priority = if matches!(tier, AccountTier::VIP) {
"HIGH"
} else if matches!(tier, AccountTier::Standard) {
if amount_cents >= 10_000 {
"NORMAL"
} else {
"LOW"
}
} else {
"LOW"
};
Ok(TransactionQuote {
base_fee_cents,
surcharge_cents,
total_fee_cents: base_fee_cents + surcharge_cents,
priority,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vip_active_quote() {
let res = evaluate_transaction(AccountTier::VIP, AccountStatus::Active, 500_000, 0.85);
assert!(res.is_ok());
let quote = res.unwrap();
assert_eq!(quote.base_fee_cents, 10);
assert_eq!(quote.surcharge_cents, 2);
assert_eq!(quote.total_fee_cents, 12);
assert_eq!(quote.priority, "HIGH");
assert_ne!(quote.priority, "LOW");
}
#[test]
fn test_suspended_account() {
let res = evaluate_transaction(AccountTier::VIP, AccountStatus::Suspended, 500_000, 0.1);
assert!(matches!(res, Err(TransactionError::AccountSuspended)));
}
#[test]
fn test_exceeds_limit() {
let res = evaluate_transaction(AccountTier::Guest, AccountStatus::Active, 50_000, 0.1);
assert!(matches!(res, Err(TransactionError::ExceedsLimit)));
}
#[test]
fn test_standard_priority_branching() {
let high_val = evaluate_transaction(AccountTier::Standard, AccountStatus::Active, 15_000, 0.2).unwrap();
let low_val = evaluate_transaction(AccountTier::Standard, AccountStatus::Active, 5_000, 0.2).unwrap();
assert_eq!(high_val.priority, "NORMAL");
assert_eq!(low_val.priority, "LOW");
assert_ne!(high_val.priority, low_val.priority);
}
}
Technical Explanation
- Expression-Based Value Assignment: In traditional imperative languages, computing values across complex business rules requires declaring dynamic mutable state (
let mut limit = 0;). In Rust,if / else if / elseblocks are first-class expressions. By placing the evaluated scalar directly at the tail of each branch block without a trailing semicolon, Rust returns that value straight into the target immutable variable (let max_limit = if ...). - Type Invariance Across Branch Arms: Every arm in an
ifexpression tree must evaluate to the identical type. In theprioritycalculation, every branch evaluates to&'static str. If one branch attempted to yield an ownedStringwhile another yielded&str, the compiler would reject the program with type mismatch errorE0308. - Ownership and Lifetime Invariants: Returning static string literals (
"HIGH","NORMAL","LOW") ensures zero heap allocation overhead. The returned&'static strlives for the entire program execution duration, eliminating reference lifetime tracking or ownership transfers for the fee quote caller. - Edge Case Safety: Early returns via guard clauses (
if matches!(status, AccountStatus::Suspended)) prevent unnecessary calculations and cleanly decouple exception handling from value computation expressions.
Exercise 2: Network Telemetry Packet Ingestion & Routing Policy Evaluator
Scenario: A network packet processing daemon receives telemetry frames over dynamic routing channels. Before buffer allocation occurs, the engine must validate payload integrity, dynamically determine max allowable buffer allocation, assign packet timeouts, and determine target routing queues based on protocol version, payload checksum validation, encryption status, and real-time latency sensitivity flags.
Requirements:
- Define the data structures:
ProtocolVersion:V1,V2,V3PacketHeader:version: ProtocolVersion,payload_bytes: usize,is_encrypted: bool,latency_critical: bool,checksum_valid: boolRoutingTarget:FastPath,StandardQueue,QuarantinePacketPolicy:max_buffer_size: usize,timeout_ms: u64,target: RoutingTargetValidationError:ChecksumFailure,PayloadTooLarge
- Implement
evaluate_packet_routing(header: &PacketHeader) -> Result<PacketPolicy, ValidationError>. - Use expression-based conditional evaluation to:
- Instantly reject invalid checksums with
Err(ValidationError::ChecksumFailure). - Assign
max_buffer_size:V3allows 65,536 bytes ifis_encryptedand 32,768 bytes otherwise;V2allows 16,384 bytes;V1allows 8,192 bytes. - Reject payloads exceeding
max_buffer_sizewithErr(ValidationError::PayloadTooLarge). - Calculate
timeout_ms: latency-critical requests receive 50 ms forV3or 100 ms for older versions; non-critical requests receive 500 ms if encrypted or 1000 ms if unencrypted. - Route
target:FastPathfor encrypted latency-critical packets;StandardQueuefor remaining encrypted packets orV3streams;Quarantinefor all unencrypted legacy frames.
- Instantly reject invalid checksums with
- Provide unit tests using
assert_eq!,assert!,assert_ne!, andmatches!.
Answer
Implementation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolVersion {
V1,
V2,
V3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacketHeader {
pub version: ProtocolVersion,
pub payload_bytes: usize,
pub is_encrypted: bool,
pub latency_critical: bool,
pub checksum_valid: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoutingTarget {
FastPath,
StandardQueue,
Quarantine,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacketPolicy {
pub max_buffer_size: usize,
pub timeout_ms: u64,
pub target: RoutingTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
ChecksumFailure,
PayloadTooLarge,
}
pub fn evaluate_packet_routing(header: &PacketHeader) -> Result<PacketPolicy, ValidationError> {
if !header.checksum_valid {
return Err(ValidationError::ChecksumFailure);
}
let max_buffer_size = if matches!(header.version, ProtocolVersion::V3) {
if header.is_encrypted {
65_536
} else {
32_768
}
} else if matches!(header.version, ProtocolVersion::V2) {
16_384
} else {
8_192
};
if header.payload_bytes > max_buffer_size {
return Err(ValidationError::PayloadTooLarge);
}
let timeout_ms = if header.latency_critical {
if matches!(header.version, ProtocolVersion::V3) {
50
} else {
100
}
} else if header.is_encrypted {
500
} else {
1000
};
let target = if header.latency_critical && header.is_encrypted {
RoutingTarget::FastPath
} else if header.is_encrypted || matches!(header.version, ProtocolVersion::V3) {
RoutingTarget::StandardQueue
} else {
RoutingTarget::Quarantine
};
Ok(PacketPolicy {
max_buffer_size,
timeout_ms,
target,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_v3_fastpath() {
let header = PacketHeader {
version: ProtocolVersion::V3,
payload_bytes: 4000,
is_encrypted: true,
latency_critical: true,
checksum_valid: true,
};
let res = evaluate_packet_routing(&header);
assert!(res.is_ok());
let policy = res.unwrap();
assert_eq!(policy.max_buffer_size, 65_536);
assert_eq!(policy.timeout_ms, 50);
assert_eq!(policy.target, RoutingTarget::FastPath);
assert_ne!(policy.target, RoutingTarget::Quarantine);
}
#[test]
fn test_invalid_checksum() {
let header = PacketHeader {
version: ProtocolVersion::V1,
payload_bytes: 100,
is_encrypted: false,
latency_critical: false,
checksum_valid: false,
};
let res = evaluate_packet_routing(&header);
assert!(matches!(res, Err(ValidationError::ChecksumFailure)));
}
#[test]
fn test_payload_exceeds_buffer() {
let header = PacketHeader {
version: ProtocolVersion::V1,
payload_bytes: 10_000,
is_encrypted: false,
latency_critical: false,
checksum_valid: true,
};
let res = evaluate_packet_routing(&header);
assert!(matches!(res, Err(ValidationError::PayloadTooLarge)));
}
#[test]
fn test_quarantine_routing() {
let header = PacketHeader {
version: ProtocolVersion::V1,
payload_bytes: 500,
is_encrypted: false,
latency_critical: false,
checksum_valid: true,
};
let policy = evaluate_packet_routing(&header).unwrap();
assert_eq!(policy.target, RoutingTarget::Quarantine);
assert_eq!(policy.timeout_ms, 1000);
}
}
Technical Explanation
- Nested Conditional Expressions:
ifexpressions can be cleanly nested within individual branch arms (such as evaluating dynamic buffer sizes inside theProtocolVersion::V3arm). Because each block produces an expression value, Rust enforces structural type consistency top-down throughout the evaluation tree. - Immutable Control Flow: By avoiding temporary mutable flags (
let mut timeout = 0;), the code guarantees thread safety and eliminates partial initialization bugs. The compiler proves that every conceivable branch initializesmax_buffer_size,timeout_ms, andtargetexactly once. - Reference Borrowing Efficiency:
evaluate_packet_routingaccepts&PacketHeaderas a read-only shared reference. The conditional expressions read primitive fields (bool,usize,enum) without consuming or moving the underlying header struct, permitting reuse of the original packet buffer in downstream network processing code. - Edge Cases: Short-circuit logic prevents downstream buffer limit calculations when payload checksums fail, minimizing telemetry pipeline latency under hostile network conditions or corrupt packet floods.
Exercise 3: Memory Cache Allocation & Eviction Tier Controller
Scenario: High-throughput caching systems need to optimize buffer placement based on system memory pressure, payload size, and allocation urgency. Depending on current resource saturation, data must be directed to fixed stack buffers, pooled heap blocks, or spilled to disk storage, while simultaneously adjusting cache item retention TTLs and eviction aggressiveness.
Requirements:
- Define the data structures:
CachePressure:Low,Medium,High,CriticalAllocationStrategy:StackBuffer { capacity: usize },HeapPool { block_size: usize },DiskSpillEvictionPolicy:None,LRU,AggressiveEvictionAllocationPlan:strategy: AllocationStrategy,eviction: EvictionPolicy,ttl_seconds: u32AllocationError:MemoryCriticalRejected
- Implement
determine_cache_allocation(payload_size: usize, pressure: CachePressure, is_urgent: bool) -> Result<AllocationPlan, AllocationError>. - Apply expression-based
if/elserules:- If
pressureisCriticaland!is_urgent, reject immediately withErr(AllocationError::MemoryCriticalRejected). - Strategy selection: small payloads (
<= 512bytes) receiveStackBuffer { capacity: 512 }. Medium payloads (<= 65,536bytes) underLow/Mediumpressure receiveHeapPoolsized to the next power of two (payload_size.next_power_of_two()). Larger payloads or high pressure spill toDiskSpill. - Eviction selection:
CriticalorHighpressure triggersAggressiveEviction;Mediumpressure usesLRU;Lowpressure usesNone. - TTL calculation:
Criticalpressure sets 30s TTL;Highpressure sets 120s TTL; urgent requests receive 3,600s TTL; default non-urgent low/medium requests receive 600s TTL.
- If
- Provide unit tests using
assert_eq!,assert!,assert_ne!, andmatches!.
Answer
Implementation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachePressure {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllocationStrategy {
StackBuffer { capacity: usize },
HeapPool { block_size: usize },
DiskSpill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvictionPolicy {
None,
LRU,
AggressiveEviction,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllocationPlan {
pub strategy: AllocationStrategy,
pub eviction: EvictionPolicy,
pub ttl_seconds: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllocationError {
MemoryCriticalRejected,
}
pub fn determine_cache_allocation(
payload_size: usize,
pressure: CachePressure,
is_urgent: bool,
) -> Result<AllocationPlan, AllocationError> {
if matches!(pressure, CachePressure::Critical) && !is_urgent {
return Err(AllocationError::MemoryCriticalRejected);
}
let strategy = if payload_size <= 512 {
AllocationStrategy::StackBuffer { capacity: 512 }
} else if payload_size <= 65_536 && !matches!(pressure, CachePressure::High | CachePressure::Critical) {
AllocationStrategy::HeapPool {
block_size: payload_size.next_power_of_two(),
}
} else {
AllocationStrategy::DiskSpill
};
let eviction = if matches!(pressure, CachePressure::Critical | CachePressure::High) {
EvictionPolicy::AggressiveEviction
} else if matches!(pressure, CachePressure::Medium) {
EvictionPolicy::LRU
} else {
EvictionPolicy::None
};
let ttl_seconds = if matches!(pressure, CachePressure::Critical) {
30
} else if matches!(pressure, CachePressure::High) {
120
} else if is_urgent {
3600
} else {
600
};
Ok(AllocationPlan {
strategy,
eviction,
ttl_seconds,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_small_payload_stack_alloc() {
let plan = determine_cache_allocation(256, CachePressure::Low, false).unwrap();
assert_eq!(plan.strategy, AllocationStrategy::StackBuffer { capacity: 512 });
assert_eq!(plan.eviction, EvictionPolicy::None);
assert_eq!(plan.ttl_seconds, 600);
assert_ne!(plan.eviction, EvictionPolicy::AggressiveEviction);
}
#[test]
fn test_medium_heap_alloc() {
let plan = determine_cache_allocation(1000, CachePressure::Medium, false).unwrap();
assert_eq!(plan.strategy, AllocationStrategy::HeapPool { block_size: 1024 });
assert_eq!(plan.eviction, EvictionPolicy::LRU);
assert!(matches!(plan.strategy, AllocationStrategy::HeapPool { .. }));
}
#[test]
fn test_critical_pressure_rejection() {
let res = determine_cache_allocation(100, CachePressure::Critical, false);
assert!(matches!(res, Err(AllocationError::MemoryCriticalRejected)));
}
#[test]
fn test_critical_pressure_urgent_spill() {
let plan = determine_cache_allocation(70_000, CachePressure::Critical, true).unwrap();
assert_eq!(plan.strategy, AllocationStrategy::DiskSpill);
assert_eq!(plan.eviction, EvictionPolicy::AggressiveEviction);
assert_eq!(plan.ttl_seconds, 30);
}
}
Technical Explanation
- Enum Variant Construction in Expressions: Rust's
ifexpression branches seamlessly yield rich algebraic data types (AllocationStrategy::StackBuffervsAllocationStrategy::HeapPoolvsAllocationStrategy::DiskSpill). The compiler computes the total memory layout ofAllocationStrategybased on its largest variant at compile time. - Combinatorial Logical Operators in Conditions: Complex conditional expressions (such as
!matches!(pressure, CachePressure::High | CachePressure::Critical)) allow clean multi-condition evaluation without requiring nested boolean flags or repetitive match expressions. - Determinism and Zero Dead Paths: The compiler guarantees exhaustiveness across all branches. Because every
ifexpression has a mandatory trailingelseblock, Rust guarantees thatstrategy,eviction, andttl_secondsare fully initialized regardless of execution path. - Edge Cases: Non-power-of-two payload sizes are automatically aligned upward via
.next_power_of_two()inside theHeapPoolexpression arm, preventing fragmentation issues in memory allocation pools under high throughput.
6. Related Terms
match— The more powerful, pattern-matching alternative toif. Usually preferred over long chains ofelse if.- Expressions —
- Statements — Related concept: Statements.
7. Key Takeaways
- Use
if,else if, andelsefor basic conditional branching. - Do not put parentheses around the condition (
if x > 5 {). ifblocks are expressions. They can evaluate to a value, which allows you to assign them directly to aletvariable.- When used as an expression, every branch must return the exact same data type.
- Using
ifas an expression is the idiomatic Rust alternative to the ternary operator (? :).