Expressions
Summary
An expression is any piece of code that evaluates to a value. 5, x,
a + b, and IsAlive() && HasAmmo() are all expressions — each one
produces something, as opposed to a statement, which does something
(and may or may not contain an expression). Understanding where an
expression ends and a statement begins clarifies what is actually legal
to write in each part of a program's syntax (an if's condition must be
an expression that produces a bool; a loop body is statements, not a
single expression).
Mental Model
An expression is a small calculation with a definite answer. Some
expressions are trivially simple (a single literal like 42, or a single
variable name like health); others are built by combining smaller
expressions with operators (health - damage), or by calling a function
that returns a value (GetDistance(a, b)).
The key mental shift: an expression always has a value and a type,
even when it's not obviously "doing math" — isGrounded is an expression
whose value is whatever bool that variable currently holds, and a
function call is an expression whose value is whatever the function
returns. Anywhere the language expects "a value of type X", any
expression that produces type X is legal there, no matter how it's built.
Core Explanation
Expressions compose: an expression can itself be built from smaller
expressions, recursively, down to literals and variable names as the base
case. (a + b) * (c - d) is one expression made of two parenthesized
sub-expressions, each made of a variable, an operator, and a variable.
This is exactly why operator precedence (see Operators) matters — the compiler has to decide, for an expression with more than one operator, which sub-expression to evaluate first to build up the final value correctly.
Expressions vs. statements. A statement is a complete instruction —
if (...) { ... }, a for loop, a variable declaration. A statement can
contain expressions (the loop's condition, the value assigned to a new
variable) but is not itself a value that could be used somewhere else. In
most C-family languages, an expression becomes a statement simply by
adding a semicolon (health -= damage; is the assignment expression, used
as a statement) — which is why assignment can be written on its own line,
even though = is technically producing a value (the assigned value)
that this usage just discards.
Side effects. Some expressions only compute a value (a + b has no
effect beyond producing a number); others also change program state as a
side effect of evaluating them (health -= damage both produces a value
and mutates health; a function call can do arbitrary work before
returning its result). Relying on which expressions have side effects,
and in what order they run, is where short-circuit evaluation (Operators)
and evaluation order genuinely matter for correctness, not just style.
Boolean expressions deserve special mention because they are what
every conditional statement and loop condition is built from — see
Conditional Statements and Loops, both of which require their condition
to be an expression whose value is a bool.
Syntax / Examples
C# (Unity)
int a = 3, b = 4;
int sum = a + b; // expression: a + b
bool canFire = ammo > 0 && !isReloading; // boolean expression
int closest = Mathf.Min(distA, distB); // function-call expression
// Assignment is itself an expression, used here as a statement:
health -= damage;
// A conditional (ternary) expression: produces a value directly.
string status = health > 0 ? "Alive" : "Dead";
C++ (Unreal)
int32 A = 3, B = 4;
int32 Sum = A + B;
bool bCanFire = Ammo > 0 && !bIsReloading;
int32 Closest = FMath::Min(DistA, DistB);
Health -= Damage;
FString Status = Health > 0 ? TEXT("Alive") : TEXT("Dead");
Unreal Blueprint
A wire carrying a value between two nodes is an expression, visually —
the "+" node's output pin is the expression a + b, ready to feed into
whatever needs that value next. There is no visual distinction between "a
value" and "an expression that produces a value"; a literal pin and a
computed pin are interchangeable anywhere a value of that type is needed.
Practical Example
Computing whether an attack lands combines several sub-expressions into one boolean expression, which a conditional statement then acts on:
bool inRange = Vector3.Distance(attacker.position, target.position) <= attackRange;
bool hasLineOfSight = !Physics.Linecast(attacker.position, target.position, obstacleMask);
bool attackLands = inRange && hasLineOfSight && target.IsAlive;
if (attackLands)
{
target.TakeDamage(attackPower);
}
Naming each sub-expression (inRange, hasLineOfSight) rather than
inlining everything into one long condition is a readability choice, not
a functional requirement — if (Vector3.Distance(...) <= attackRange && ...)
would evaluate identically, but the named version documents what each
piece of the overall expression means.
Common Mistakes
- Treating an assignment's side effect as its purpose while forgetting
it's also an expression —
while ((line = reader.ReadLine()) != null)relies on assignment producing a value (the line just read) that the loop condition then tests; missing that assignment is an expression makes this pattern look like a syntax error rather than what it is. - Overly long, un-decomposed expressions — a single condition combining
five comparisons with mixed
&&/||is hard to verify by eye. Breaking it into named boolean sub-expressions (as in the Practical Example) costs nothing at runtime and is far easier to review. - Assuming a function call is "free" inside a larger expression — a
condition like
while (GetEnemyCount() > 0)callsGetEnemyCount()every single iteration; if that function does real work, the loop is repeating that work, not just checking a cached number. - Losing evaluation order across expressions with side effects —
Attack(target) && target.IsAlivediffers fromtarget.IsAlive && Attack(target)because&&'s short-circuit means the second one may skip the attack entirely ifIsAliveis already false.
Best Practices
- Give a name to any boolean sub-expression whose meaning isn't obvious
from its code alone — a well-named
boolis cheaper to read than a comment explaining a raw condition. - Keep expressions with side effects (assignment, function calls that mutate state) out of larger boolean expressions unless the ordering and short-circuit behavior are deliberate and understood.
- Cache the result of an expensive function call in a local variable before using it more than once, rather than calling it repeatedly inside a condition or loop.
- Use parentheses to make the intended grouping of a multi-operator expression unambiguous to a reader, even where the language's precedence rules would already produce the intended result.
Exercises
- Identify every expression in the line
float speed = Mathf.Clamp(baseSpeed + bonus, 0f, maxSpeed);and state each one's type. - Rewrite a five-condition
ifstatement (write one) by extracting each sub-condition into a named boolean variable, then compare readability. - Explain, using short-circuit evaluation, why swapping the operand order
in
Attack(target) && target.IsAlivechanges behavior. - Write a
whileloop whose condition both assigns and tests a value in one expression (mirroring theReadLine()example), and explain what would break if assignment were not itself an expression.
Related Concepts
- Operators — the pieces expressions are built from.
- Conditional Statements and Loops — both consume a boolean expression as their condition.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules.- C# Language Reference — Expressions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/#expressions
- cppreference — Expressions: https://en.cppreference.com/w/cpp/language/expressions