DEV Community

loading...
Cover image for Swift vs. Rust -- an Overview of Swift from a Rusty Perspective

Swift vs. Rust -- an Overview of Swift from a Rusty Perspective

rhymu8354
Long time programmer who enjoys reinventing the wheel
・33 min read

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:
      • Encodableserde::Serialize
      • Decodableserde::Deserialize
      • Encoderserde::Serializer
      • Decoderserde::Deserializer
    • C Interoperability — various types and functions analogous to Rust's Foreign Function Interface (FFI).
  • 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 as tokio, async-std, and smol 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.
  • 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.
  • 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

  • Intisize
  • UIntusize
  • Int8, Int16, Int32, Int64i8, i16, i32, i64
  • UInt8, UInt16, UInt32, UInt64u8, u16, u32, u64
  • Uint32.min, Uint32.maxu32::MIN, u32::MAX
  • Double, Floatf64, f32
    • Literals can be in hexidecimal: 0xFp-2 == 3.75
  • Boolbool

Optionals

  • T?Option<T>
  • nilNone
  • value == nil, value != nilvalue.is_none(), value.is_some()
  • Optional binding:
    • let possible_value: Int? = Int("123")let possible_value = "123".parse().ok()
    • if let value = possible_valueif 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 just let 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 = 10let name = 10
  • "Variable" is a mutable value:
    • var name = 10let 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 combination println! and print! where it behaves like println! by default but you can set a custom terminator argument to "" to be like print! 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)namename as f64
  • Some type casts return optionals:
    • let possible_number: Int? = Int("123")

Type aliases

  • typealias AudioSample = UInt16type 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))")
Enter fullscreen mode Exit fullscreen mode

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 { vs func 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)
}
Enter fullscreen mode Exit fullscreen mode

Note that the special name error is available in the unspecified error handling block.

Assertions and Preconditions

  • assert and precondition are like the assert! macro, except that assert is only executed for a "Debug" build.
  • preconditionFailure is essentially a panic!.

Basic Operators

  • Swift has the ternary operator a ? b : c, unlike Rust where you would instead use a conditional expression if a { b } else { c }.
  • String type implements the + operator for concatenation; in Rust the + operator (or String::push_str) requires the right-hand side to be a slice.
  • Bool type does not support comparison, unlike bool in Rust.
  • "Nil-Coalescing Operator": a ?? ba.unwrap_or_else(|| b)
    • Note that b is not evaluated unless a is nil, which is why it maps to Option::unwrap_or_else rather than Option::unwrap_or.
  • Range operators
    • Closed range: 1...51..=5
    • Half-open range: 1..<51..5
    • One-sided range: 1..., ...11.., ..1
    • There isn't a Swift equivalent of the Rust RangeFull (unbounded range ..)?

Strings and Characters

  • Swift strings are "composed of encoding-independent Unicode characters" and "built from Unicode scalar values", whereas in Rust String and str are UTF-8 encoded.
  • A Swift Character is a single extended grapheme cluster (sequence of one or more Unicode scalars), whereas a Rust char 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.
  • In Swift you can modify characters in strings by concatenating Unicode scalars together. For example, "e" + "\u{301}" == "é".
  • Swift String has an associated type String.Index representing the position of a Character in the String.
    • String methods index(before:), index(after:), and index(_:offsetBy:) are used with String properties startIndex and endIndex to construct String.Index values.
    • The indices property of String in Swift iterates over characters just like str::chars in Rust.
  • Indexing or slicing a string returns a Substring which is similar to str (holds a reference to the String it was sliced from).
    • let beginning = line[..<index]
    • Converting Substring to String is a type cast
  • You can encode String to UTF-8 or UTF-16 with the utf8 or utf16 properties which are String.UTF8View or String.UTF16View and are iterable as a sequence of UInt8 or UInt16.
  • You can get the Unicode scalar values of a String with the unicodeScalars property which is UnicodeScalarView and is iterable as a sequence of UnicodeScalar.
  • 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 and str (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>?
  • "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 the sorted() method, which returns an Array of the keys sorted in order.
  • To order the contents of a Dictionary, use the sorted() method of the keys or values properties, which return an Array 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 either append or extend 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)
  • countHashSet::len
  • substracting(_:)HashSet::difference
  • isStrictSubset(of:) and isStrictSuperset(of:) are like isSubset(of:) and isSuperset(of:) except they return false 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)
  • countHashMap::len
  • dict.updateValue("answer", forKey: 42) (or dict[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 operator y ? 1 : 2 instead)
    • Another consequence of this is that break statements cannot accept an argument.
  • Swift has an additional "repeat-while" syntax similar to the "do-while" syntax in C, but lacks the loop infinite loop and while let syntax.
  • Label names for loops (where a continue or break 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 Rust match except:
    • The case keyword is present in front of each arm, similar to C.
    • The default keyword, without case, 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 of if 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 a case in a switch in order to mimic the "fall through" behavior of a switch in C where the lack of a break causes execution to flow from the end of one case to the beginning of the next case.
  • Swift has an additional "guard" syntax intended for use in "early exit" scenarios. It looks simiar to an if statement, except if is replaced by guard, the else keyword is placed before the body, and any value bound to the condition (with guard let) is scoped to the block containing the guard. The body statements are only executed if the condition is false, and typically contain an "early exit" return.
  • Conditional compilation in Swift is done using a special conditional you can use in if and guard statements, as opposed to the cfg 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.

Functions

  • funcfn
  • 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:).
    • If unspecified, the label is the same as the name:
      • func speak(person: String, what speech: String) is the same as func 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:)
    • Labels not omitted must be used when calling the function.
      • For example: speakTo("Frank", what: "Hello!")
    • Although names must be unique, labels need not be (but usually are for readability).
  • 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 an Array 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)
