Swift for Rust Developers: The Surprising Parts
Swift shares several syntactic similarities with Rust, making it easy to assume that familiar constructs behave the same way, even when their semantics differ.
This article documents the differences that caught me off guard while learning Swift and developing a custom app launcher for macOS.
Rather than providing an exhaustive comparison of the two languages, it focuses on the differences worth knowing to make the transition from Rust to Swift smoother.
Computed properties and property observers
In Swift, property access can execute arbitrary logic.
var area : Double {
get { width * height }
set {
// The setter's implicit `newValue` is the value assigned to `area`.
width = newValue / height
}
} Reading area executes the getter, while assigning area = 100 executes the
setter.
Even a var declaration can be read-only if it defines only a getter:
var id : String {
return "hello"
} Additionally, you can attach observers to monitor changes:
var count = 0 {
// Swift provides `newValue` and `oldValue`.
willSet { print ( "About to become \( newValue ) " ) }
didSet { print ( "Was \( oldValue ) " ) }
}
// Prints one line from willSet and one from didSet.
count = 5 Opaque types (some) and Existential types (any)
The some keyword in Swift has nothing to do with optionals. Instead, it is
roughly equivalent to the impl keyword in Rust.
Similarly, any is roughly equivalent to dyn.
Package manifest directives in comments
The Swift Package Manager requires a comment like // swift-tools-version: 6.0
at the very top of Package.swift. Removing it breaks compilation entirely
because the parser relies on it to determine the toolchain version.
Documentation generation (DocC)
To get something roughly equivalent to cargo doc, Swift uses DocC.
First, add the DocC plugin to your Package.swift dependencies:
. package (
url : "https://github.com/swiftlang/swift-docc-plugin" ,
from : "1.4.0"
), This does not add any runtime dependencies to your library.
Then, you can preview your documentation locally by running:
swift package --disable-sandbox \
preview-documentation --product < NAME_OF_LIBRARY> Modules and visibility
In Swift, a "module" is roughly equivalent to a crate, rather than an individual
source file as it typically is in Rust. All files within the same Swift module
can access each other's internal declarations without mod or use statements.
The public keyword in Swift exposes declarations to other Swift modules.
Top-level statements
Swift allows top-level executable statements in an entry-point file, commonly,
but not always named main.swift.
Semicolons
Semicolons are optional in Swift and are only required when writing multiple statements on the same line.
Idiomatic Swift omits semicolons unless needed.
Trailing Closure Syntax
Imagine a function that takes a closure:
func run ( _ action : () -> Void ) { /* ... */ } A closure is an expression that can look like a block:
{ print ( "Hello" ) } // This is a closure. Swift allows placing closure arguments outside the parentheses of a function call!
Thus, the following three are equivalent:
run ({ print ( "Hello" ) }) // Inline
run { print ( "Hello" ) } // Trailing without parentheses
run () { print ( "Hello" ) } // Trailing with parentheses Swift also supports multiple trailing closures using argument labels:
func perform ( task : () -> Void , onSuccess : () -> Void ) { /* ... */ }
perform {
print ( "Task" )
} onSuccess : {
print ( "Success" )
} Now imagine a function that takes an ordinary argument followed by a closure:
func repeatAction ( times : Int , action : () -> Void ) { /* ... */ } You will commonly see it expressed like this:
repeatAction ( times : 3 ) {
print ( "Hello" )
} Result builders
Swift has a compiler-supported feature called @resultBuilder, which allows
multiple expressions inside a closure to be combined into a single value.
For example, in SwiftUI, you can write:
VStack ( spacing : 24 ) {
Text ( "Hello" )
Image ( systemName : "star" )
Button ( "Click me" ) {}
} At first glance, this looks like a closure containing multiple expressions, without an array, commas, or even an explicit return statement.
This works because VStack takes a closure annotated with @ViewBuilder, which
is itself implemented using @resultBuilder.
@resultBuilder is built into the Swift compiler, but the builders themselves
are ordinary Swift types. SwiftUI's @ViewBuilder is one such builder.
if let and guard let
Swift's if let is conceptually similar to Rust's if let, and guard let
corresponds to Rust's let else statements.
Unlike Rust—which uses pattern matching on enums like Some(T), Swift has
built-in optionals.
Here is if let for conditional unwrapping within a block:
func greet ( _ name : String ? ) {
if let name {
print ( name ) // `name` is unwrapped here
}
// `name` is again an optional here
} And here is guard let, which exits early and leaves the unwrapped variable in
scope for the rest of the function:
func greet ( _ name : String ? ) {
guard let name else {
return
}
// `name` is unwrapped for the rest of the function scope
} For more complex enum unpacking with associated values closer to Rust's full
pattern matching, Swift provides if case let and guard case let:
enum Result {
case success ( String )
case failure ( Int )
}
func handle ( _ result : Result ) {
if case let . success ( message ) = result {
print ( message )
}
} Mutability with let
In Rust, let bindings are immutable by default, requiring let mut for
mutation. In Swift, let also declares an immutable binding, while var
declares a mutable one.
For structs (value types), let behaves as expected:
struct Configuration {
var activates = false
}
let configuration = Configuration ()
configuration . activates = true // Error! However, classes in Swift are reference types, and let only prevents
reassigning the reference, not mutating the object it refers to:
class Configuration {
var activates = false
}
let configuration = Configuration ()
configuration . activates = true // OK!
configuration = Configuration () // Error! Swift's let does not guarantee that an object is immutable; it only guarantees
that the binding cannot be reassigned.