Article

Preventing Timing Problems When Using Closures クロージャを使う場合のタイミング問題を防ぐ

Understand how different API calls to your closures can affect your app. あなたのクロージャに対する異なるAPI呼び出しがどのようにあなたのアプリに影響を与えられるかを理解します。

Overview 概要

Many of the APIs you use in Swift take a closure—or a function passed as an instance—as a parameter. Because closures can contain code that interacts with multiple parts of an app, it's important to understand the different ways closures can be called by the APIs you pass them to. Closures you pass to APIs can be called synchronously (immediately) or asynchronously (sometime later). They may be called once, many times, or never. あなたがSwiftで使うAPIの多くは、クロージャ — またはインスタンスとして渡される関数 — をパラメータとして取ります。クロージャはアプリの複数の部分と相互作用するコードを含むことが可能なため、クロージャが、あなたがそれを渡す相手であるAPIによって呼び出される様々に異なる方法を理解するのは重要です。あなたがAPIに渡すクロージャは、同期的に(直ちに)または非同期的に(いつか後で)呼び出されることができます。それらが呼び出されるのは、一度だけ、何度も、または決してないかもしれません。

Understand the Results of Synchronous and Asynchronous Calls 同期および非同期呼び出しの結果を理解する

When you pass a closure to an API, consider when that closure will be called relative to the other code in your app. In synchronous APIs, the result of calling the closure will be available immediately after you pass the closure. In asynchronous APIs, the result won't be available until sometime later; this difference affects how you write code both in your closure as well as the code following your closure. あなたがクロージャをAPIに渡す場合、クロージャが呼び出されるのはあなたのアプリの他のコードと比較していつかを考えてください。同期APIでは、クロージャ呼び出しの結果は、あなたがクロージャを渡した直後に利用可能になります。非同期APIでは、結果はいつか後まで利用可能でないでしょう;この違いは、あなたがコードを書く方法に、あなたのクロージャにおいてそれだけでなくあなたのクロージャの後のコードの両方で影響します。

The example below defines two functions, now(_:) and later(_:). You can call both functions the same way: with a trailing closure and no other arguments. Both now(_:) and later(_:) accept a closure and call it, but later(_:) waits a couple seconds before calling its closure. 下の例は、2つの関数、now(_:)later(_:)を定義します。あなたは、両方の関数を同じ方法で呼び出せます:後付クロージャとともにそして他の引数なしで。now(_:)later(_:)の両方ともクロージャを受け取りそれを呼び出します、しかしlater(_:)はそれのクロージャを呼び出す前に2秒ほど待ちます。


import Dispatch
let queue = DispatchQueue(label: "com.example.queue")


func now(_ closure: () -> Void) {
    closure()
}


func later(_ closure: @escaping () -> Void) {
    queue.asyncAfter(deadline: .now() + 2) {
        closure()
    }
}

The now(_:) and later(_:) functions represent the two most common categories of APIs you'll encounter in methods from app frameworks that take closures: synchronous APIs like now(_:), and asynchronous APIs like later(_:). now(_:)later(_:)関数は、クロージャを取るアプリフレームワーク由来のメソッドにおいてあなたが出くわす、2つの最も一般的なAPIのカテゴリを表します:now(_:)のような同期的API、そしてlater(_:)のような非同期的API。

Because calling a closure can change the local and global state of your app, the code you write on the lines after passing a closure needs to be written with a careful consideration of when that closure is called. Even something as simple as printing a sequence of letters can be affected by the timing of a closure call: クロージャ呼び出しはあなたのアプリのローカルおよびグローバルな状態を変える可能性があることから、クロージャを渡す後の行であなたが書くコードは、いつそのクロージャが呼び出されるか注意深く考えて書かれる必要があります。一連の文字を印字するような単純なことでさえ、クロージャ呼び出しの時機によって影響される可能性があります:


later {
    print("A") // Eventually prints "A"
}
print("B") // Immediately prints "B"


now {
    print("C") // Immediately prints "C"
}
print("D") // Immedately prints "D"


// Prevent the program from exiting immediately if you're running this code in Terminal.
let semaphore = DispatchSemaphore(value: 0).wait(timeout: .now() + 10)

Running the code in the example above usually prints the letters in the order BCDA. Even though the line that prints A is first in the code, it's ordered later in the output. The ordering difference happens due to the way the now(_:) and later(_:) functions are defined. You need to know how each function calls its closure if you write code that relies on a specific execution order. 上の例のコードを実行することは、通常は文字をこの順番で出力します、BCDA。たとえAを印字する行がコードにおいて最初であっても、それは出力においてもっと後の順番にされます。順番の違いは、now(_:)later(_:)関数が定義される方法のために起こります。あなたは、各関数がそれのクロージャを呼び出す方法を知る必要があります、もしあなたがある明確な実行順序を当てにするコードを書くならば。

