Expressions & Operators

An expression is any piece of code that produces a value; an operator is the symbol that combines values into a new one. Swift operators are strict: they will not silently mix types, they report overflow instead of wrapping, and your own types can redefine them.

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.

GroupOperatorsExample
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.