🦅

その Swift コード、こう書き換えてみないか

2023/05/22に公開
3

本記事の内容は DeNA×STORES×ラクマ iOS Meetup!!(2023年5月22日 開催)の登壇資料(原稿)です。

とある処理を Swift で実装しようとするとき、「自分ならこういうことを意識して書く〜」というものをまとめてみました。

Bool を反転するには toggle() を使う

Bool を今ある値から反転させるには、例えばこのようにしていました。

var flag = true
flag = !flag
print(flag) // false
struct Hoge {
    var fuga1 = Fuga()
    var fuga2 = Fuga()
    
    struct Fuga {
        var piyo1 = Piyo()
        var piyo2 = Piyo()
        
        struct Piyo {
            var flag = true
        }
    }
}

var hoge = Hoge()
hoge.fuga1.piyo2.flag = !hoge.fuga2.piyo1.flag
// 😫 fuga1・2、piyo2・1 を取り違えている

他のプログラミング言語でも似たような記法であり慣例となっていますが、あまり読みやすくはなく、また複雑な場合に誤る可能性もあります。

hoge.fuga1.piyo2.flag.toggle()

Swift 4.2 で追加された toggle() を用いることで、可読性が向上し、誤りが発生する心配も減ります。

Swift Evolution および実装

x % 2 == 0 ではなく x.isMultiple(of: 2) を使う

プログラミングにおいて「その数は何の倍数か?」を計算する際、割り算のあまりを求める % 演算子を用いることがありますが、そもそもなぜ % 記号を用いるのでしょうか。

// 2の倍数なら true
let x = 4
print(x % 2 == 0) // true - `%` 演算子を用いる例
print(x & 1 == 0) // true - `&` 演算子を用いる例

print(x.isMultiple(of: 2)) // true - 英語の文章のように読める (x は 2 の倍数です)
print(x.isMultiple(of: 3)) // false

Swift 5.0 で追加された isMultiple(of:) を用いることで、英語の文章のように読め、可読性が向上します。

Swift Evolution および実装

Collection の「count」と「endIndex」の違いを意識する

Collection(配列(Array)や辞書(Dictionary)など)には、countendIndex という、2つの似たインスタンスプロパティがあります。
count は「Collection に含まれる要素の数」を示し、endIndex は「Collection の「末尾を超えた」位置(最後の有効な添え字の引数より1つ大きい位置)」を示します。また、型に注目したとき、count は必ず Int ですが、endIndexCollection の Associated Type である Index になります。