You'll need to consider this kind of time-based execution problem frequently when using APIs that take closures. In many cases, only one sequence of calls is correct for your app, so it's important to think through what the state of your app will be, given the APIs you're using. Use API names and parameter names along with documentation to determine whether an API is synchronous or asynchronous. あなたは、クロージャを取るAPIを使う場合に、この種の時間基盤の実行問題をしばしば考える必要があるでしょう。多くの場合に、ただ1つの呼び出し順番だけがあなたのアプリにとって正しいです、それで重要なのは、あなたが使うAPIを与えられて、あなたのアプリの状態がどうなるのか考え抜くことです。API名とパラメータ名を、加えてAPIが同期的または非同期的かを明らかにする文書とともに使ってください。

A common timing mistake is expecting the results of an asynchronous call to be available within the calling synchronous code. For example, the later(_:) method above is comparable to the URLSession class's dataTask(with:completionHandler:) method, which is also asynchronous. A timing scenario you should avoid is calling the dataTask(with:completionHandler:) method within your app's viewDidLoad() method and attempting to use the results outside of the closure you pass as the completion handler. よくあるタイミング間違いは、非同期呼び出しの結果が、呼び出している同期的コード内部で利用可能であると予想することです。例えば、上のlater(_:)メソッドは、URLSessionクラスのdataTask(with:completionHandler:)メソッドと似ています、それもまた非同期です。あなたが避けるべきタイミングシナリオは、dataTask(with:completionHandler:)メソッドをあなたのアプリのviewDidLoad()メソッド内部で呼び出す、そして結果をあなたが完了ハンドラとして渡したクロージャの外部で使うのを試みることです。

Don't Write Code That Makes a One-Time Change in a Closure That's Called Multiple Times 一回限りの変更をするコードを複数回呼び出されるクロージャの中に書かないでください

If you're going to pass a closure to an API that might call it multiple times, omit code that's intended to make a one-time change to external state. あなたがあるクロージャをそれを複数回呼び出すかもしれないAPIに渡そうとしているならば、一度限りの変更を外部状態に対してするように意図されるコードを省いてください。

The example below creates a FileHandle and an array of data lines to write to the file that the handle refers to: 下の例は、FileHandleと、データ行からなる配列を作成して、そのハンドルが参照するファイルに書き出します:


import Foundation


let file = FileHandle(forWritingAtPath: "/dev/null")!
let lines = ["x,y", "1,1", "2,4", "3,9", "4,16"]

To write each line to the file, pass a closure to the forEach(_:) method: 各行をファイルに書き出すには、クロージャをforEach(_:)メソッドに渡してください:


lines.forEach { line in
    file.write("\(line)\n".data(using: .utf8)!)
}

When you're finished using a FileHandle, close it using closeFile(). The correct placement of the call to closeFile() is outside of the closure: あなたがFileHandleの使用を完了した場合、それをcloseFile()を使って閉じてください。closeFile()への呼び出しの正しい配置は、クロージャの外側です:


lines.forEach { line in
    file.write("\(line)\n".data(using: .utf8)!)
}


file.closeFile()

If you misunderstand the requirements of closeFile(), you might place the call inside the closure. Doing so crashes your app: あなたがcloseFile()の必要条件を取り違えるならば、あなたは呼び出しをクロージャの内側に置くかもしれません。そうすることはあなたのアプリをクラッシュさせます:


lines.forEach { line in
    file.write("\(line)\n".data(using: .utf8)!)
    file.closeFile() // Error
}

Don't Put Critical Code in a Closure That Might Not Be Called 呼び出されないかもしれないクロージャの中に重大なコードを置かないでください

If there's a chance that a closure you pass to an API won't be called, don't put code that's critical to continuing your app in the closure. あなたがAPIに渡すクロージャが呼び出されない可能性があるならば、あなたのアプリを継続するのに重要なコードをクロージャの中に置かないでください。

The example below defines a Lottery enumeration that randomly picks a winning number and calls a completion handler if the right number is guessed: 下の例は、Lottery列挙を定義します、それはランダムに当選番号を選んで、正しい数が推測されるならば完了ハンドラを呼び出します。


enum Lottery {
    static var lotteryWinHandler: (() -> Void)?
    
    @discardableResult static func pickWinner(guess: Int) {
        print("Running the lottery.")
        if guess == Int.random(in: 0 ..< 100_000_000), let winHandler = lotteryWinHandler {
            winHandler()
            return true
        }
        
        return false
    }
}

Writing code that depends on the completion handler being called is dangerous. There's no guarantee that the random guess will be correct, so important actions like paying bills—scheduled for after you win the lottery—might never happen. 呼び出される完了ハンドラに頼るコードを書くことは危険です。ランダムな推測が正しくなる保証はありません、それで請求書を払うような — あなたが宝くじに当たった後に予定される — 重要な行為は決して起こらないかもしれません。


func payBills() {
    amountOwed = 0
}


var amountOwed = 25
let myLuckyNumber = 42


Lottery.lotteryWinHandler = {
    print("Congratulations!")
    payBills()
}


// You get 10 chances at winning.
for _ in 1..10 {
    Lottery.pickWinner(guess: myLuckyNumber)
}


if amountOwed > 0 {
    fatalError("You need to pay your bills before proceeding.")
}


// Code placed below this line runs only if the lottery was won.

See Also 参照

Data Flow and Control Flow データの流れと制御の流れ