Expressions & Operators
This lesson walks through every operator family you will use daily, how Swift decides which one applies first, and the two places where beginners get burned: integer division and floating-point equality.
Operator Categories
Operators are classified by arity: unary takes one operand, binary takes two, and ternary takes three. The families below cover arithmetic, assignment, comparison, logic, and ranges.
Arithmetic Operators
The five arithmetic operators accept any numeric pair, but both sides must have the same type — Swift never silently widens an Int into a Double.
let a = 7
let b = 2
print(a + b) // 9 — addition
print(a - b) // 5 — subtraction
print(a * b) // 14 — multiplication
print(a / b) // 3 — integer division truncates toward zero
print(a % b) // 1 — remainder
let x = 7.0
let y = 2.0
print(x / y) // 3.5 — floating-point division keeps the fraction
// print(a + x) // ❌ compile error: mixed Int and Double
print(Double(a) + x) // 9.0 — convert one side explicitly
Assignment & Compound Assignment
The = operator stores a value, and the compound forms apply an operation and store the result in one step. Unlike C, assignment in Swift returns Void, so an accidental assignment inside a condition is a compile error rather than a silent bug.
var total = 10
total += 5 // total = total + 5 -> 15
total -= 3 // -> 12
total *= 2 // -> 24
total /= 4 // -> 6
print(total) // prints: 6
// Swift's = produces no value, so this misuse cannot compile:
var flag = false
// if flag = true { } // ❌ error: cannot assign to 'flag' in a condition
// The nil-coalescing assignment operator unwraps-or-defaults in place:
var cache: Int? = nil
cache ??= 42 // stores 42 because cache was nil
print(cache ?? -1) // prints: 42
Comparison Operators
Any type conforming to Comparable supports the six comparisons below, and each one yields a Bool. Strings and tuples compare lexicographically, which is useful for sorting without extra code.
let p = 5
let q = 9
print(p == q) // false — equal
print(p != q) // true — not equal
print(p < q) // true — less than
print(p <= q) // true — less than or equal
print(p > q) // false — greater than
print(p >= q) // false — greater than or equal
print("apple" < "banana") // true — lexicographic on Strings
print((1, 2) < (1, 3)) // true — compared element by element
print((1, 2) < (1, 2, 0)) // true — a shorter prefix sorts first
Logical Operators
The three logical operators replace deeply nested conditionals, and they short-circuit: Swift skips the right operand as soon as the answer is determined. That is what makes if list.isEmpty == false && list[0] == 1 safe.
let age = 20
let hasID = true
print(age >= 18 && hasID) // true — both operands must hold
print(age < 18 || hasID) // true — at least one operand holds
print(!hasID) // false — negation
// Short-circuit proof: the function body never runs in these cases
func expensiveCheck() -> Bool {
print("evaluated!")
return true
}
print(false && expensiveCheck()) // false — short-circuited
print(true || expensiveCheck()) // true — short-circuited
Range Operators
Ranges describe a span of values and are the idiomatic way to iterate a numeric sequence or slice a collection. The half-open form ..< is the safer default because its end is excluded.
let closed = 1...5 // 1, 2, 3, 4, 5 (inclusive)
let halfOpen = 1..<5 // 1, 2, 3, 4 (exclusive)
print(closed.count) // 5
print(halfOpen.contains(5)) // false — 5 is outside the half-open range
for i in 1...3 { print(i) } // prints: 1 2 3
let names = ["Ada", "Linus", "Grace"]
for name in names[...1] { print(name) } // one-sided prefix: Ada, Linus
Nil-Coalescing & Ternary
Two compact operators handle the most common conditional shapes: ?? supplies a fallback for an optional, and ?: chooses between two expressions based on a Bool.
let score: Int? = nil
// ?? unwraps an optional, or falls back to a default when it is nil
print(score ?? 0) // 0
print(score ?? -1) // -1
// ?: picks one of two expressions based on a condition
print((score ?? 0) >= 60 ? "pass" : "fail") // fail
// Ternary chains stay readable as long as each branch is short
let n = 7
print(n % 2 == 0 ? "even" : "odd") // odd
Precedence & Associativity
When one expression mixes several operators, precedence decides which binds tighter and associativity decides the order between equal-precedence operators. Swift defines this formally through precedence groups, so you never have to guess.
Precedence Groups in Order
The table runs from the tightest binding at the top to the loosest at the bottom. Parentheses always win, and adding them is the clearest way to document intent.
| Group | Operators | Example |
|---|---|---|
| Bitwise shifting | << >> | a << 2 |
| Multiplication | * / % &* | a * b |
| Addition | + - | a + b |
| Range | ..< ... | 1..<5 |
| Nil-coalescing | ?? | a ?? b |
| Comparison | == != < > <= >= | a == b |
| Logical AND | && | a && b |
| Logical OR | || | a || b |
| Ternary | ?: | a ? b : c |
| Assignment | = += *= | a = b |
print(2 + 3 * 4) // 14 — * binds tighter than +
print((2 + 3) * 4) // 20 — parentheses override precedence
print(10 - 4 - 3) // 3 — - is left-associative
print(2 + 3 > 4 && true) // true — arithmetic, then comparison, then logic
Overflow & Wrapping
Fixed-width integers trap when they overflow, which surfaces real bugs early. When modular wrapping is intentional, the ampersand-prefixed operators say so explicitly and never trap.
var big = Int.max
// big += 1 // ❌ runtime trap: arithmetic overflow
var wrapped = Int.max
wrapped = wrapped &+ 1 // wraps to the smallest value, no trap
print(wrapped) // -9223372036854775808
print(UInt8.max) // 255 — largest 8-bit unsigned value
print(UInt8.max &* 2) // 254 — 510 wrapped modulo 256
Advanced Operators
Beyond the everyday set, Swift exposes bit-level manipulation for low-level work, and it lets your own types decide what an operator means for them.
Bitwise Operators
Bitwise operators treat an integer as a fixed-width pattern of bits. Each integer type has its own width, so a mask must be written for that width — which is why binary literals and digit separators are common in this style of code.
let a = 0b1100 // 12
let b = 0b1010 // 10
print(a & b) // 8 — AND: bits set in both
print(a | b) // 14 — OR: bits set in either
print(a ^ b) // 6 — XOR: bits set in exactly one
print(~a) // -13 — NOT: every bit inverted (two's complement)
print(a << 2) // 48 — shift left multiplies by 4
print(a >> 1) // 6 — shift right divides by 2
// Underscores group digits, which keeps permission masks readable
let readOnly = 0b100
let chmod = 0b111_101_101
print(chmod & readOnly) // 4 — test a single flag
Custom Operators & Overloading
A type may give new meaning to an existing operator, and you may also invent a new one after declaring its precedence group. Both features shape an API's readability, so use them sparingly and only where the symbol is genuinely idiomatic.
struct Vector2D {
var x: Double
var y: Double
}
// Overload an existing operator for your own type
extension Vector2D {
static func + (lhs: Vector2D, rhs: Vector2D) -> Vector2D {
Vector2D(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func == (lhs: Vector2D, rhs: Vector2D) -> Bool {
lhs.x == rhs.x && lhs.y == rhs.y
}
}
let v = Vector2D(x: 1, y: 2) + Vector2D(x: 3, y: 4)
print(v) // Vector2D(x: 4.0, y: 6.0)
print(v == Vector2D(x: 4, y: 6)) // true
// A brand-new operator needs a precedence group first
precedencegroup DotProductPrecedence {
associativity: left
higherThan: MultiplicationPrecedence
}
infix operator • : DotProductPrecedence
extension Vector2D {
static func • (lhs: Vector2D, rhs: Vector2D) -> Double {
lhs.x * rhs.x + lhs.y * rhs.y
}
}
print(Vector2D(x: 1, y: 2) • Vector2D(x: 3, y: 4)) // 11.0
Common Pitfalls
Integer Division & Remainder
Dividing two integers keeps only the whole part, and the remainder operator follows the sign of the dividend — not the mathematical modulo rule. Convert to a floating-point type when you need a real quotient.
print(7 / 2) // 3 — the fraction is discarded
print(7.0 / 2.0) // 3.5 — floating point keeps the fraction
print(Double(7) / 2) // 3.5 — converting one side is enough
print(7 % 3) // 1
print(-7 % 3) // -1 — sign follows the dividend
// Integer division by zero traps instead of producing infinity
// print(1 / 0) // ❌ runtime trap: division by zero
Floating-Point Comparison
Binary floating point cannot represent most decimal fractions exactly, so == on computed values is unreliable. Compare with a tolerance instead of demanding bit-for-bit equality.
let sum = 0.1 + 0.2
print(sum) // 0.30000000000000004
print(sum == 0.3) // false — rounding, not a logic error
print(abs(sum - 0.3) < 1e-9) // true — epsilon comparison
You now have Swift's full operator vocabulary and the precedence model behind it. Next up: wrapping that logic into reusable pieces in Functions.