Swift & runtimeHistorical technical note

Seven Swift pitfalls and how to avoid them

Uses code examples to investigate surprising behaviour in protocol extensions, optional-chain assignment and functional processing. It compares dispatch for value and reference types and explains how code can compile successfully while producing an unexpected result.

Original byline:bestswifter

Originally credited to bestswifter; edited and archived by Bison.

Source listed in the original

English translation of the Chinese original, translated on . The article body uses automated translation; app descriptions, historical reading notes and video subtitles receive separate editorial checks. Original publication dates, attribution, code and version notes are retained.

Editor: Bison
Contributor: bestswifter

"Swift is accomplishing something remarkable: changing how we program Apple devices by introducing modern paradigms such as functional programming and richer type checking than a purely object-oriented language like Objective-C."

Swift aims to prevent bugs through safe programming patterns, but this inevitably creates artificial traps that introduce bugs without compiler errors. Some are mentioned in the Swift book and others are not. Here are seven I encountered last year, involving protocol extensions, optional chaining and functional programming.

Protocol extensions: powerful, but use them carefully


A Swift class can inherit from another class, clarifying relationships and enabling fine-grained code sharing. Non-reference types, such as structures and enumerations, cannot inherit from classes. A value type can conform to a protocol, and protocols can inherit from protocols. Protocols contain type information rather than implementation code, but protocol extensions can contain code. This enables a code-sharing inheritance tree with value types at the leaves and protocols and their extensions at the roots and internal nodes.

Swift protocol extensions are still relatively new territory with unresolved problems. Code does not always behave as expected. Because these problems occur when value types—structures and enumerations—are combined with protocols, we first use classes and protocols to show a case without the trap. Switching back to value types then produces a surprise.

Introducing the example: classy pizza

Suppose we have three pizzas made from two different grains:

enum Grain  { case Wheat, Corn }

class  NewYorkPizza  { let crustGrain: Grain = .Wheat }
class  ChicagoPizza  { let crustGrain: Grain = .Wheat }
class CornmealPizza  { let crustGrain: Grain = .Corn  }

We can obtain the pizza’s ingredient through crustGrain.

NewYorkPizza().crustGrain     // returns Wheat
ChicagoPizza().crustGrain     // returns Wheat
CornmealPizza().crustGrain     // returns Corn


Because most pizzas use wheat, the common code can live in a superclass as the default implementation.

enum Grain { case Wheat, Corn }

class Pizza {
    var crustGrain: Grain { return .Wheat }
    // other common pizza behavior
}
class NewYorkPizza: Pizza {}
class ChicagoPizza: Pizza {}


The default implementation can be overridden for other cases, such as using corn.

class CornmealPizza: Pizza {
    override var crustGain: Grain { return .Corn }
}


Oops! This code is wrong, and fortunately the compiler detects it. Can you spot the mistake? The second crustGain omits r. Swift’s explicit override marker helps prevent this: the misspelled crustGain does not actually override any property. Here is the corrected code:

class CornmealPizza: Pizza {
    override var crustGrain: Grain { return .Corn }
} <br> 现在它可以通过编译并成功运行:<br>

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain         // returns Corn


The Pizza superclass also lets code work with pizzas without knowing their concrete types. We can declare a variable of type Pizza.

var pie: Pizza <br>

Yet the general Pizza type can still obtain concrete-type information.

pie =  NewYorkPizza();        pie.crustGrain     // returns Wheat
pie =  ChicagoPizza();      pie.crustGrain     // returns Wheat
pie = CornmealPizza();      pie.crustGrain     // returns Corn


Swift reference types work well in this demo. If concurrency and race conditions are involved, value types may help avoid them. Let’s try value-type pizzas!

This is as simple as before: change class to struct.

enum Grain { case Wheat, Corn }

struct  NewYorkPizza     { let crustGrain: Grain = .Wheat }
struct  ChicagoPizza     { let crustGrain: Grain = .Wheat }
struct CornmealPizza     { let crustGrain: Grain = .Corn  }


Execute

NewYorkPizza()    .crustGrain     // returns Wheat
ChicagoPizza()    .crustGrain     // returns Wheat
CornmealPizza()    .crustGrain     // returns Corn <br> 当我们使用引用类型的时候,我们通过一个超类Pizza来达到目的。但是对于值类型将要求一个协议和一个协议扩展来合作完成。<br>

protocol Pizza {}

extension Pizza {  var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza { }
struct  ChicagoPizza: Pizza { }
struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn } <br>

This compiles. Let’s test it:

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain         // returns Wheat  What?! <br> 对于执行结果,我们想说cornmeal pizza并不是Wheat制作的,返回结果出现错误!哎呀!我把<br>

struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn } <br>

has crustGrain misspelled as crustGain, again omitting r. For a value type, however, no override keyword helps the compiler detect the mistake. Without that help, we must write the code more carefully.

⚠️ Check carefully when overriding protocol properties in a protocol extension.

