In today’s post we’re going to take a look at the different types of operators that are available in Swift. We’ll take a closer look at how the Swift compiler interprets the symbols and tokens we include in our source code and look at how the concepts of precedence and associativity play a key role in shaping the results of our expressions.
Table of Contents
Expressions
In Swift, an expression is any type of statement that when evaluated returns a value, causes a side-effect or both.
In Swift, there are four main types of expression:
- Prefix expressions – These kinds of expressions combine an optional prefix operator with an expression. Prefix operators take a single argument (the expression they will be combined with).
- Postfix expressions – Postfix expressions are the counterparts to the prefix expressions. Postfix expressions combine a postfix operator or other postfix syntax with an expression.
- Binary expressions – Binary expressions combine an infix operator with left-hand and right-hand arguments. You’ll already be relatively familiar with binary expressions.
x + y
ora - b
are just two examples. - Primary expressions – The final type of expression in Swift are the primary expressions. Conceptually these are the simplest kind of expression in Swift and simply provide a way to access values. They can be used as expressions on their own or can be combined with other tokens to make prefix expressions, postfix expressions or binary expressions. For example literal expressions, the identifiers
self
andsuperclass
, closures and the wildcard expression (_
) are all examples of primary expressions. We won’t be covering these types of expressions in this article.
Now that we understand the different types of expression available in Swift, the next thing to look at is how the Swift compiler evaluates these expressions. We’ll do that next.
Abstract Syntax Trees (AST)
When processing an expression in Swift, the Swift compiler interprets the expression by constructing a tree representation of the expression as an Abstract Syntax Tree (AST). An AST is nothing more than a conceptual representation of the expression but is used by the compiler to identify which order elements within the expression should be evaluated. For example something simple like 1 + 2
would be represented in an AST as:
Compound Expressions
But as you know, not all expressions are that simple. Compound expressions are expressions that themselves contain other expressions. For example 1 + 2 + 3 is one example and could be broken down as two expressions:
Similarly, we can also have more complex expressions like:
1 + 2 / 3 * 4 - 5 % 6
To resolve expressions like our last example above, the compiler uses operator precedence to determine how the various elements of the expression are grouped.
Operator Precedence
Operator precedence is similar to the rules you learnt when you were in school and provide a canonical way of ordering and grouping elements within a compound statement. Generally the sequence is (from highest priority to lowest priority):
- exponents and roots
- multiplication and division
- addition and subtraction
Note: If you’re a bit rusty, check out this link for more details on operator precedence. We’ll also be taking a more detailed look the precedence of different operators later in this article.
If we apply the basic rules above, our example would look something like:
1 + ((2 / 3) * 4) - ( 5 % 6 )
In Swift, the precedence of an operator is defined as a decimal integer value in the range 0 to 255 inclusive. They higher the number, the higher the precedence and the sooner the compiler evaluates it.
Precedence alone however is not enough.
Complications occur when we have multiple operators that all have the same level of precedence. For example, consider the expression 6 - 1 + 5
. Addition and subtraction are both defined to have the same level of precedence in Swift so what order should we evaluate the expression in?
If we evaluate the subtraction first ( 6 - 1 ) + 5
we get an answer of 10
. If however we evaluate the addition first 6 - (1 + 5)
we get an answer of 0
. It’s pretty obvious then, that we need an additional mechanism that helps us decide which order the operators are evaluated in. This is where associativity comes in.
Associativity
In programming languages, associativity (or fixity) is a property that determines how operators with the same level or precedence are grouped in the absence of any parentheses.
In Swift, operators can be either left-associative, right-associative or have no associativity. Operators that are left-associative, group their operands from the left of the statement, right-associative operators from the right and operators with no associativity have no defined behaviour when used in sequence in an expression.
Swift Operators
Overall, the operators in Swift can be categorised into three groups:
- Unary Operators
- Binary Operators
- Ternary Operators
In the next few sections, we’re going to dive into the detail of these operators. We’ll look at what each of them do, their precedence and their associativity. We’ll kick off by looking at the Unary Operators.
Unary Operators
The Unary operators, are those operators in Swift that operate on a single operand and can be sub-divided into two sub-groups, the prefix operators and the postfix operators.
Prefix Unary Operators
The Unary Prefix operators appear before their associated operand and as such have white-space to their left. The unary prefix operators available in Swift are:
+
: Unary Plus – A short-hand for multiplying the value of the operand by 1. This operator doesn’t actually change the value of the operand. Instead it can be used to add clarity to your code.
+42
-
: Unary Minus – A short-hand for multiplying the value of the operand by -1
:
let a = 2
let b = -a // Equals -2
!
: Logical NOT – Be careful to use this operator as a prefix operator in order to avoid confusing it with the force-value operator (see later). As a prefix operator, it is used to invert the value of a boolean value so a value of true
becomes false
and visa versa.
let a = true
let b = false
!a // Equals false
!b // Equals true
~
: Bitwise NOT – The Bitwise NOT operator inverts all the bits in a given number. For example the binary number 10101010
would become 01010101
. The prefix operator is written immediately before the operand it operates on:
let input = 0b10101010
let output = ~input // equals 0b01010101
++
: Prefix Increment – A short hand for incrementing the value of the associated operand by 1 before the operand has been evaluated.
var a = 1
b = ++a // 'a' equals 2, 'b' equals 2
--
: Prefix Decrement – A short hand for decrementing the value of the associate operand by 1 before the operand has been evaluated.
var a = 2
var b = --a // 'a' equals 1, 'b' equals 1
Postfix Unary Operators
The postfix operators are the counterpart to the prefix operators. In the case of the postfix operators, they are preceded by their operand (with no white space between them), have whitespace after the operator and are applied after the value in their operand is returned:
++
: Postfix Increment – A shorthand for incrementing the value of the associated operand by 1 after the operand has been evaluated.
var a = 1
b = a++ // 'a' equals 2, 'b' equals 1
--
: Postfix Decrement – A shorthand for decrementing the value of the associated operand by 1 after the operand has been evaluated.
var a = 2
b = a-- // 'a' equals 1, 'b' equals 2
A Note on the Prefix and Postfix Increment and Decrement Operators
Although available in Swift up to and including the early Swift 2.x releases, you shouldn’t spend much time looking at either the prefix or postfix varieties of the increment and decrement operators.
There is currently an accepted proposal to deprecate these operators in later 2.x releases of Swift and remove them completely in Swift 3.0 and beyond due to the additional cognitive burden they bring to those learning Swift for the first time. Further details of the proposal and reasoning can be found here.
Binary Operators
Binary operators are defined to be operators that take two operands and return a single result. They are also termed infix operators as they appear inbetween their operands. The binary operators available in Swift are as follows:
Exponentiative Operators
(Precedence: 160 / Left-associative)
<<
: Bitwise left shift – The bitwise left shift operator is used to shift the bits of the first operand by the number of places indicated by the second operand. Moving the bits left has the effect of multiplying the value of the first operand by 2 to the power of the second operand.
With signed integers, all existing bits are moved to the left and any bits that don’t fit into the type are discarded. Any spaces left by the bits that are moved are filled with zeros. This is known as a logical shift.
For signed integers things are more complicated. The initial bit of a signed number is a sign bit which is used to indicate whether the number is positive (a sign bit set to 0
) or negative (a sign bit set to 1
). The remaining bits are then used to store the value. Positive numbers are stored in the same way as unsigned numbers. Negative numbers however have their initial bit set and the absolute value represented by the value bits is then subtracted from 2
to the power of n
where n
is the number of value bits. This representation is known as two’s complement representation. I’ve written more about this representation in this post.
let unsignedBits : UInt8 = 3 // Equals 00000011 in binary
unsignedBits << 1 // Equals 00000110 in binary
unsignedBits << 5 // Equals 01100000 in binary
unsignedBits << 7 // Equals 10000000 in binary
let positiveSignedBits : Int8 = 3 // Equals 00000011 in binary
let negativeSignedBits : Int8 = -3 // Equals 11111101 in binary
positiveSignedBits << 2 // Equals 00001100 in binary
negativeSignedBits << 2 // Equals 11110100 in binary
>>
: Bitwise right shift - As you might guess, the bitwise right shift operator moves the bits of the first operand to the right by the number of places indicated in the second operand. All the same rules apply as with the bitwise left shift operator except one. When we right-shift a signed value, instead of filling the spaces that are left by the bits being moved with 0
s, the places are filled with the sign bit. This is known as an arithmetic shift.
let unsignedBits : UInt8 = 4 // Equals 00000100 in binary
let signedBits : Int8 = -4 // Equals 11111100 in binary
Multiplicative Operators
(Precedence: 150 / Left-associative)
*
: Multiply - Multiplies the value of the first operand by the value of the second. The compiler will raise an error if the result overflows the capacity of the resulting type.
let a = 10
let b = 2
a * b // Equals 20
/
: Divide - Divides the value of the first operand by the value of the second. Again, the compiler will raise an error if the result overflows the capacity of the resulting type.
let a = 10.0
let b = 2.5
a / b // Equals 4.0
%
: Remainder - The remainder operator works out how many times the second operand will fit completely within the first operand and returns the remainder. The remainder operator can be used with both positive and negative numbers and uses the following equations: a = (b x multiple) + remainder
:
7 % 3 // Returns 1 (7 = (3 * 2) + 1)
-4 % 3 // Returns -1 (-4 = (3 * -1) + -1)
*
: Multiply with overflow - The multiply with overflow operator multiples the value of the first operand by the value of the second. If the result overflows the capacity of the result type, the result wraps.
let a = Int8.max // Equals 127
a &* 2 // Equals -2
: Bitwise AND - Performs a bitwise AND on each of the bits of the first operand with each of the bits of the second operand. If the bit is set in both the first operand and the second operand, the corresponding bit in the return value is set to
1
otherwise it is set to 0
.
let a : UInt8 = 0b11001100
let b : UInt8 = 0b10101010
a & b // equals 0b10001000
Additive Operators
(Precedence: 140 / Left-associative):
+
: Addition - When used with numbers it performs arithmetic addition much like you learnt at school. Adds the value of the second operand to the value of the first. If the two operands are strings, the addition operator concatenates the two strings:
1 + 2 // equals 3
”Hello " + "World" // Equals "Hello World"
-
: Subtraction - Simple arithmetic subtraction. Subtracts the value of the second operand from the value of the first.
2 - 3 // equals -1
Note: Unlike in C or some other languages, the arithmetic operators in Swift (which include the addition and subtraction operators above), do not overflow by default. If you want to use value overflow you need to explicitly opt-in using the corresponding overflow operators:
+
: Addition with overflow - Adds the value of the second operator to the value of the first. This time, if the magnitude of the resulting value exceeds the capacity of the result type, the value will wrap.
var maxInt = Int8.max // Equals 127
maxInt &+ 1 // Equals -128
-
: Subtraction with overflow - The counterpart to the addition with overflow operator, the subtraction with overflow operator subtracts the value of the second operand from the value of the first operand. If the resulting value exceeds the capacity of the result type, the value wraps.
var minInt = Int8.min // Equals -128
minInt &- 1 // Equals 127
|
: Bitwise OR - The bitwise OR operator takes two operands and compares that individual bits of it's two operands. Where the a bit in either number is set to 1
a 1
is returned in the output number, otherwise a 0
is returned.
let operand1 : UInt8 = 0b10101010
let operand2 : UInt8 = 0b11110000
let result = operand1 | operand2 // Equals 0b11110000
^
: Bitwise XOR - Although the names sound similar, the bitwise exclusive OR is different to the bitwise OR we just looked at. Again it accepts two operands and compares individual bits in those operands. However, this time, the operator sets a bit to 1
in the returned value if, and one and only one of the two operands has the bit set, otherwise it returns zero.
let operand3 : UInt8 = 0b10101010
let operand4 : UInt8 = 0b11110000
let result2 = operand3 ^ operand4 // Equals 0b01011010
Range Operators
(Precedence: 135 / No associativity):
...
: Closed Range - The closed-range operator is particularly useful for iterating over all elements of a range such as in a for-loop. It defines a range of values running from the first operand up to and including the second operand. The first operand must not be greater than the second operand though.
for i in 1...3 {
print(i)
}
// 1
// 2
// 3
..<
: Half-open range - Defines a range of values running from the first operand up to but not including the second operand. It is said to be half open as only one of the two operands is included within the range. The half-closed range operator is particularly useful for when iterating over the contents of zero-based collections such as arrays where it is useful to count up to but not including the number of elements in the collection. When using the half-open range operator the value of the first operand must never be greater than that of the second and if the first operand is equal to the second, the range will be empty.
let colors = ["Red", "Green", "Blue"]
for i in 0 ..< colors.count {
print(colors[count])
}
// Red
// Green
// Blue
Cast Operators
(Precedence: 132 / Left-Associative):
is
: Type Check - The type check operator is used to check whether an it's first operand is of the type specified by the second operand. The operator returns 'true' if the instance is is of the type specified in the second operand or is a subclass of that type and returns 'false' otherwise. The operator works with all types.
class ParentClass {}
class FirstChildClass : ParentClass {}
class SecondChildClass : ParentClass {}
var myArray = [FirstChildClass(), SecondChildClass()]
for (index, item) in myArray.enumerate() {
if item is FirstChildClass {
print("Item \(index) is an instance of FirstChildClass.")
} else if item is SecondChildClass {
print("Item \(index) is an instance of SecondChildClass.")
}
}
// Item 0 is an instance of FirstChildClass.
// Item 1 is an instance of SecondChildClass.
as
, as?
and as!
: Type Cast - The type cast operator is used to try to upcast or downcast a constant or variable to an instance of a particular subclass. On the face of it, the constant or variable refers to an instance of a certain type but behind the scenes, that type may be an instance of a subclass of that type. The operator comes in three forms. The as
operator is used to upcast a value from one type to a more generic type. The as?
operator optionally downcasts a value to of a particular type to a more specific type returning nil
if the downcast fails or a value of the more specific type if the downcast was successful. Finally the as!
operator, also performs a downcast but this time forces the cast, raising an error if the downcast fails.
class Vehicle {}
class Bike : Vehicle {}
let bike : Bike = Bike()
let vehicle : Vehicle = Vehicle()
let otherVehicle : Vehicle = Bike()
let genericVehicle = bike as Vehicle // Upcast
let forcedBike = otherVehicle as! Bike // Forced Downcast - Raises exception if it fails.
let otherBike = otherVehicle as? Bike // Optional Downcast - Returns an optional
Comparative Operators
(Precedence: 130 / No association):
<
: Less than - Returns a boolean value indicating whether the first operand is less than the second operand.
let a = 1
let b = 2
let c = 1
a < b // Equals true
a < c // Equals false
<=
: Less than or equal to - Returns a boolean value indicating whether the first operand is less than or equal to the second operand.
a <= b // Equals true
a <= c // Equals true
>
: Greater than - Returns a boolean value indicating whether the first operand is greater than the second operand.
b > a // Equals true
c > a // Equals false
>=
: Greater than or equal to - Returns a boolean value indicating whether the first operand is greater than or equal to the second operand.
7 >= 5 // true
7 >= 7 // true
7 >= 9 // false
==
: Equal - Returns a boolean value indicating whether the first and second operands have equal value. Returns true
if and only if the first operand has the same type as the second operand and they have identical value. In the case of collections, it returns true
if both collections contain the same elements.
6 == 5 // false
5 == 5 // true
!=
: Not equal - Returns a boolean value indicating whether the first and second operands do not have equal value.
6 != 5 // true
5 != 5 // false
===
: Identical - Returns a boolean value indicating whether the first and second operands are in fact the same object.
class firstClass {}
class secondClass {}
let a = firstClass()
let b = secondClass()
let c = a
a === b // Returns false
a === c // Returns true
!==
: Not identical - Returns a boolean value indicating whether the first and second operands are in fact not the same object.
a !== c // Returns false
a !== b // Returns true
~=
: Pattern match - Returns a boolean value indicating whether the second operand matches the pattern provided in the first operand.
0...10 ~= 10 // Returns true
Conjunctive Operators
(Precedence: 120 / Left-associative):
: Logical AND - Performs a logical AND of the first operand with the second operand. If the first operand is
false
, it returns false
otherwise it evaluates the second operand and returns it’s boolean value.
let a = true
let b = false
a && b // Equals false (evaluate the second operand)
Disjunctive Operators
(Precedence: 110 / Left-associative):
||
: Logical OR - Performs a logical OR of the first operand with the second operand. If the first operand is true
, it returns true
. Otherwise it evaluates the second operand and returns it’s boolean value.
let c = true
led d = false
c || d // Equals true (doesn't evaluate the second operand)
Nil Coalescing Operator
(Precedence: 110 / Right-associative):
??
: Nil coalescing - If the first operand is nil
, returns the value of the second operand otherwise return the value of the first.
var opt : Int?
opt ?? 10 // equals 10 because opt is nil.
Assignment
(Precedence: 90 / Right-associative):
=
: Assign - Assigns the second operand to the first operand. One thing to note is that the assignment operator in Swift is different from the assignment operator in other C-based languages, in that it does not return a value from the assignment so you can't use it in place of boolean expressions.
let b = 4
a = b // 'a' equals 4
// The following is illegal in Swift
if (a = b) {
// Do stuff...
}
Compound Assignment
(Precedence: 90 / Right-associative)
The compound assignment operators combine the assignment operator (=
) with another operation. The compound assignment operators make use of the equivalent non-compound versions we saw earlier. The compound assignment operators in Swift are:
+=
: Add and assign - Add the second operand to the first operand and assign the result to the first operand.
var a = 1
let b = 2
a += b // 'a' equals 3
-=
: Subtract and assign - Subtract the second operand from the first operand and assign the result to the first operand.
var a = 3
let b = 1
a -= b // 'a' equals 2
*=
: Multiply and assign - Multiply the first operand by the second and assign the result to the first operand.
var a = 3
let b = 2
a \*= b // 'a' equals 6
/=
: Divide and assign - Divides the first operand by the second operand and assigns the result to the first operand.
var a = 4
var b = 2
a /= b // 'a' equals 2
%=
: Remainder and assign - Perform integer division of the first operand by the second. Assign any remainder to the first operand.
var a = 7
let b = 3
a %= b // 'a' equals 1
<<=
: Left bit shift and assign - Left bit shift the first operand by the number of places indicated in the second operand. Assign the result to the first operand.
var a : UInt8 = 0b11110000
a <<= 2 // equals 0b11000000
>>=
: Right bit shift and assign - Right bit shift the first operand by the second operand and assign the result to the first operand.
var a : UInt8 = 0b11110000
a >>= 2 // equals 0b00111100
=
: Bitwise AND and assign - Perform a bitwise AND of the first operand with the second operand. Assign the result to the first operand.
var a : UInt8 = 0b10001000
let b : UInt8 = 0b11001100
a &= b // 'a' equals 0b10001000
|=
: Bitwise OR and assign - Perform a bitwise OR of the first operand with the second operand. Assign the result to the first operand.
var a : UInt8 = 0b10001000
let b : UInt8 = 0b11001100
a |= b // 'a' equals 0b11001100
^=
: Bitwise XOR and assign - Perform a bitwise XOR of the first operand with the second operand. Assign the result to the first operand.
var a : UInt8 = 0b10001000
let b : UInt8 = 0b11001100
a |= b // 'a' equals 0b01000100
=
: Logical AND and assign - Perform a logical AND of the first operand with the second operand and assign the result to the first operand.
var a = true
let b = false
a &&= b // 'a' equals false
||=
: Logical OR and assign - Perform a logical OR of the first operand with the second operand and assign the result to the first operand.
var a = true
let b = false
a ||= b // 'a' equals true
Ternary Operators
Like C, the Swift language only has a single ternary operator the ternary conditional operator:
Ternary Conditional
(Precedence: 100 / Right-associative):
?:
: Ternary conditional - The ternary conditional operator takes three operands. The first of which is a boolean value. If this first operand evaluates to true
, the ternary conditional returns the second operand otherwise it returns the third operand.
var a = true
a ? 10 : 20 // equals 10
var a = false
a ? 10 : 20 // equals 20
Member Functions
In addition to the standard operators above, the Swift language also provides some additional operators:
.
: Member access
?
: Optional
!
: Force the value
[]
: Subscript
[]=
: Subscript assignment
Were not going to go into these in this post but I’ll explain them in other articles as we come across them.
Summary
With that then, I’ll wrap up for today. In this post we’ve gone over the main operators that are available to you in Swift. We’ve looked at precedence and associativity and seen how they combine to influence the order that compound expressions are evaluated by the Swift compiler. The bottom line is that having a good understanding of the operators that are available in Swift is essential to getting the best from the language so I hope that this article has helped raise your awareness to the range of operators that are available as well as how they are used. As ever, if I’ve made any mistakes or you have any questions please get in touch.