Top 7 Subtle Swift Features
1. Keyword indirect
It’s used with enums only. As you know, enums are value type and stored on the stack. Therefore, the compiler needs to know how much memory each enum takes. As only one option is possible at any moment, the enum occupies the memory of the largest case plus some operational information.
// Just a general enum, nothing fancy
enum Foo {
case bizz(String)
case fizz(Int)
}
But what if we make enum dependant on itself?
// Infinite size??
enum Foo {
case bizz(Foo)
case fizz
}
This definition generates a compiler error.
Foo
is not marked indirect
The error makes sense: the compiler can’t calculate Foo
size as it tends to infinity. Here comes the indirect
keyword.
// Oh, fine
enum Foo {
indirect case bizz(Foo)
case fizz
}
Simple: it modifies the enum memory structure to solve the recursion problem. Detailed: .bizz(Foo)
is no longer stored inline in memory. Actually, with the indirect
modifier data is now stored behind a pointer (indirectly).
Problem solved! Also, we can modify the whole enum as indirect
// Every case is indirect now
indirect enum Foo {
case bizz(Foo?)
case fizz(Foo?)
}
2. Attribute @autoclosure
Swift’s @autoclosure
attribute enables you to define an argument that automatically gets wrapped in a closure. It’s mostly used to defer the execution of an expression to when it’s actually needed.
func calculate(_ expression: @autoclosure () -> Int,
zero: Bool) -> Int {
guard !zero else {
return 0
}
return expression()
}
Then, calculate can be called like this:
calculate(1 + 2, zero: false) // 3
calculate([Int](repeating: 5, count: 10000000).reduce(0, +),
zero: false) // 50000000
calculate([Int](repeating: 5, count: 1000).reduce(0, +),
zero: true) // 0
So, in this case, when zero: true
, the call of calculate
does not calculate the expression at all, improving code performance.
3. Lazy
A lazy
stored property is a property whose initial value isn’t calculated until the first time it’s used. Lazy properties must always be declared as a variable. Note that if you use lazy
in struct
, then the function that uses it must be marked as mutating
.
class Foo {
lazy var bonk = DBConnection()
func send() {
bonk.sendMessage()
}
}
We already covered @autoclosure
which also can help to defer expression evaluation. That can be used with lazy
! Consider this common case of dependency injection.
class Foo {
let bonkProvider: () -> DBConnection
lazy var bonk: DBConnection = bonkProvider()
init(_ expression: @escaping @autoclosure () -> DBConnection) {
self.bonkProvider = expression
}
func send() {
// Here bonkProvider() is called
// only for the first call of send()
bonk.sendMessage()
}
}
4. Enums as namespaces
Swift does not have namespaces, which may be a problem in big projects. This is easily solved with enums.
enum API {}
extension API {
static let token = "…"
struct CatsCounter {
…
}
}
let a = API.CatsCounter()
print(API.token)
5. Dynamic member lookup
This section describes the @dynamicMemberLookup
attribute. It can be used with structs and classes.
Just adding @dynamicMemberLookup
to the definition generates an error
@dynamicMemberLookup
attribute requires Foo
to have a subscript(dynamicMember:)
method that accepts either ExpressibleByStringLiteral
or a key path
Therefore, such subscript needs to be defined
@dynamicMemberLookup
class Foo {
subscript(dynamicMember string: String) -> String {
return string
}
}
let a = Foo()
print(a.helloWorld)
In subscript
you can implement much more complex logic to retrieve data. But you can see how this implementation is limited to strings only and not really safe. This can be modified with a key path
.
class Bob {
let age = 22
let name = "Bob"
}
@dynamicMemberLookup
class Foo {
let himself = Bob()
subscript<T>(dynamicMember keyPath: KeyPath<Bob, T>) -> T {
return himself[keyPath: keyPath]
}
}
let a = Foo()
print(a.age)
Even though you know about this feature does not mean that it should be used everywhere. It’s up to you what is more readable and expressive: a.himself.age
or a.age
.
6. Dynamically callable
Also, a compiler feature that allows you to call objects. Can be applied to struct
, enum
, and class
.
After adding the attribute, the error is generated:
@dynamicCallable
attribute requires RangeGenerator
to have either a valid dynamicallyCall(withArguments:)
method or dynamicallyCall(withKeywordArguments:)
methodThe method signature is similar to that of @dynamicMemberLookup
.
@dynamicCallable
struct RangeGenerator {
var range: Range<Int>
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> [Int] {
if args.count > 1 || args.first?.key != "count" {
fatalError("Unknown arguments \(args)")
}
let count = args.first!.value
return (0..<count).map{ _ in Int.random(in: range) }
}
}
let gen = RangeGenerator(range: 0..<100)
print(gen(count: 13))
// [2, 89, 4, 17, 65, 26, 73, 86, 93, 13, 25, 96, 96]
7. Inlining
Sometimes you want to give additional information about optimisations the compiler can use. Inlining code is one of the most important optimization features. So, how to use @inlinable
, @inline(__always)
, @usableFromInline
?
The @inlinable
attribute exports the body of a function as part of a module's interface, making it available to the optimizer when referenced from other modules.
As a result, @inlinable
makes the implementation of the method public and able to be inlined into the caller. Secondly, it forces you to make everything it calls @usableFromInline
.
@inline(__always)
tells the compiler to ignore inlining heuristics and always (almost) inline the function.
A function that is @inline(__always)
, but not @inlinable
, will not be available for inlining outside its module, because the function's code is not available.
@inline(__always)
can be beneficial for performance, but it can also have catastrophic effects on macro performance due to code size increase.struct Foo {
@inlinable
@inline(__always)
func simpleComputation(_ a: Int, _ b: Int) -> Int {
duplicate(a) + duplicate(b)
}
@usableFromInline
func duplicate(_ c: Int) -> Int {
c * 2
}
func general() {
print("Hello world")
}
}
This has more effects on implementation, check the discussion on this forum if you want to understand this in-depth
References
- https://www.swiftbysundell.com/articles/using-autoclosure-when-designing-swift-apis/
- https://www.swiftbysundell.com/articles/powerful-ways-to-use-swift-enums/
- https://www.hackingwithswift.com/articles/134/how-to-use-dynamiccallable-in-swift
- https://www.hackingwithswift.com/example-code/language/what-are-lazy-variables
- https://forums.swift.org/t/who-benefits-from-the-indirect-keyword/20167
- https://www.tothenew.com/blog/recursive-enumerations-in-swift/
- https://www.avanderlee.com/swift/dynamic-member-lookup/