Now correct the spelling:

struct CornmealPizza: Pizza {  let crustGrain: Grain = .Corn } <br> 重新执行<br>

NewYorkPizza().crustGrain         // returns Wheat
ChicagoPizza().crustGrain         // returns Wheat
CornmealPizza().crustGrain     // returns Corn  Hooray! <br>

To discuss Pizza without worrying about New York, Chicago or cornmeal varieties, use the Pizza protocol as the variable’s type.

var pie: Pizza <br>

This variable can be used with different kinds of Pizza.

pie =  NewYorkPizza(); pie.crustGrain  // returns Wheat
pie =  ChicagoPizza(); pie.crustGrain  // returns Wheat
pie = CornmealPizza(); pie.crustGrain  // returns Wheat    Not again?! <br>

Why does the program say a cornmeal pizza contains wheat? During compilation, Swift ignores the variable’s actual runtime value and uses only compile-time information. It knows pie has the Pizza protocol type and its extension returns wheat, so the override in CornmealPizza has no effect. The compiler could warn about this potential error when static dispatch replaces dynamic dispatch, but does not. Carelessness here creates a serious pitfall.

Swift offers a solution here: declare crustGrain in the protocol as well as defining it in the protocol extension.

protocol  Pizza {  var crustGrain: Grain { get }  }
extension Pizza {  var crustGrain: Grain { return .Wheat }  }


Declaring the variable in the protocol and defining it in the extension tells the compiler to consider pie’s runtime value.

Declaring a property in a protocol has two different consequences—static or dynamic dispatch—depending on whether the property is defined in a protocol extension.

After declaring the variable in the protocol, the code works correctly:

pie =  NewYorkPizza();  pie.crustGrain     // returns Wheat
pie =  ChicagoPizza();  pie.crustGrain     // returns Wheat
pie = CornmealPizza();  pie.crustGrain     // returns Corn    Whew!


⚠️ Declare every property defined in a protocol extension in the protocol itself.

However, this attempt to avoid the trap does not always work.

Imported protocols cannot be extended completely.

Frameworks let programs import interfaces without including implementation details. Apple’s frameworks provide user-interface, system and other facilities. Swift extensions let programs add their own non-stored properties to imported classes, structures, enumerations and protocols. Properties added through protocol extensions look like original protocol members, but are not first-class citizens because an extension cannot add their declarations to the protocol.

First, implement a framework defining the Pizza protocol and concrete types.

// PizzaFramework:

public protocol Pizza { }

public struct  NewYorkPizza: Pizza  { public init() {} }
public struct  ChicagoPizza: Pizza  { public init() {} }
public struct CornmealPizza: Pizza  { public init() {} }


Import the framework and extend Pizza.

import PizzaFramework

public enum Grain { case Wheat, Corn }

extension Pizza         { var crustGrain: Grain { return .Wheat    } }
extension CornmealPizza { var crustGrain: Grain { return .Corn    } } <br>

As before, static dispatch produces the wrong answer.

var pie: Pizza = CornmealPizza()
pie.crustGrain                            // returns Wheat   Wrong! <br>

As explained above, crustGrain is defined only in the extension, not declared in the protocol. We cannot modify the framework’s code to fix this. Thus adding properties to another framework’s protocol through extensions is unsafe.

⚠️ Do not extend imported protocols with new properties that may require dynamic dispatch.

As just described, interactions between frameworks and protocol extensions limit extensions’ usefulness. Frameworks are not the only limitation; type constraints also affect them.

Attributes in restricted protocol extensions: declaration is no longer enough

Recall the earlier Pizza example:

enum Grain { case Wheat, Corn }

