Logical Operators
Logical operators combine or invert boolean (bool) expressions and always produce a bool result (true or false). Rust provides three logical operators: logical NOT (!), logical AND (&&), and logical OR (||).
Overview of Logical Operators
| Operator | Name | Arity | Syntax | Evaluates to true When |
|---|---|---|---|---|
! |
Logical NOT | Unary (prefix) | !a |
a is false |
&& |
Logical AND | Binary (infix) | a && b |
Both a and b are true |
|| |
Logical OR | Binary (infix) | a || b |
At least one of a or b is true |
fn main() {
let is_authenticated = true;
let is_admin = false;
println!("!is_authenticated: {}", !is_authenticated);
println!("is_authenticated && is_admin: {}", is_authenticated && is_admin);
println!("is_authenticated || is_admin: {}", is_authenticated || is_admin);
}
!is_authenticated: false
is_authenticated && is_admin: false
is_authenticated || is_admin: true
Truth Tables
Every combination of bool inputs for unary ! and binary && and || is defined by the following truth tables:
Unary Logical NOT (!)
a |
!a |
|---|---|
true |
false |
false |
true |
Binary Logical AND (&&) and Logical OR (||)
a |
b |
a && b |
a || b |
|---|---|---|---|
true |
true |
true |
true |
true |
false |
false |
true |
false |
true |
false |
true |
false |
false |
false |
false |
Strict bool Operand Requirement
Unlike C, C++, Python, or JavaScript, Rust does not implicitly convert integers, strings, slices, or pointers into booleans. There is no concept of "truthy" or "falsy" values. Both operands of && and || must have the exact type bool.
Passing an integer or string to && or || is a compile-time type error:
// ❌ does not compile
fn main() {
let count = 5;
let ready = true;
if count && ready {
println!("Running");
}
}
error[E0308]: mismatched types
--> src/main.rs:5:8
|
5 | if count && ready {
| ^^^^^ expected `bool`, found integer
error: aborting due to 1 previous error
Write an explicit comparison such as count != 0 && ready or !name.is_empty() to produce a bool.
The ! Operator on bool vs Integers
While && and || are defined exclusively for bool, the ! operator is overloaded in the standard library (std::ops::Not) for both bool and integer types:
- On
bool,!xperforms logical negation (!true == false). - On integers (
u8,i32,usize, etc.),!xperforms bitwise NOT (inverting every bit, equivalent to~xin C).
Because ! is valid on integers and returns an integer rather than a bool, writing if !count (a common C idiom for testing whether count == 0) fails to compile in Rust:
// ❌ does not compile
fn main() {
let count: u8 = 0;
// `!count` flips all 8 bits to `255_u8`, which is NOT a `bool`!
if !count {
println!("count is zero");
}
}
error[E0308]: mismatched types
--> src/main.rs:5:8
|
5 | if !count {
| ^^^^^^ expected `bool`, found `u8`
error: aborting due to 1 previous error
Always write count == 0 instead of !count when testing whether a number is zero.
Operator Precedence and Grouping
When an expression combines comparisons and multiple logical operators without parentheses, Rust evaluates them according to a strict precedence hierarchy from highest to lowest:
Highest Precedence
1. ! (Logical NOT - unary prefix)
2. == != < > <= >= (Comparison operators)
3. && (Logical AND)
4. || (Logical OR)
Lowest Precedence
Because comparison operators bind tighter than && and ||, expressions like x > 0 && x < 10 are parsed automatically as (x > 0) && (x < 10). Likewise, because && binds tighter than ||, a || b && c is parsed as a || (b && c):
fn main() {
let a = true;
let b = false;
let c = false;
// Parsed as `a || (b && c)` -> `true || false` -> `true`
let default_precedence = a || b && c;
// Explicit parentheses override precedence: `(a || b) && c` -> `true && false` -> `false`
let grouped = (a || b) && c;
println!("a || b && c = {default_precedence}");
println!("(a || b) && c = {grouped}");
}
a || b && c = true
(a || b) && c = false
Precedence of ! with Comparisons
Because unary ! has higher precedence than ==, <, and >, writing !x == y is parsed as (!x) == y. If x and y are integers, (!x) performs a bitwise NOT on x before comparing it to y! To negate the result of a comparison, either use != or wrap the comparison in parentheses !(x < y):
fn main() {
let x = 10;
let y = 20;
let not_less = !(x < y);
println!("!(10 < 20) = {not_less}");
}
!(10 < 20) = false
Chaining Range Checks
In mathematics and Python, 0 < x < 10 checks whether x lies between 0 and 10. In Rust, comparison operators cannot be chained directly; both conditions must be joined with && (or expressed with .contains(&x) on a range):
// ❌ does not compile
fn main() {
let x = 5;
if 0 < x < 10 {
println!("in range");
}
}
error: comparison operators cannot be chained
--> src/main.rs:3:10
|
3 | if 0 < x < 10 {
| ^ ^
|
help: split the comparison into two
|
3 | if 0 < x && x < 10 {
| ++++++++
error: aborting due to 1 previous error
Joining the two comparisons with && compiles and runs as intended:
fn main() {
let x = 5;
if 0 < x && x < 10 {
println!("{x} is strictly between 0 and 10");
}
}
5 is strictly between 0 and 10
De Morgan's Laws
Negating a compound boolean expression follows De Morgan's laws, which distribute ! across && and || by flipping the operator:
!(a && b)is logically equivalent to!a || !b!(a || b)is logically equivalent to!a && !b
fn main() {
for a in [false, true] {
for b in [false, true] {
assert_eq!(!(a && b), !a || !b);
assert_eq!(!(a || b), !a && !b);
}
}
println!("De Morgan's laws verified for all boolean inputs.");
}
De Morgan's laws verified for all boolean inputs.
Eager (Non-Short-Circuiting) Boolean Operators and XOR
The && and || operators are lazy (short-circuiting): a && b skips evaluating b if a is false, and a || b skips evaluating b if a is true.
When you want both boolean operands to be evaluated unconditionally—or when you need an exclusive OR (XOR) operation—Rust also supports the single-character operators &, |, and ^ (or !=) on bool values:
| Expression | Behavior on bool Operands |
Evaluates Right Operand Always? |
|---|---|---|
a && b |
Short-circuiting logical AND | No (skipped if a == false) |
a || b |
Short-circuiting logical OR | No (skipped if a == true) |
a & b |
Eager logical AND | Yes |
a | b |
Eager logical OR | Yes |
a ^ b or a != b |
Logical XOR (true when a and b differ) |
Yes |
fn step(name: &str, result: bool) -> bool {
println!(" evaluated {name} -> {result}");
result
}
fn main() {
println!("Short-circuit &&:");
let r1 = step("left", false) && step("right", true);
println!("Result: {r1}\n");
println!("Eager &:");
let r2 = step("left", false) & step("right", true);
println!("Result: {r2}\n");
println!("Logical XOR (^):");
println!("true ^ false = {}", true ^ false);
println!("true ^ true = {}", true ^ true);
}
Short-circuit &&:
evaluated left -> false
Result: false
Eager &:
evaluated left -> false
evaluated right -> true
Result: false
Logical XOR (^):
true ^ false = true
true ^ true = false