Enter fullscreen mode Exit fullscreen mode
  • 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: >) sorts names using the > operator of the type of the items in names.
  • 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 as names.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 or unowned 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 value self by strong reference
      • [weak self] — capture value self by weak reference
      • [weak delegate = self.delegate] — capture value self.delegate by weak reference named delegate.
  • 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 if TypeName can be inferred.
  • By adding : CaseIterable after the name of an enum in its declaration (in other words, specifying that the enum conforms to the CaseIterable protocol), Swift will add an allCases 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 placing let followed by a name in the case pattern, or by placing a single let immediately following the case 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).
  • 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 the case of the variant. This causes Swift to add indirection to the variant, just as you would have to do manually using Box<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 and Arc 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 of struct 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 like Arc::ptr_eq, returning true 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 "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 with set 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 a return 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 explicit return statement.
    • 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.
    • A read-only computed property is declared by dropping the get keyword, keeping the block of statements that would have come after get, and dropping the set keyword and its code block entirely.
  • 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 and didSet keywords.
      • Both willSet and didSet and their code blocks are optional. You can have one or both.
      • The willSet block has the same syntax as the set block (besides the keyword being different) and is called just before the property is set.
      • The didSet block has the same syntax as the set block except for the keyword and the default name of the argument provided to it being oldValue rather than newValue. The didSet block is called just after the property is set. The argument provided is the value the property had before it was set.
  • 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.
    • 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 named projectedValue which provides the projected value.
  • 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.
  • 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 the this value in C++ methods, and similar to self in Rust impl 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 the mutating keyword in front of the func 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.
  • 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.
  • 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 and IndexMut 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 the subscript keyword.
    • They have get and set blocks in the body, similar to computed properties.
      • The get block corresponds to Index::index in Rust.
      • The set block corresponds to IndexMut::index_mut in Rust, except rather than returning a reference, it takes a value to be assigned, like a computed property setter.
  • 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 or class 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 the func 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 the new function convention and Default trait in Rust, except that the instance is provided through the implicit self 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 of init 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.
  • By using init! instead of init?, 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 of init.

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()
Enter fullscreen mode Exit fullscreen mode

This is analogous to the following in Rust:

let result: Option<SomeType> = value.property().map(Property::change)
Enter fullscreen mode Exit fullscreen mode

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 to Any::is in Rust.
    • For example: let isApple: Bool = (fruit is Apple)
  • The as? and as! operators are used to "downcast" a value to a subclass type. The as? operator returns an optional subclass value. The as! operator returns a subclass value or triggers a runtime error if the value isn't of the subclass type. These are similar to Any::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, and Comparable protocols for types that adhere to certain rules specific to those protocols.
    • EquatablePartialEq
    • HashableHash
    • ComparablePartialOrd
  • 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?, and as! 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 and optional 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)
  • 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 the type 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>: Containertype 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>
  • 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 be nil). 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 — like public, but allows subclassing in other modules.
    • public — essentially pub in Rust
    • internal — essentially pub(crate) in Rust
    • fileprivate — analogous to pub(self) in Rust, if in the top-level module for a source file.
    • private — analogous to private 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.

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.

Discussion (0)