protocol  Pizza { var crustGrain: Grain { get }  }
extension Pizza { var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza  { }
struct  ChicagoPizza: Pizza  { }
struct CornmealPizza: Pizza  { let crustGrain: Grain = .Corn } <br> 让我们用Pizza做一顿饭。不幸的是,并不是每顿饭都会吃pizza,所以我们使用一个通用的Meal结构体来适应各种情况。我们只需要传入一个参数就可以确定进餐的具体类型。<br>

struct Meal: MealProtocol {
    let mainDish: MainDishOfMeal
} <br>

The Meal structure conforms to MealProtocol, which tests whether a meal contains gluten.

protocol MealProtocol {
    typealias MainDish_OfMealProtocol
    var mainDish: MainDish_OfMealProtocol {get}
    var isGlutenFree: Bool {get}
}


To avoid a reaction, the code defaults to gluten-free.

extension MealProtocol {
    var isGlutenFree: Bool  { return false }
} <br> Swift中的 Where提供了一种方式去表达约束性协议扩展。当主菜是pizza的时候,我们知道pizza有scrustGrain属性,我们就可以访问这个属性。如果没where这里的限制,我们在不是Pizza的情况下访问scrustGrain是不安全的。<br>

extension MealProtocol  where  MainDish_OfMealProtocol: Pizza {
    var isGlutenFree: Bool  { return mainDish.crustGrain == .Corn }
} <br> 一个带有Where的扩展叫做约束性扩展。<br>

Let’s make a delicious cornmeal Pizza.

let meal: Meal = Meal(mainDish: CornmealPizza())


Result:

meal.isGlutenFree    // returns false
// 根据协议拓展,理论上应该返回true <br>

As shown earlier, dynamic dispatch requires a declaration in the protocol and a definition in its extension. But definitions in constrained extensions are always statically dispatched. To prevent bugs caused by unintended static dispatch:

⚠️ Avoid constrained protocol extensions when a new property needs dynamic dispatch.

Optional-chained assignment and side effects

Swift helps avoid errors by checking whether a variable is nil and provides convenient optional-chaining shorthand to skip nil references. This is also Objective-C’s default behaviour.

Unfortunately, an optional-chained assignment can go wrong when its reference is nil. Consider this example, where Holder stores an integer:

class Holder  {
    var x = 0
}

var n = 1
var h: Holder? = nil
h?.x = n++ <br>

In the final line, we assign n++ to a property of h. Besides producing a value, n increments; we call that a side effect.

The final value of n depends on whether h is nil. If h is non-nil, assignment occurs and n++ executes. If h is nil, neither assignment nor n++ occurs. To avoid surprises caused by a missing side effect:

⚠️ Avoid assigning expressions with side effects through optional chaining.

Functional-programming pitfalls

Swift brings functional-programming advantages to Apple’s ecosystem. Functions and closures are first-class citizens, both convenient and powerful. Unfortunately, there are pitfalls to avoid.

For example, an inout parameter may silently stop working in a closure.

Swift inout parameters allow a function to accept and assign directly to an argument. Closures can refer to captured values during execution. Both features help write elegant, readable code, so combining them may seem natural, but that combination can cause problems.

Override crustGrain to demonstrate inout parameters. For simplicity, begin without closures:

enum Grain {
    case Wheat, Corn
}

struct CornmealPizza {
    func setCrustGrain(inout grain: Grain)  {
        grain = .Corn
    }
} <br>

To test the function, pass a variable whose value should change from Wheat to Corn when the function returns:

let pizza = CornmealPizza()
var grain: Grain = .Wheat
pizza.setCrustGrain(&grain)
grain        // returns Corn <br> 现在我们尝试在函数中返回闭包,然后在闭包中设置参数的值:<br>

struct CornmealPizza {
    func getCrustGrainSetter() -> (inout grain: Grain) -> Void {
        return { (inout grain: Grain) in
            grain = .Corn
        }
    }
} <br> 使用这个闭包只需要多一次调用:<br>

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter()
grain            // returns Wheat (We have not run the closure yet)
aClosure(grain: &grain)
grain            // returns Corn <br> 到目前为止一切正常,但如果我们直接把参数传进getCrustGrainSetter函数而不是闭包呢?<br>

struct CornmealPizza {
    func getCrustGrainSetter(inout grain: Grain)  ->  () -> Void {
        return { grain = .Corn }
    }
} 然后再试一次:<br>

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetter(&grain)
print(grain)                // returns Wheat (We have not run the closure yet)
aClosure()
print(grain)                // returns Wheat  What?!? <br>

inout parameters stop working outside the scope passed into the closure, so:

⚠️ Avoid inout parameters in closures.

The Swift documentation mentions this issue, but a related issue also deserves attention: currying, an equivalent way of constructing closures.

inout parameters behave inconsistently when currying is involved.

Swift offers concise syntax for functions that create and return closures, shortening both their type and body. Although currying looks like shorthand, combining it with inout parameters can be surprising. To demonstrate, rewrite the example using currying: rather than declaring a closure return type, add a second parameter list after the first and omit explicit closure creation from the body:

struct CornmealPizza {
    func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {
        grain = .Corn
    }
} 和显式创建闭包时一样,我们调用这个函数然后返回一个闭包:<br>

var grain: Grain = .Wheat
let pizza = CornmealPizza()
let aClosure = pizza.getCrustGrainSetterWithCurry(&grain) <br>

The preceding explicitly created closure failed to assign through the inout parameter, but this version succeeds:

aClosure()
grain                // returns Corn <br>

This shows that inout works in the curried function but not with explicitly created closures.

⚠️ Avoid inout parameters in curried functions; later rewriting them as explicit closures would introduce an error.

Summary: seven things to avoid

  • Check carefully when overriding protocol properties in a protocol extension.

  • Declare every property defined in a protocol extension in the protocol itself.

  • Do not extend imported third-party protocols with properties that may require dynamic dispatch.

  • Avoid constrained protocol extensions when a new property needs dynamic dispatch.

  • Avoid assigning an expression with side effects through optional chaining.

  • Avoid inout parameters in closures.

  • Avoid inout parameters in curried functions; rewriting them later as explicit closures would introduce an error.


Recommended reading: Integrating LianLian Pay into an iOS app