I first learned Swift in 2017, which for me was ages ago. Back then it was at version 3.1. I'm much more familiar with Rust, having been studying and using it now for about 8 months. So when I went back to check out Swift again this week, I was really surprised by three things:
- Swift is now version 5.3 (two major versions!)
- Swift has finally been ported to Windows!
- Wow, Swift code sure looks a lot like Rust code!
I felt it might be fun to sit down and take notes while reading through the official Swift Book to "freshen up" on the language. Being very much into Rust, I thought it would also be fun to include in my notes any comparisons I could draw between Swift and Rust. It took a few days, and by the end I had quite a few notes written down. So I decided to clean them up a bit and post them here!
The following is essentially a boiled-down comparison of Swift (version 5.3) with respect to Rust (edition 2018). It follows along the chapters of the official Swift Book. Swift and Rust are very similar, so in general, if a specific point of comparison isn't mentioned here, it may be that the two languages are the same on that point, or "close enough" that I felt it didn't matter. For example, true
and false
are the same for both Swift and Rust, and the syntax for various kinds of string literals are largely the same, so I didn't bother to write them down. On the other hand, something not listed here could be something I didn't consider or know about at the time, in which case please let me know in the comments. Thank you in advance!
If any information below is not completely verified or tested, a question-mark ?
punctuation will be used. This is an open invitation to anyone who can verify the information (whether correct or incorrect) and provide evidence one way or another.
First of all, I found a few general topics of interest that weren't covered directly by the Swift Book. I grouped them together to list here:
-
Swift Package Manager — analogous to cargo in Rust, being a standard package management system for the language.
- Note that unfortunately, as of the time of this writing, the Swift Package Manager is not available for Windows, so ad-hoc solutions based on tools such as CMake and Ninja are often used to construct build systems for Swift projects on Windows instead.
-
Swift Standard Library — this is tightly coupled with the compiler and facilitates the runtime and various types and functions essential to various aspects of the language. It also provides support for features not covered by the book:
-
Serialization — a set of protocols analogous to serde:
-
Encodable
—serde::Serialize
-
Decodable
—serde::Deserialize
-
Encoder
—serde::Serializer
-
Decoder
—serde::Deserializer
-
- C Interoperability — various types and functions analogous to Rust's Foreign Function Interface (FFI).
-
Serialization — a set of protocols analogous to serde:
-
Foundation — this library originates from the Apple ecosystem (iOS, macOS, etc.) and is typically bundled with Swift due to providing access to essential operating system services, which in Rust are typically either built into the Rust standard library or are available from the Rust community's crate registry:
- Date and time management
- Localization (formatting of locale-aware string representations)
- File system — creating, reading, writing, or examining files and directories
- JSON encoding/decoding
- Processes and threads
- Networking
-
Dispatch (also known as "Grand Central Dispatch" (GCD) — this library originates from the Apple ecosystem (iOS, macOS, etc.) and is typically bundled with Swift due to providing facilities for asynchronous programming, analogous to futures/async in Rust along with
futures-rs
and runtimes such astokio
,async-std
, andsmol
available from the Rust community's crate registry.
TL;DR — Overall Differences
I wrote quite a few notes down here, perhaps too many for some people. So here's a summary of "what's important" when comparing Swift and Rust:
- Swift broadly categorizes types into "value" and "reference" types, hiding most of the implementation details of references from the programmer. Rust makes references a first-class type and opens the entire type system up for the programmer, for better or worse.
- Swift supports class inheritance, while Rust does not.
- Swift uses the throwing and catching of exceptions for error handling, while Rust encourages the use of enumerated types to return "success or failure" variants from fallible operations.
- Most code constructs in Rust are expressions, whereas Swift has a more traditional separation between "statement" and "expression".
- Unlike Rust, Swift offers a comprehensive "properties" feature set (i.e. computed properties, property observers, projected values) similar to C#.
- Many language features that have their own keywords and syntax in Swift are handled through Rust's type system (i.e. optional types, boxing, references, operator overloading).
- Rust includes direct syntax for asynchronous programming (
async
,await
), while Swift does not, relying on adjunct library facilities to support it.
Opinion Section
I've tried to remain as objective as possible while writing this post. However, there were a few subjective things I felt during this process. Skip to the next section unless you're comfortable with a bit of biased opinions.
- Both languages aim to be general purpose systems programming languges. However, Swift has solid Apple roots and thus suffers from two main weaknesses:
- Although Swift has strong corporate (Apple) backing, its overall community is weak in comparison to Rust. Being relatively unencumbered, Rust has a more "well-rounded" base and robust community, as evidenced by:
- Wider range of target platforms which support it.
- Easier procedure to obtain, set up, and operate its toolchain.
- Richer online environment for publishing and reusing libraries.
- While Rust includes more advanced features such as threading, networking, and asynchronous coding directly in its standard library, Swift leverages adjunct libraries such as Foundation and GCD which were designed for Apple's ecosystem specifically. Supporting other platforms entails "porting" these libraries after the fact or risking "crippling" the language for platforms which don't support the Apple libraries.
- Although Swift has strong corporate (Apple) backing, its overall community is weak in comparison to Rust. Being relatively unencumbered, Rust has a more "well-rounded" base and robust community, as evidenced by:
- Rust and Swift both aim to be "safe", referring to language design and compiler features which prevent and/or detect errors at compile-time. However, they have fundamentally different approaches:
- Rust has invested in the "borrow checker" approach where references are first-class types and the concept of "lifetimes" rises to the conscious level of the programmer.
- Cost: more difficult for the programmer to learn, due to this approach being non-traditional and the syntax being more elaborate, requiring more practice and different ways of thinking about and designing code.
- Gain: faster, more efficient code which can leverage both the stack and heap, allowing more sophisticated zero-copy designs and optimizations.
- Swift took a traditional "pass by copy or reference" approach, with references being inherently reference-counted.
- Gain: very similar to traditional programming languages, with semantics already familiar to most programmers, making the language easier to learn and use.
- Cost: since the more traditional approach is taken, many of the more traditional drawbacks remain, such as unnecessary copying and indirection; although arguably the compiler has opportunities to optimize at least some of these away.
- Rust has invested in the "borrow checker" approach where references are first-class types and the concept of "lifetimes" rises to the conscious level of the programmer.
- In the realm of Object-Oriented Programming, Rust takes a heavy-handed approach encouraging the Composition over inheritance principle by not supporting inheritance at all. Although Swift supporting "protocols" (traits, interfaces) makes the language more "composition-friendly", it also includes class inheritance. This makes Swift friendlier to programmers used to traditional object-oriented programming techniques using inheritance, at the cost of complicated rules for things such as initializers and access control.
Now, with all that out of the way, let's focus on language comparisons drawn by following along in the Swift Book and comparing to Rust. In many places, I will abbreviate by showing Swift vs. Rust side-by-side with an "em (long) dash" (—) between. Sorry if the formatting isn't too great; it works for me. ¯\_(ツ)_/¯
The Basics
Fundamental types
-
Int
—isize
-
UInt
—usize
-
Int8
,Int16
,Int32
,Int64
—i8
,i16
,i32
,i64
-
UInt8
,UInt16
,UInt32
,UInt64
—u8
,u16
,u32
,u64
-
Uint32.min
,Uint32.max
—u32::MIN
,u32::MAX
-
Double
,Float
—f64
,f32
- Literals can be in hexidecimal:
0xFp-2
== 3.75
- Literals can be in hexidecimal:
-
Bool
—bool
Optionals
-
T?
—Option<T>
-
nil
—None
-
value == nil
,value != nil
—value.is_none()
,value.is_some()
- Optional binding:
-
let possible_value: Int? = Int("123")
—let possible_value = "123".parse().ok()
-
if let value = possible_value
—if let Some(value) = possible_value
-
- You can have many optional bindings in a single conditional:
if let first = Int("4"), let second = Int("42"), first < second
- Unwrapping values (
!
—unwrap()
):-
let value: Int = Int("123")!
—let value = "123".parse().ok().unwrap()
or justlet value = "123".parse().unwrap()
-
- "Implicitly unwrapped optionals" are types that are unwrapped automatically if used in a non-optional context, but left optional otherwise:
-
let x: Int! = Int("123")
<-- adding!
to type makes it an implicitly unwrapped optional -
let y: Int = x
<-- implicitly unwrapped -
if let z = x
<-- left optional, tested in conditional
-
Constants and Variables
- "Constant" is an immutable value, with the same syntax:
-
let name = 10
—let name = 10
-
- "Variable" is a mutable value:
-
var name = 10
—let mut name = 10
-
- You can have multiple declarations on a single line:
var x = 0, y = 1, z = 2
- Type annotations are the same, except you can define multiple values that share the same type annotation:
var x, y, z: Int
- Constant and variable names can contain almost any character:
let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"
- No name shadowing allowed
-
print
is a combinationprintln!
andprint!
where it behaves likeprintln!
by default but you can set a customterminator
argument to""
to be likeprint!
or something else. - Interpolation is intrinsically supported for strings:
print("The answer is \(add(40, 2))")
- Note that the interpolation can be any expression, including literals and function calls.
Comments
- Comments are the same, including ability to nest
/* */
style comments.
Semicolons
- Swift only requires a semicolon at the end of a statement if another statement follows it on the same line.
Type casts
-
(Double)name
—name as f64
- Some type casts return optionals:
let possible_number: Int? = Int("123")
Type aliases
-
typealias AudioSample = UInt16
—type AudioSample = u16
Tuples
- Tuples are the same except you can also name the parts of tuple literals:
let status = (statuscode: 200, description: "OK")
print("Status: \(status.statuscode) (\(status.description))")
Error Handling
Here we have the first fundamental difference between Swift and Rust. In Rust, errors are handled by representing them as values of the Result::Error
type variant and returning them up the call chain through Result
values. In other words, successful return values and error return values essentially follow the same path out of function calls. In Swift, errors are represented by values conforming to the Error
protocol (trait) that are "thrown" at one point and "caught" (or not) somewhere up the call chain. They follow a fundamentally separate path from successful return values.
A function that can throw an error must have the keyword throws
placed in its declaration (before the body):
-
func can_throw() throws {
vsfunc does_not_throw() {
When calling such a function, you must prefix the call with the try
keyword:
try can_throws()
To automatically catch an error at the call site and convert it to an optional, use try?
(equivalent to Result::ok()
on the return value of a function in Rust):
-
let x: Int? = try? something()
—let x: Option<isize> = something().ok()
To automatically catch an error at the call site and terminate with a runtime error, use try!
(equivalent to Result::unwrap()
on the return value of a function in Rust):
-
let x: Int = try! something()
—let x: isize = something().unwrap()
Throw errors with the throw
statement:
throw some_error
Unless caught at the call site with try?
or try!
, errors "unwind the stack" or cause code execution to return out of functions until explicitly "caught". You catch errors outside the call site using a "do-catch" statement:
do {
try something()
} catch some_error {
handle_specified_error(some_error)
} catch {
handle_unspecified_error(error)
}
Note that the special name error
is available in the unspecified error handling block.
Assertions and Preconditions
-
assert
andprecondition
are like theassert!
macro, except thatassert
is only executed for a "Debug" build. -
preconditionFailure
is essentially apanic!
.
Basic Operators
- Swift has the ternary operator
a ? b : c
, unlike Rust where you would instead use a conditional expressionif a { b } else { c }
. -
String
type implements the+
operator for concatenation; in Rust the+
operator (orString::push_str
) requires the right-hand side to be a slice. -
Bool
type does not support comparison, unlikebool
in Rust. - "Nil-Coalescing Operator":
a ?? b
—a.unwrap_or_else(|| b)
- Note that
b
is not evaluated unlessa
isnil
, which is why it maps toOption::unwrap_or_else
rather thanOption::unwrap_or
.
- Note that
- Range operators
- Closed range:
1...5
—1..=5
- Half-open range:
1..<5
—1..5
- One-sided range:
1...
,...1
—1..
,..1
- There isn't a Swift equivalent of the Rust
RangeFull
(unbounded range..
)?
- Closed range:
Strings and Characters
- Swift strings are "composed of encoding-independent Unicode characters" and "built from Unicode scalar values", whereas in Rust
String
andstr
are UTF-8 encoded. - A Swift
Character
is a single extended grapheme cluster (sequence of one or more Unicode scalars), whereas a Rustchar
is a single Unicode scalar. For example, in Rust,"café".chars().count()
is 5 and"café".len()
is 6, whereas in Swift,"café".count
is 4.- The letter
é
is a single grapheme cluster composed of the Unicode scalar "LATIN SMALL LETTER E" (U+0065), which is one byte when UTF-8 encoded, followed by the Unicode scalar "COMBINING ACUTE ACCENT" (U+0301), which is two bytes when UTF-8 encoded.
- The letter
- In Swift you can modify characters in strings by concatenating Unicode scalars together. For example,
"e" + "\u{301}" == "é"
. - Swift
String
has an associated typeString.Index
representing the position of aCharacter
in theString
.-
String
methodsindex(before:)
,index(after:)
, andindex(_:offsetBy:)
are used withString
propertiesstartIndex
andendIndex
to constructString.Index
values. - The
indices
property ofString
in Swift iterates over characters just likestr::chars
in Rust.
-
- Indexing or slicing a string returns a
Substring
which is similar tostr
(holds a reference to theString
it was sliced from).let beginning = line[..<index]
- Converting
Substring
toString
is a type cast
- You can encode
String
to UTF-8 or UTF-16 with theutf8
orutf16
properties which areString.UTF8View
orString.UTF16View
and are iterable as a sequence ofUInt8
orUInt16
. - You can get the Unicode scalar values of a
String
with theunicodeScalars
property which isUnicodeScalarView
and is iterable as a sequence ofUnicodeScalar
. - Multi-line strings drop indented space, which Rust doesn't do inherently, but you can get the equivalent behavior if you use the indoc crate.
- Swift strings are value types, which are copied when passed to functions. There is no type distinction between mutable and immutable strings, like there is in Rust (with
String
being mutable andstr
(slice) being immutable).- Note that the book mentions, "Swift’s compiler optimizes string usage so that actual copying takes place only when absolutely necessary", which implies the implementation is perhaps closer to
Cow<'a, str>
?
- Note that the book mentions, "Swift’s compiler optimizes string usage so that actual copying takes place only when absolutely necessary", which implies the implementation is perhaps closer to
- "Extended String Delimiters" are similar to "raw string literals" in that special characters can be placed in the string without invoking their effect. However, in Swift you can "manually invoke an effect" by adding a matching number of
#
pound signs following the\
backslash escape character:-
#"Line 1\nLine 2"#
—r#"Line 1\nLine 2"#
-
##"Line 1\nLine 2"##
—r##"Line 1\nLine 2"##
-
#"Line 1\#nLine 2"#
breaks the line and is equivalent to"Line 1\nLine 2"
in Swift or Rust; no way to do this with a Rust raw string literal?
-
Collection Types
There are three primary collection types in Swift:
-
Array<T>
or[T]
(shorthand) —Vec<T>
-
Set<T>
—HashSet<T>
-
Dictionary<Key, Value>
or[Key: Value]
(shorthand) —HashMap<K, V>
The only ordered collection type is the Array
.
- To order the keys of a
Set
, use thesorted()
method, which returns anArray
of the keys sorted in order. - To order the contents of a
Dictionary
, use thesorted()
method of thekeys
orvalues
properties, which return anArray
of the keys or values sorted in order.
The keys of Set
and Dictionary
need to be hashable, just as in Rust. The protocol Hashable
in Swift is like the trait Hash
in Rust.
Using Arrays
-
[Int32]()
—Vec::<i32>::new()
-
Array(repeating: 0.0, count: 3)
—vec![0.0; 3]
-
Array
supports the+
and+=
operators to concatenate and append, whereas in Rust you would have to start with the left-hand side and then eitherappend
orextend
it. - You can use subscript syntax to replace a value at a given index, just as in Rust:
values[4] = "apples"
- You can also use subscript syntax to splice:
-
values[4...6] = ["apples", "oranges"]
—values.splice(4..=6, ["apples", "oranges"].iter().cloned());
-
-
removeLast()
—Vec::pop
Using Sets
-
Set<Int32>()
—HashSet::<i32>::new()
-
let values: Set<Int> = [1, 2, 3]
—let values = hashset!{1, 2, 3}
(using the maplit crate) -
count
—HashSet::len
-
substracting(_:)
—HashSet::difference
-
isStrictSubset(of:)
andisStrictSuperset(of:)
are likeisSubset(of:)
andisSuperset(of:)
except they returnfalse
if the sets are equal.
Using Dictionaries
-
[Int32: String]()
—HashMap::<i32, String>::new()
-
[:]
—HashMap::new()
-
let dict = [1: "apples", 2: "oranges", 3: "bananas"]
—let dict = hashmap!{1 => "apples", 2 => "oranges", 3 => "bananas"}
(using the maplit crate) -
count
—HashMap::len
-
dict.updateValue("answer", forKey: 42)
(ordict[42] = "answer"
if the optional old value is not needed) —dict.insert(42, "answer")
-
dict[42]
—dict.get(42)
-
dict[42]!
—dict[42]
Control Flow
- Control flow uses statement semantics, so they can't be used in expression contexts.
- For example, this is valid in Rust but not Swift:
let x = if y { 1 } else { 2 }
(one could use the ternary operatory ? 1 : 2
instead) - Another consequence of this is that
break
statements cannot accept an argument.
- For example, this is valid in Rust but not Swift:
- Swift has an additional "repeat-while" syntax similar to the "do-while" syntax in C, but lacks the
loop
infinite loop andwhile let
syntax. - Label names for loops (where a
continue
orbreak
in a nested loop can jump out to a labelled outer loop) must begin with a quote'
character in Rust, but not in Swift. - A Swift
switch
is similar to a Rustmatch
except:- The
case
keyword is present in front of each arm, similar to C. - The
default
keyword, withoutcase
, takes the place of_
to match any cases not otherwise covered. - The body of each case must have at least one statement.
- Alternative patterns are separated by comma
,
rather than pipe|
. - Value binding in patterns requires placing the
let
keyword before the name to bind:-
case (let x, 0):
—(x, 0) =>
-
- Swift uses the
where
keyword in place ofif
for "additional conditions" (called "match guards" in Rust).-
case let (x, y) where x == y:
—(x, y) if x == y =>
-
- Swift has an additional control transfer statement
fallthrough
you can use as the last statement of acase
in aswitch
in order to mimic the "fall through" behavior of aswitch
in C where the lack of abreak
causes execution to flow from the end of one case to the beginning of the next case.
- The
- Swift has an additional "guard" syntax intended for use in "early exit" scenarios. It looks simiar to an
if
statement, exceptif
is replaced byguard
, theelse
keyword is placed before the body, and any value bound to the condition (withguard let
) is scoped to the block containing the guard. The body statements are only executed if the condition isfalse
, and typically contain an "early exit" return. - Conditional compilation in Swift is done using a special conditional you can use in
if
andguard
statements, as opposed to thecfg
attribute in Rust.- The only special conditional mentioned in the book for use in conditional compilation is the
#available
condition used to conditionally compile based on the target platform.
- The only special conditional mentioned in the book for use in conditional compilation is the
Functions
-
func
—fn
- If the entire body of the function is a single expression, the function implicitly returns that expression. Rust supports this, but also allows there to be statements before the expression, which Swift doesn't support.
- Arguments are "labelled", and such argument labels are considered part of the function name. This gives them an "Objective-C" flavor designed for compatibility with that language.
- For example:
func speak(toPerson person: String, what speech: String)
- The name for this function overall is considered to be
speak(toPerson:what:)
.
- The name for this function overall is considered to be
- If unspecified, the label is the same as the name:
-
func speak(person: String, what speech: String)
is the same asfunc speak(person person: String, what speech: String)
-
- A label may be omitted by replacing it with an underscore (
_
) in the function declaration.-
func speakTo(_ person: String, what speech: String)
- In this case the function name is considered to be
speakTo(_:what:)
- In this case the function name is considered to be
-
- Labels not omitted must be used when calling the function.
- For example:
speakTo("Frank", what: "Hello!")
- For example:
- Although names must be unique, labels need not be (but usually are for readability).
- For example:
- Arguments can have default values specified in the function declaration by adding after the type an equals sign (
=
) followed by a value. If an argument has a default value, it can be omitted when calling the function. - Functions support variadic parameters. If the type of a parameter is followed by
...
, the actual type of the parameter will be anArray
of the specified type, while callers provide the values for the array without using array syntax.- Note that a function can have at most one variadic parameter, but it doesn't necessary need to be the last parameter?
func average(_ numbers: Double...) -> Double {
/* body omitted for brevity */
}
average(1, 2, 3)
average(1.2, 3.5)
- Parameters are constants (immutable) by default.
- Parameters are passed by value (copied, as in the
Copy
trait in Rust) if they are fundamental types, strings, or structures. Otherwise, they are passed by reference. - Placing the
inout
keyword before a parameter type in the function declaration gives the parameter a "copy-in copy-out" semantic (which may be optimized into "pass by mutable reference", but you shouldn't count on this).- When the function is called, the parameter is copied.
- During the function call, the parameter is mutable and should be considered separate from the original value (although it may be optimized to be the same address in memory).
- When the function returns, the parameter is copied back to caller, replacing the original value.
- For the purpose of identifying the type of a function with no return value,
Void
takes the place of()
as the type returned. - Functions passed as parameters are declared without any keyword like
func
:-
func useFunction(_ function: (Int) -> Int)
—fn use_function(function: Fn(isize) -> isize)
-
- Unlike in Rust, there is no distinction in Swift between "by-value" (
FnOnce
), "mutable" (FnMut
), "immutable" (Fn
), or "function pointer" (fn
) closure/function types. - A function declared inside another function is technically a closure in Swift (has access to all values in the scope of the outer function), whereas in Rust such a function is not a closure (although you could make the inner function a closure in Rust to do the same thing).
Closures
-
{(x: Int, y: Int) -> Int in x * y}
—|x: isize, y: isize| x * y
-
{x, y in x * y}
—|x, y| x * y
- Argument names can be omitted entirely if types can be implied; in this case special names formed by
$
followed by argument index represent each argument-
{$0 * $1}
—|x, y| x * y
-
- Operator methods are probably the shortest possible syntax for a closure. For example,
names.sorted(by: >)
sortsnames
using the>
operator of the type of the items innames
. - Swift has a "trailing closure" syntax where you can move a closure parameter to a block immediately after the parentheses of the function call. For example,
names.sorted(by: { $0 > $1 })
can be written asnames.sorted() { $0 > $1 }
instead.- The argument label for the first trailing closure is omitted. If there are additional trailing closures, they follow the first trailing closure body and are proceeded by their argument labels.
- If there are no arguments besides the trailing closure(s), the parentheses of the function call itself can be omitted as well. For example:
names.sorted{ $0 > $1 }
- Closures capture constants and variables from the surrounding context by reference. As an optimization, the compiler may capture them by value instead, if they are not mutated by the closure or any code executed after the closure is created.
- Closures are reference types (may be obvious, but the book has a special section to emphasize this point).
- Closures can have a "capture list" where you explicitly control what values from the surrounding context are captured and how they are captured.
- If present, the capture list comes first inside the opening curly brace (
{
) of the closure. - Inside square brackets, place a comma-separated list of items, where each item is a value to capture, optionally with the
weak
orunowned
keyword in front to specify whether the reference should be weak or not counted at all (respectively). Each value may also be bound to a name local to the closure. For example:-
[self]
— capture valueself
by strong reference -
[weak self]
— capture valueself
by weak reference -
[weak delegate = self.delegate]
— capture valueself.delegate
by weak reference nameddelegate
.
-
- If present, the capture list comes first inside the opening curly brace (
- A closure which "escapes" a function (called after the function returns) must be marked with
@escaping
before the closure's type in the function parameter list. - Marking a closure parameter with
@autoclosure
before the closure's type in a function parameter list allows you to call the function and pass an expression that is automatically wrapped by a closure. In other words, the expression passed to the function is "lazily evaluated", or not evaluated until and unless the closure is called by the function.
Enumerations
- When listing variants of enums, each line must begin with the
case
keyword.- Multiple variants may be declared per line, if they are comma-separated.
- The syntax for specifying a variant is
TypeName.variant
or just.variant
ifTypeName
can be inferred. - By adding
: CaseIterable
after the name of an enum in its declaration (in other words, specifying that the enum conforms to theCaseIterable
protocol), Swift will add anallCases
property which is a collection of all the variants. - Enum values can be printed without implementing any protocol.
- In a
switch
on an enum, the associated values (if any) are bound to names by placinglet
followed by a name in the case pattern, or by placing a singlelet
immediately following thecase
keyword. - As an alternative to associated types, enums can handle "raw values" by adding a colon followed by raw value type after the name of the enum. Each variant gets a corresponding raw value, which can be either assigned by default or assigned by providing a literal value after the variant name with an equals sign (
=
) between the name and value. Raw value types can be characters, strings, integers, or floating-point numbers. Variants not assigned an explicit raw value get one assigned according to their position and type:- Integers default to
0
for the first variant or the value of the previous variant plus one. - Strings default to the name of the variant itself.
- Raw value type enums automatically get an initializer method that takes a raw value of the proper type and returns an optional enum value (the variant that corresponds to the given raw value, or
nil
if no there is no corresponding variant for the raw value).
- Integers default to
- An enum variant can have an associated value that has the same enum type, as long as the variant is marked with the
indirect
keyword before thecase
of the variant. This causes Swift to add indirection to the variant, just as you would have to do manually usingBox<T>
in Rust to achieve the same thing.
Structures and Classes
- Both structures and classes in Swift are like structures in Rust.
- Classes have additional capabilities that structures don't have:
- Inheritance (not directly supported in Rust at all)
- Type casting
- Deinitializers (analogous to the
Drop
trait in Rust) - Reference counting (analogous to the
Rc
andArc
types in Rust)- Structures (and enumerations) are "value types", meaning they are copied when assigned or passed to functions.
- Classes are "reference types" meaning they behave as if implicitly wrapped in an
Rc
/Arc
, where a reference is added when assigned or passed to functions, and the value is only deallocated when the reference count goes to zero.
- Use the
class
keyword in place ofstruct
to make a class, similar to C++. - Fields are defined (and may be initialized as well) in the type definition using the same syntax as variable declaration and assignment, including the ability to let the compiler infer types (unlike in Rust where each field's type must be explicitly defined, and initialization happens in a method or explicit structure instantiation).
- To distinquish equality from identity, Swift defines the operator
===
(and inverse!==
) for references, where===
is likeArc::ptr_eq
, returningtrue
if the operands refer to the same value. - Swift references are automatically dereferenced when used; there is no syntax for "dereferencing" or for creating a new reference (other than assigning a reference or passing it to a function). In other words, references are not first class types in Swift?
Properties
- Properties are either "stored" (as in Rust) or "computed".
- A stored property marked with the
lazy
keyword before its declaration is not calculated until the first time it's used.
- A stored property marked with the
- A "computed" property can be defined for classes, structures, and enum types. They are declarated similarly to properties with accessors in C# classes:
- After the type name of the property is a block containing two inner blocks, one with
get
before it and the other withset
before it. These inner blocks are like functions called when the property is read or written, respectively. - The
get
block is expected to compute the value of the property and return it using areturn
statement.- A shorthand syntax is supported where the inner block of the
get
is a single expression. In this case, that expression is evaluated to compute the value of the property, without a need for an explicitreturn
statement.
- A shorthand syntax is supported where the inner block of the
- The
set
block gets an value argument which is the expression assigned to the property. There are two ways of defining this value:- By placing a name in parentheses between
set
and its code block, the value is given that name. - Otherwise, an implicit value named
newValue
is defined within the code block.
- By placing a name in parentheses between
- A read-only computed property is declared by dropping the
get
keyword, keeping the block of statements that would have come afterget
, and dropping theset
keyword and its code block entirely.
- After the type name of the property is a block containing two inner blocks, one with
- Swift supports "property observers" which are like functions called automatically whenever a property's value is set. They may be added for stored properties as well as inherited computed properties (for non-inherited computed properties, you put the observer code directly in the property's "setter").
- Property observers are added in a syntax similar to computed properties, with a block after the type and default value (if any) of the property, and inner blocks proceeded by the
willSet
anddidSet
keywords.- Both
willSet
anddidSet
and their code blocks are optional. You can have one or both. - The
willSet
block has the same syntax as theset
block (besides the keyword being different) and is called just before the property is set. - The
didSet
block has the same syntax as theset
block except for the keyword and the default name of the argument provided to it beingoldValue
rather thannewValue
. ThedidSet
block is called just after the property is set. The argument provided is the value the property had before it was set.
- Both
- Property observers are added in a syntax similar to computed properties, with a block after the type and default value (if any) of the property, and inner blocks proceeded by the
- Swift supports "property wrappers" which are special structures which represent properties and provide reusable code for the getters and setters of properties.
- They are declared like normal structures but with
@propertyWrapper
in front. - They must expose a computed property named
wrappedValue
which provides the getter and setter for the wrapped property. - Property wrappers are applied to a computed property by prefixing the computed property with the name of the property wrapper with
@
in front (in other words, the name of the property wrapper turned into an attribute). - Initializers provided with property wrappers can be used to set the initial value of wrapped properties.
- A wrapped property with no default value provided uses the
init()
initializer to initialize the property. - A wrapped property with a single value assigned to it uses the
init(wrappedValue:)
initializer to initialize the property. - Other initializers may be used by expanding the property wrapper attribute applied to the computed property to look like a function call, with labels and values provided that need to match one of the
init
initializers of the property wrapper.
- A wrapped property with no default value provided uses the
- They support "projected values" which are exposed as if the type containing the computed property had a second read-only computed property with the same name but prefixed by a dollar sign (
$
). To add a projected value, the property wrapper declares its own property namedprojectedValue
which provides the projected value.
- They are declared like normal structures but with
- Computed properties and property observers can also be applied to global and local variables, which act like stored properties of structures.
- Global variables act as if declared with the
lazy
keyword, in that they are not initialized until used for the first time.
- Global variables act as if declared with the
- Swift supports "type properties" which are properties applying to types themselves (rather than values of a type). These have the
static
keyword added before the property declaration.- Type properties are accessed through the type name itself, as if the type was a structure.
Methods
- Classes, structures, and enums can have methods.
- Instance methods are like functions declared in an
impl
block for a Rust type, except in Swift they are placed inside the type declaration itself, similar to how methods are declared in C++. - Methods have access to the properties of their instance as if the names of the properties were within the scope of the method (similar to C++).
- A special implicit property
self
is available to methods, bound to the instance itself. This is similar to thethis
value in C++ methods, and similar toself
in Rustimpl
functions, except that it's implicit (as if declared as&mut self
for classes or&self
for structures and enums). - To make
self
mutable for structures and enums, add themutating
keyword in front of thefunc
keyword in the method declaration.- Note that you cannot call a "mutating" method on a constant structure or enum.
- You can assign to
self
in a "mutating" method to replace the value with a new one. This can be useful, for example, in enums for changing the variant.
- Type methods are like instance methods except the methods are called on the type itself,
self
refers to the type, not any specific value of the type, and any unqualified method or property names used in the method refer to the other type-level methods and properties of the type, rather than of any specific value of the type.- To declare a type method, add the keyword
static
in front of the method declaration.
- To declare a type method, add the keyword
- Class methods are like type methods, except they can be overridden in subclasses (and therefore may only be declared in classes).
- To declare a class method, add the keyword
class
in front of the method declaration.
- To declare a class method, add the keyword
- Unlike Rust, in Swift you can have "overloaded" methods, or multiple methods with the same name but different types for parameters and/or return value.
Subscripts
- Swift "subscripts" are similar to the
Index
andIndexMut
traits in Rust, allowing types to define special methods to be used if instances are indexed. - The syntax of subscripts are a mix of function and computed property syntax.
- Their declarations begin like functions except there is no
func
keyword, and the name is replaced by thesubscript
keyword. - They have
get
andset
blocks in the body, similar to computed properties.- The
get
block corresponds toIndex::index
in Rust. - The
set
block corresponds toIndexMut::index_mut
in Rust, except rather than returning a reference, it takes a value to be assigned, like a computed property setter.
- The
- Their declarations begin like functions except there is no
- Subscripts can have multiple parameters, which are separated by commas both in the declaration and usage of the subscripts.
- A "type subscript" or "class subscript" is similar to a "type method" or "class method", respectively. They're declared like a normal subscript, but with the
static
orclass
keyword in front of the declaration. They enable indexing the overall type itself.
Inheritance
As inheritance is entirely foreign to Rust, this section will compare inheritance in Swift to inheritance in C++.
- Properties can be overridden to provide custom getters, setters, or observers for any inherited property. An overridden property has the
override
keyword added similarly to an overriden method.- A read-only property may be overridden to be read-write.
- A read-write property may not be overridden to be read-only.
- Properties and methods can be prevented from being overridden by adding the
final
keyword before their declarations in the superclass. - An entire class can be prevented from being subclassed by adding the
final
keyword before its declaration. - There are no "pure virtual" classes or methods in Swift. Use "protocols" instead.
- "Multiple inheritance" (a subclass having two or more superclasses) is not supported.
Initialization
- An "initializer" is similar to a C++ constructor; declared like a method, named
init
, but without thefunc
keyword, and called when the type is used like a function, in order to ensure all properties of a new instance are initialized. This is roughly analogous to thenew
function convention andDefault
trait in Rust, except that the instance is provided through the implicitself
value rather than being returned. - Default initializers are generated for each struct/class type (as long as all fields have default values and the type doesn't provide at least one explicit initializer).
- Memberwise initializers are also generated for each struct/class type (if they don't provide at least one explicit initializer). They are the same as the default initializers except they take parameters with labels matching the names of the fields of the type.
- All properties of a value must be initialized by the time the initializer returns. Properties may be initialized either in the property declaration itself, or in the code of the initializer.
- Initializers can call other initializers of the same type, to reduce duplication of code.
- Classes can have special initializers called "convenience initializers" (declared by adding the
convenience
keyword before the initializer). These are not used implicitly by subclasses, but can be used for direct initialization of values, as well as by other initializers. - Superclass initializers are called from subclass initializers through the name
super
, as in:super.init()
. - Initializers that are not "convenience" are referred to as "designated initializers".
- Swift enforces rules about how initializers can call other initializers in the inheritance hierarchy of a class:
- Designated initializers must call a designated initializer from its immediate superclass, if any.
- Convenience initializers must call another initializer from the same class.
- Convenience initializers must "ultimately" (either directly or through another convenience initializer) call a designed initializer.
- Swift enforces rules about property initialization within the inheritance hierarchy of a class:
- A designated initializer must ensure all non-inherited properties are initialized before calling a superclass designated initializer.
- A designated initializer cannot assign to an inherited property until after calling a superclass designated initializer.
- A convenience initializer must call a designated initializer before assigning to any property.
- An initializer cannot call any methods of the type, read any property values, or refer to
self
as a value until after all superclass designated initializers have returned.
- An initializer can be marked fallable by using
init?
instead ofinit
for the initializer name.- Such an initializer can use a
return nil
statement to indicate that initialization failed. - Calling such an initializer produces an optional value.
- Such an initializer can use a
- By using
init!
instead ofinit?
, an initializer produces an instance that is "implicitly unwrapped". - A superclass can require all subclasses to provide an initializer of a certain form (parameters) by declaring its own initializer with that form and adding the
required
keyword in front. - Default property values can be specified as closure or function calls, in order for those default values to be computed during initialization.
Deinitialization
- Swift supports "deinitializers" for classes (not structures or enums). They are analogous to the
Drop
trait in Rust. - The deinitializer is declared like an initializer with no parameters, but with the name
deinit
instead ofinit
.
Optional Chaining
Optional chaining in Swift refers to using the question-mark (?
) operator in an expression that calls a property or method of an optional value. It's analogous to Option::map
or Option::and_then
in Rust.
For example, in Swift::
let result: SomeType? = value.property?.change()
This is analogous to the following in Rust:
let result: Option<SomeType> = value.property().map(Property::change)
The way to think of it is the call chain consists of intermediate optional values, and stops if at any point an intermediate value is nil
(analogous to None
in Rust).
Optional chaining which ends in an assignment results in that assignment only happening if the whole left-hand side expression is not nil
.
Swift automatically "flattens" the evaluated values in optional chaining, whereas in Rust it's up to the developer to select either Option::map
or Option::and_then
depending on whether an optional value or non-optional value is returned at any given point to avoid "nesting" options. In other words, in Swift:
- If a non-optional value is evaluated in an optional chain, it will become optional.
- If an optional value is evaluated in an optional chain, it remains optional (it does not become something like
Option<Option<T>>
for example).
Error Handling
Error handling was mostly already covered in The Basics. Essentially, Swift uses an "exception throw/catch" approach for handling errors, whereas Rust uses a "return success/failure" approach instead.
Swift also supports a special code block called a "defer statement". By adding the defer
keyword in front of a nested block of code, the compiler will execute the code inside the block just before the enclosing scope is exited. This is useful for "cleanup" code that needs to be run in the event of an error being thrown causing the current scope to unwind. Defer statements are executed in the reverse of the order in which they're written.
Type Casting
Two additional operators are needed in the Swift type system when dealing with classes, due to the nature of class inheritance:
- The
is
operator is used to determine at runtime if a value is of a certain subclass type. This is similar toAny::is
in Rust.- For example:
let isApple: Bool = (fruit is Apple)
- For example:
- The
as?
andas!
operators are used to "downcast" a value to a subclass type. Theas?
operator returns an optional subclass value. Theas!
operator returns a subclass value or triggers a runtime error if the value isn't of the subclass type. These are similar toAny::downcast_ref
in Rust.
Nested Types
Swift, unlike Rust, supports "nested types", which are types declared within the scope of another type. They are merely syntactic sugar for the purpose of grouping types or limiting types used in implementations from being exposed in the public interface.
Extensions
Swift supports adding computed properties, methods, initializers, subscripts, nested types, and type protocol implementations to existing types through a special syntax. The existing type name is prefixed with the extension
keyword and followed by a block containing the new properties, methods, etc. inside. This is all syntactic sugar giving the appearance of extending a type, when in actuality the new functionality is added alongside the existing type. It's similar to "extension traits" in Rust?
Protocols
Protocols in Swift are analogous to "traits" in Rust.
- Protocols are declared using the
protocol
keyword, followed by the protocol name, followed by a block containing declarations of the requirements of any type implementing the protocol. - Property requirements are specified like computed properties without statement blocks.
- Method requirements are specified like methods but without bodies.
- As with superclasses, protocols include the
required
keyword with initializer requirements. - Types implementing protocols list the protocols after the type name. If more than one protocol is implemented, they are all listed together separated by commas.
- Swift protocols can be used as types for values. Such values are analogous to "trait objects" in Rust, allocated on the heap and always handled by reference-counted pointer, although Swift hides most of these details from the programmer.
- Generic types in Swift can conditionally conform to a protocol by listing constraints on the type, similar to trait bounds in Rust.
- Swift provides automatic ("synthesized") implementations of the
Equatable
,Hashable
, andComparable
protocols for types that adhere to certain rules specific to those protocols.-
Equatable
—PartialEq
-
Hashable
—Hash
-
Comparable
—PartialOrd
-
- Protocols can be restricted for use by classes only by inheriting from the special
AnyObject
protocol. - Protocol composition is a way to combine multiple protocol requirements for a value of some concrete type. It's specified similarly to how multiple trait bounds are specified in Rust, except that
&
is used to separate multiple kprotocols rather than the+
used to separate multiple traits in Rust trait bounds. - The
is
,as?
, andas!
operators work with procotols the same way as they do for subclasses. - For interoperability with Objective-C, Swift allows protocols marked with the
@objc
attribute to contain "optional requirements", or specifications of properies or methods which may or may not be implemented for a type.- Such requirements have the keywords
@objc
andoptional
at the front. - An "optionally required" method is essentially an "optional function". The
?
operator can be used in a call to such a method, to use "optional chaining" to test at runtime to see if the method is available on the value before calling it:someOptionalMethod?(someArgument)
- Such requirements have the keywords
- Default implementations for protocol requirements can be specified using property extensions. This makes such requirements essentially "optional" for types to implement; if a type implements it, that implementation is used, otherwise the default implementation is used.
Generics
- An extension declaration for a generic type does not repeat the declaration of the type parameters.
- Generic associated types are declared in protocols using the
associatedtype
keyword (in Rust you would just use thetype
keyword). When implementing such a protocol, the associated type is inferred, unlike in Rust where you have assign it to the associated type placeholder in the implementation of the generic.-
struct Stack<Element>: Container
—type Item = Element
-
- Associated type constraints are declared along with the associated type in the protocol, whereas in Rust you would apply the constraints in the implementation blocks or methods where those constraints are needed.
- Swift supports type constraint specifications on generic type parameters using
where
clauses similar to Rust; however, the clauses can specify that types be equal, in addition to that types implement certain protocols. The equivalent of requiring type equality in Rust is to use "associated type bindings". For example:- Swift:
func foo<I: Iterator>(it: I) where I.Item == Foo
- Rust:
fn foo<I>(it: I) where I: Iterator<Item=Foo>
- Swift:
- Extensions of generics may also add type constraint specifications of their own.
Opaque Types
The some
keyword in Swift is used similarly to the impl
keyword, particularly in the return value position, to stand for "some type that implements a protocol (trait)" where the compiler can determine the actual concrete type:
-
-> some Shape
—-> impl Shape
Automatic Reference Counting
- Weak references:
weak var tenant: Person?
—let mut tenant: Weak<Person>
- An "unowned reference" has the
unowned
keyword before the declaration of a non-optional value. Such a reference does not participate in automatic reference counting, but unlike a weak pointer it's not "optional" (cannot benil
). A "dangling" unowned reference causes a runtime error if accessed. There is no clear analogue to this in Rust;*const T
, and*mut T
come close to realizing this concept but don't match precisely? - An "unowned optional reference" is the same as an "unowned reference" except the value is an optional type. This makes it analogous to a
*const T
or*mut T
in Rust?
Memory Safety
Essentially, Swift does a limited form of "borrow checking" by detecting "memory conflicts" where there is some overlap (multi-borrow) in referencing values. The following examples are provided by the book:
- In-out parameters: passing a value for an
inout
parameter to a function which accesses the same value - "Self": passing a value for an
inout
parameter to a method of the same value - Passing two parts of the same tuple as
inout
parameters to a function
Access Control
- A "module" in Swift is a "crate" in Rust.
- Access levels are relative to module and source file.
- Swift has finer-grained access control than Rust:
-
open
— likepublic
, but allows subclassing in other modules. -
public
— essentiallypub
in Rust -
internal
— essentiallypub(crate)
in Rust -
fileprivate
— analogous topub(self)
in Rust, if in the top-level module for a source file. -
private
— analogous toprivate
in C++; not applicable to Rust.
-
Advanced Operators
-
&+
—{integer}::overflowing_add
-
&-
—{integer}::overflowing_sub
-
&*
—{integer}::overflowing_mul
-
static func +
—std::ops::Add
-
static prefix func -
—std::ops::Neg
-
static postfix func !
is for postfix operators (no equivalent in Rust?). - You can define custom operators in Swift, such as:
static prefix func +++
.- For custom infix operators, you can define what operator precedence group they should belong to like this:
infix operator +-: AdditionPreference
.
- For custom infix operators, you can define what operator precedence group they should belong to like this:
Closing Thoughts
If you made it here by reading through the entire post,
🌟Congratulations!🌟
If you jumped around and just read the sections that interested you, that's fine too. Thanks for reading, and please let me know in the comments if there are any mistakes, things I missed, and ways I can improve.
Top comments (5)
Thanks for the article.
Knowing Swift very well your article was like a hole book in 15 minutes. I have a good enough idea of Rust now.
Best,
Chip
Great job. I appreciate how hard you tried to keep the article objective, but I equally appreciate that you made a section for your opinions — and that you clearly delineated the sections. Very readable.
As someone who is unfamiliar with both Swift and Rust, I would say that the essay would be improved if you explicitly clarified before the heading "The Basics" that the left side of the em dash is Swift, and the RIGHT side is Rust.
You almost say that (but not quite), so I had to look up the syntax of both languages for your first few examples, just to make sure. 🙂
Question: in your opinion what would be the best learning path for an absolute beginner to programming to learn Rust?
All resources seem to assume that learners are coming to Rust from another language.
It's frustrating because I KNOW I want to work in Rust, and I am not at all interested in learning another language so I can throw it away and use it as a scaffold to let me switch to Rust.
At any rate, since I'm unable to find resources for absolute beginners, and since there are abundant resources for learning Javascript, Python, and Swift...
Would you recommend learning Swift first then sliding over to Rust?
Or would you recommend some other learning path?
Tom, you didn't ask me but still I'm giving my opinion here. An absolute beginner can learn Rust directly. It'd be difficult for sure, but then for an absolute beginner, any language would be difficult (not the same level of difficulty though). But learning one language first, then Rust ... is a more difficult path, as Rust changes the way one thinks about resources (memory, file, lock, etc).
When I started learning Rust almost 7 years back, I knew several languages including C++ (which I'm most comfortable with). Programmers who know C++ have one advantage over others, when they start learning Rust ... and it is because Rust attempts to solve the issues which are common in C++. It's designed from C++'s problems perspective. So a C++ programmer quickly understands why a certain Rust's feature is designed in a specific way. They know the context! At least that is what I felt when I came to Rust.
Really well-written. Now that Swift 6 has been released, the concurrency design is quite interesting to compare.
Omg! I’ve never seen a comparison article so detailed and yet so interesting to want to read every line one by one like this .
This is Fab_solutely awesome