let prefectures = ["Hokkaido", "Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]

print(prefectures.count)    // 7
print(prefectures.endIndex) // 7

上記の prefectures ような Array では、countendIndex が同じ値となります。

let tohoku = prefectures.dropFirst()
print(tohoku) // ["Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
print(tohoku.count)    // 6
print(tohoku.endIndex) // 7

しかし、例えばこちらの tohoku のような ArraydropFirst(_:) して作った ArraySlice では、countendIndex とで結果が異なります。

ArraySlice の場合

ArraySlice.Index のドキュメントにあるように、ArraySlice のインスタンスは必ず 0 からインデックスされるというわけではありません。要素へのアクセスには、0count ではなく、startIndexendIndex を境界として使用します。

上記のような countendIndex の違いがあるため、例えば添字によって Collection の「最後の要素」にアクセスしたいときは、count ではなく endIndex を使ってアクセスします。

print(tohoku) // ["Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
print(tohoku[tohoku.count - 1])    // 😧 Yamagata
print(tohoku[tohoku.endIndex - 1]) // ✅ Fukushima

計算量に注目すると、countO(n) です[1]が、endIndexO(1) となります。

定義
本章の記載内容アップデートについて

記事初出時の内容

Collection のインスタンスプロパティである countendIndex について紹介する内容でした。それぞれの型、呼び出す際の計算量、Apple のドキュメントへのリンク、GitHub の apple/swift にある Collection.endIndex の実装へのリンクについて言及していました。

(記事初出時の内容すべてについては 本章アップデート前の内容 からご覧いただけます。)

本章で至らなかった点といただいたリアクション

計算量に注目すると、countO(n) です[*]が、endIndexO(1) となります。

脚注Zenn の場合、ページの最下部にまとめて表示されます
* ただし、RandomAccessCollection に準拠する場合は O(1) となる

上記は記事初出時の内容の一部です。これは Apple のドキュメントにある以下の記載を取り上げたものでした。

O(1) if the collection conforms to RandomAccessCollection; otherwise, O(n), where n is the length of the collection.

count | Apple Developer Documentation https://developer.apple.com/documentation/swift/collection/count-4l4qk

以上の count よりも endIndex の方が計算量が少ないという言及から、本章は endIndex の方がよいと印象付ける内容でした。countendIndex はそれぞれ役割が異なるため、それら同士をいかなる場合においても置換できる、というわけではありません。しかし、本章以外の章における内容は、Swift コードの別な書き方を提案するとともに、その提案したコードに置換して使用できるというものがほとんどでした。これにより、本章においても count を用いている場所すべてで endIndex に置換可能であると認識される文章表記・記事の構成でした(「要素数を得たいとき、endIndex を使うこともできる」という表現が正確ではありませんでした)。

これらについて誤解を招くものとなっていましたこと、誠に申し訳ございませんでした。

普段の iOS App の開発等でよく使われるであろうモノたちは、脚注の RandomAccessCollection に準拠していたり、それに準拠していなくとも別途計算量がドキュメントに明記されたりしており、いずれも以下の通りとなっています。

そのため、「要素数」が必要とされている状況では count を用いることになります。もし、「最後の要素のインデックス+1」が必要とされている状況で count を用いている場合は、endIndex に置換できる可能性があると考えています。

本章の記載内容について(または関連する内容)の Twitter でのリアクション(一部抜粋)

本章アップデート前の内容

コレクションの「要素数」を示す count」ではなく「最後のインデックス+1」を示す endIndex を使う

コレクション(配列(Array)や辞書(Dictionary)など)の要素数を得たいとき、慣例として count を用いたりしますが、endIndex を使うこともできます。

let list = ["DeNA", "STORES", "Rakuten Rakuma"]
print(list.count)    // 3
print(list.endIndex) // 3

計算量に注目すると、countO(n) です[*]が、endIndexO(1) となります。


脚注
* ただし、RandomAccessCollection に準拠する場合は O(1) となる


また、型に注目したとき、count は必ず Int ですが、endIndexCollection の Associated Type である Index になります。

定義

Collection が空かどうかは count == 0 ではなく isEmpty で得る

Collection(配列(Array)や辞書(Dictionary)など)に要素があるかないかの条件分岐を書きたいとき、count == 0 と書くことがありますが、isEmpty に置き換えることができます。

let list1 = ["DeNA", "STORES", "Rakuten Rakuma"]
print(list1.count == 0) // false
print(list1.isEmpty)    // false

var list2 = list1
list2.removeAll()
print(list2.count == 0) // true
print(list2.isEmpty)    // true

計算量に注目すると、countO(n) です[2]が、isEmptyO(1) となります。

定義および実装

Collection のインデックスを得るには enumerated() でも zip(_:_:) でもなく indexed() を使う

Collection の要素のインデックスがほしいとき、enumerated()zip(_:_:) を使う方法が紹介されていることがありますが、多くの場合は Swift Algorithmsindexed() の利用が適していると思っています。

import Algorithms

let prefectures = ["Hokkaido", "Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
let tohoku = prefectures.dropFirst()

for (index, prefecture) in tohoku.indexed() {
    print(index, prefecture, prefectures[index])
    /*
     1 Aomori Aomori
     2 Iwate Iwate
     3 Miyagi Miyagi
     4 Akita Akita
     5 Yamagata Yamagata
     6 Fukushima Fukushima
     */
}

zip(_:_:)enumerated() と比較すると圧倒的にパフォーマンス面で負けますが、indexed() のパフォーマンスはその2つの間に入ります。

なぜ enumerated() や zip(_:_:) を使わないか

Collection(配列(Array)など)の要素のインデックスを取り出したいとき、enumerated() を用いることがありますが、これが上手くいくのは限定された条件下[3]でのみです。

let prefectures = ["Hokkaido", "Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]

for (offset, prefecture) in prefectures.enumerated() {
    print(offset, prefecture, prefectures[offset])
    /*
     0 Hokkaido Hokkaido
     1 Aomori Aomori
     2 Iwate Iwate
     3 Miyagi Miyagi
     4 Akita Akita
     5 Yamagata Yamagata
     6 Fukushima Fukushima
     */
}

enumerated() は、0 始まりの連番とシーケンスの要素のペアを返してくれます。ここdで、上記の例で prefectures から先頭の要素を無くした後の Collectiontohoku を作るとどうなるでしょうか。

let tohoku = prefectures.dropFirst()

for (offset, prefecture) in tohoku.enumerated() {
    print(offset, prefecture, prefectures[offset])
    /*
     0 Aomori Hokkaido
     1 Iwate Aomori
     2 Miyagi Iwate
     3 Akita Miyagi
     4 Yamagata Akita
     5 Fukushima Yamagata
     */
}
print(tohoku[0]) // 💥 Fatal error: Index out of bounds

for-in ループで取り出している tohoku の各要素と、インデックスによってアクセスしている prefectures の各要素がズレて出力されてしまっています。また、tohoku[0] を得ようとすると実行時にクラッシュしてしまいます。

ArraydropFirst(_:) して作った tohoku の型は、Array ではなく ArraySlice になっています。ArraySlice は元となった Array のインデックスを保持しています。そのため、tohoku のインデックスは 1 から始まり、存在しないインデックス 0 にアクセスしようとするとクラッシュしてしまいます。

Collection のインデックスを得るための方法が enumerated() のドキュメントで紹介されています。

for (index, prefecture) in zip(tohoku.indices, tohoku) {
    print(index, prefecture, prefectures[index])
    /*
     1 Aomori Aomori
     2 Iwate Iwate
     3 Miyagi Miyagi
     4 Akita Akita
     5 Yamagata Yamagata
     6 Fukushima Fukushima
     */
}

zip(_:_:) は、2つのシーケンスをペアにして1つのシーケンスにします。上記の例では Collection のインデックスたち indices と、tohoku をペアにしています。

しかし、zip(_:_:)enumerated() と比較すると圧倒的にパフォーマンスが悪いです。そこで Swift Algorithmsindexed() を用います。

import Algorithms

for (index, prefecture) in tohoku.indexed() {
    print(index, prefecture, prefectures[index])
    /*
     1 Aomori Aomori
     2 Iwate Iwate
     3 Miyagi Miyagi
     4 Akita Akita
     5 Yamagata Yamagata
     6 Fukushima Fukushima
     */
}

indexed()zip(_:_:) と同等の結果を得ながら、パフォーマンス面で非常に有利となっています。また、命名からも Collection のインデックスが得られることが容易に認識できるでしょう。

二重の for-in ループは product(_:_:) を使う

let years = [2021, 2022, 2023]
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]

for year in years {
    for month in months {
        print(year, month)
        /*
         2021 January
         2021 February
         2021 March
         ...
         2022 January
         2022 February
         2022 March
         ...
         */
    }
}

このような二重の for-in ループは、Swift Algorithmsproduct(_:_:) を使います。

import Algorithms

for (year, month) in product(years, months) {
    print(year, month)
}

これにより、ネストを1段階減らすことができますし、product(_:_:) に渡した SequenceCollection の一方が空だった場合、無駄なループ処理が発生しません。

for-in ループと forEach(_:) の使い分けを考えてみる

for-in ループと forEach(_:) は同じ挙動をします。

func printWithLowercased(_ value: String) { print(value.lowercased()) }
let list = ["DeNA", "STORES", "Rakuten Rakuma"]

for name in list {
    printWithLowercased(name)
}
/*
 dena
 stores
 rakuten rakuma
 */

list.forEach { name in
    printWithLowercased(name)
}
// 同じ出力

for-in ループと forEach(_:) でできること・できないことを意識して使い分けると、実装時の意図をコードで伝えられるかもしれません。

for-in ループ forEach(_:)
ループを途中でおしまいにしたい break を使う できない
処理の途中で次の要素の処理に移りたい continue を使う return を使う
return 文を使うと for-in ループがあるスコープから出る 次の要素の処理に移る
要素名の省略 できない できる($0
関数などを直接渡す できない できる(上記の例だと list.forEach(printWithLowercased(_:))
非同期(async/await)処理 できる できない
実装

名前空間(Namespaces)には enum を使う

名前空間(Namespaces)がほしいときは、enum を用います。

enum BreakfastMenu {
    static let rice = "ご飯"
    static let bread = "パン"
    // ...
}

print(BreakfastMenu.rice) // ご飯

structclass などでも実現できますが、デフォルトで internal で使えるイニシャライザが用意されてしまうため、意図しない生成を行うことができてしまいます。enum ではそれが発生しません。

ランダムな値・要素がほしいときは

ランダムな Bool がほしい、ランダムな数字1つがほしい、Collection(配列(Array)や辞書(Dictionary)など)の中からランダムに1つ要素がほしいという場合に使えるメソッドがあります。

let flag = Bool.random() // true または false
let num = Int.random(in: 0..<10) // 0 から 9 までのどれかの数字1つ
let area = [1: "Shibuya", 2: "Shinjuku"].randomElement() // (key: 1, value: "Shibuya") または (key: 2, value: "Shinjuku")
let player = ["uhooi", "treastrain"].randomElement() // "uhooi" または "treastrain"

Swift 4.2 未満ではこれらが用意されていなかったため、arc4random()arc4random_uniform(_:) を用いていましたが、今は使用しません。

Swift Evolution および実装

Optional なプロパティが nil でないときにある処理を行い、nil のときは nil のままにする には map(_:) を使う

struct Talk {
    let title: String
}

let selectedTalkTitle: String? = nil
let selectedTalk: Talk? = selectedTalkTitle.map { Talk(title: $0) }
print(selectedTalk) // nil

Optional なプロパティを使って処理を行いたいとき、map(_:) を使うとそのプロパティが nil でなかったときのみ実行する処理を記述できます。

実装

「get するときまでには必ず set する」プロパティを var にしたり Optional にしたりせず、let にして 非Optional にする

enum Area {
    case osaka, hiroshima
}
let area: Area = // ...

var okonomiyaki: String?
switch area {
case .osaka:
    okonomiyaki = "関西風"
case .hiroshima:
    okonomiyaki = "広島風"
}
print(okonomiyaki)

上記のような、表記上は2度以上、変数に代入を行うような例を見てみます。okonomiyaki に初期値として nil を持たせるために String? にしており、その後代入を行うので var としています。

let okonomiyaki: String
switch area {
case .osaka:
    okonomiyaki = "関西風"
case .hiroshima:
    okonomiyaki = "広島風"
}
print(okonomiyaki)

しかし、okonomiyaki が初めて取得される print のタイミングまでに、okonomiyaki への代入は確実に行われます。この場合、String? にする必要はなく、let にできます。この例だと switch 文を抜けた後、okonomiyaki に再代入することはできません。

あとから必ず初期化することになっているインスタンスプロパティを Optional にしない

あるインスタンスを作成(init)したとき、まだその段階ではインスタンスプロパティに何も入っていないが、それを使うタイミングでは必ず初期化済みである… ということがあります。

下記の例では、collectionViewViewControllerinit 段階ではまだ準備していないものの、viewDidLoad() の中で collectionView を作ってから使う…… という順番になっています。

final class ViewController: UIViewController {
    // この collectionView は viewDidLoad() の後に作ることにしている(と仮定)
    private var collectionView: UICollectionView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // collectionView を作る
        collectionView = UICollectionView(frame: .null)
        
        // ...
        
        // collectionView を使う
        view.addSubview(collectionView!)
    }
}

この「あとから必ず初期化することになっているインスタンスプロパティ」のためだけに Optional を用いると、過剰な nil チェックが文法上行えたり、オプショナルチェーン(Optional Chaining)を行うことになったりします。

final class ViewController: UIViewController {
    // この collectionView は viewDidLoad() の後に作ることにしている(と仮定)
    private lazy var collectionView: UICollectionView = { preconditionFailure("collectionView has not been set") }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // collectionView を作る
        collectionView = UICollectionView(frame: .null)
        
        // ...
        
        // collectionView を使う
        view.addSubview(collectionView)
    }
}

そこで、そのインスタンスプロパティを lazy var にして遅延させ、nil を使っていたところでは preconditionfailure(_:file:line:) を用います。

これにより、もし初期化前にプロパティにアクセスしようとすると preconditionfailure(_:file:line:) による例外を発生させることができつつ、使用する場面で不必要な nil の絡む考慮を行わずに済みます。

iOS 16.4 以降などの UIViewController では @ViewLoading が利用できる

当日の発表を聞いてくださった方によるツイートから、UIViewController.ViewLoading の存在を知りました。

iOS 16.4 以降などの UIViewController では、上記の preconditionfailure(_:file:line:) を用いる方法を取らずとも、Optional にしないことができます。

final class ViewController: UIViewController {
    // この collectionView は viewDidLoad() の後に作ることにしている(と仮定)
    @ViewLoading private var collectionView: UICollectionView
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // collectionView を作る
        collectionView = UICollectionView(frame: .null)
        
        // ...
        
        // collectionView を使う
        view.addSubview(collectionView)
    }
}

拡張(Extensions)はプロトコル(Protocols)ごとに作り、準拠のための実装はできる限りその中でのみ行う

プロトコルへの準拠に必要な実装は、できる限りそのプロトコルへの準拠のための extension を作り、その中でのみ実装を行うと、あとからコードを読みに来たときに追いやすくなります。

また、別なメリットもあります。SE-0316 に登場する、以下の例を見てみます。

// https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md より

protocol P {
  @MainActor func f()
}

struct X { }

extension X: P {
  func f() { } // implicitly @MainActor
}

struct Y: P { }

extension Y {
  func f() { } // okay, not implicitly @MainActor because it's in a separate extension
               // from the conformance to P
}

プロトコル P に準拠する構造体 XY があります。メソッド P.f() はプロトコル側の定義で @MainActor となっており、XP に準拠させるための extension 内で f() を実装すると、X.f() は暗黙的に @MainActor になります。
しかし、YP への準拠を別なところで記述し、そのための f() を別な extension で行ったとき、Y.f()P.f() に反して暗黙的に @MainActor となりません。

import SwiftUI

struct ContentView: View {
    let greeting = "Hello, happy world!"
    
    var body: some View { // プロトコル View 側の定義で @MainActor になっている
        Text(greeting)
    }
}

同じ事象を SwiftUI の例で見てみます。上記の ContentViewView に準拠しています。 View に準拠するために必要な body は、プロトコル側の定義で @MainActor となっています。

この ContentView は、以下のように extension に分けることもできるでしょう。

import SwiftUI

//                  ↓ `View` がここに書かれている
struct ContentView: View {
    let greeting = "Hello, happy world!"
}

extension ContentView {
    var body: some View { // 😫 暗黙的に @MainActor ではなくなり、プロトコル View 側の定義とは異なってしまう
        Text(greeting)
    }
}

body の実装を別な extension に移動させました。これは文法等上は問題のない書き方であり、ContentView は引き続き View に準拠していることになります。
しかし、この extension の分け方では body@MainActor ではなくなってしまいました。

import SwiftUI

struct ContentView {
    let greeting = "Hello, happy world!"
}

//                     ↓ `View` をここに書く
extension ContentView: View {
    var body: some View { // ✅ プロトコル View 側の定義より暗黙的に @MainActor になる
        Text(greeting)
    }
}

もし、body の実装を別な extension で行いたい場合、View への準拠の宣言をその extension で行うようにします。

UIKit(iOS など)をさわるときは

noppe さんによるこちらの記事では、主に UIKit を使うときに「こっちの方がいい」という書き方・使用方法が紹介されています。私もとても勉強させていただきました。

同じような処理だけどこっちの方がいいよってやつ - Qiita

(イベント(スライド資料)では省略した内容)

【Swift】Result とスロー関数(Throwing Functions)の相互変換

【Swift】Delegate パターンやクロージャパターン(completionHandler など)を Swift Concurrency で使う

【Foundation】NotificationCenter を Swift Concurrency で使う

【Combine】【RxSwift】Combine・RxSwift を Swift Concurrency で使う

脚注
  1. ただし、ArrayRange などの RandomAccessCollection に準拠している場合は O(1) となる(DictionarySet などは RandomAccessCollection に準拠していないが O(1) である) ↩︎

  2. ただし、ArrayRange などの RandomAccessCollection に準拠している場合は O(1) となる(DictionarySet などは RandomAccessCollection に準拠していないが O(1) である) ↩︎

  3. ArrayContiguousArray など ↩︎

Discussion

jrsaruojrsaruo

素晴らしい記事ありがとうございます!
一点だけ気になったのでコメントさせてください。

コレクションの「要素数」を示す count ではなく「最後のインデックス+1」を示す endIndex を使う

こちらの節の説明では、「要素数を得たいとき endIndex を使えば良いんだ」という誤解を招くのではないかと思いました(実際、この記事を読んだであろう方からそのような誤用をしたコードが上がってきました)。
countendIndex が異なる値を返す場合があることや、そもそも要素数を得るために endIndex を利用するのはセマンティクス的にも間違っているということをふまえ、下記のような説明が適切かと思います。

  • 要素数よりも endIndex の方が適しているケースがある
    • 例)特定のインデックスがコレクションの範囲内かどうか判定する処理(indices を使うべきかなどの論点もありますが)
  • ただし countendIndex は異なる値を取る場合がある
    • 真に「要素数」を得たいのであれば count を利用すべき
treastrain / Tanaka Ryogatreastrain / Tanaka Ryoga

コメントいただきありがとうございます。また、本記事の内容を利用いただいたことに感謝申し上げるとともに、一部の章において誤解を招きやすい文章・記事構成となってしまったことについてお詫び申し上げます。ご指摘の章については、いろいろな場所で多くのリアクションをいただいており、またその中で私が意図しない内容・理解についても含まれてしまっていました。

これらを受けて、当該章の内容をアップデートし、またアップデートに至った経緯についても合わせて記しました。jrsaruo さんのおっしゃる CollectioncountendIndex についての説明に私も同意いたします。

改めまして、コメントにてご指摘いただき誠にありがとうございました。

jrsaruojrsaruo

アップデートありがとうございます!
こちらこそいつも treastrain さんの分かりやすい記事やツイートから学ばせていただいてます。これからも楽しみにしています!