その Swift コード、こう書き換えてみないか
本記事の内容は 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
)など)には、count
と endIndex
という、2つの似たインスタンスプロパティがあります。
count
は「Collection
に含まれる要素の数」を示し、endIndex
は「Collection
の「末尾を超えた」位置(最後の有効な添え字の引数より1つ大きい位置)」を示します。また、型に注目したとき、count
は必ず Int
ですが、endIndex
は Collection
の Associated Type である Index
になります。
let prefectures = ["Hokkaido", "Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
print(prefectures.count) // 7
print(prefectures.endIndex) // 7
上記の prefectures
ような Array
では、count
と endIndex
が同じ値となります。
let tohoku = prefectures.dropFirst()
print(tohoku) // ["Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
print(tohoku.count) // 6
print(tohoku.endIndex) // 7
しかし、例えばこちらの tohoku
のような Array
を dropFirst(_:)
して作った ArraySlice
では、count
と endIndex
とで結果が異なります。
ArraySlice の場合
ArraySlice.Index
のドキュメントにあるように、ArraySlice
のインスタンスは必ず 0 からインデックスされるというわけではありません。要素へのアクセスには、0
や count
ではなく、startIndex
と endIndex
を境界として使用します。
上記のような count
と endIndex
の違いがあるため、例えば添字によって Collection
の「最後の要素」にアクセスしたいときは、count
ではなく endIndex
を使ってアクセスします。
print(tohoku) // ["Aomori", "Iwate", "Miyagi", "Akita", "Yamagata", "Fukushima"]
print(tohoku[tohoku.count - 1]) // 😧 Yamagata
print(tohoku[tohoku.endIndex - 1]) // ✅ Fukushima
計算量に注目すると、count
は endIndex
は
定義
Collection
の定義
本章の記載内容アップデートについて
記事初出時の内容
Collection
のインスタンスプロパティである count
・endIndex
について紹介する内容でした。それぞれの型、呼び出す際の計算量、Apple のドキュメントへのリンク、GitHub の apple/swift にある Collection.endIndex
の実装へのリンクについて言及していました。
(記事初出時の内容すべてについては 本章アップデート前の内容 からご覧いただけます。)
本章で至らなかった点といただいたリアクション
計算量に注目すると、
count
はです[*]が、 O(n) endIndex
はとなります。 O(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
の方がよいと印象付ける内容でした。count
・endIndex
はそれぞれ役割が異なるため、それら同士をいかなる場合においても置換できる、というわけではありません。しかし、本章以外の章における内容は、Swift コードの別な書き方を提案するとともに、その提案したコードに置換して使用できるというものがほとんどでした。これにより、本章においても count
を用いている場所すべてで endIndex
に置換可能であると認識される文章表記・記事の構成でした(「要素数を得たいとき、endIndex
を使うこともできる」という表現が正確ではありませんでした)。
これらについて誤解を招くものとなっていましたこと、誠に申し訳ございませんでした。
普段の iOS App の開発等でよく使われるであろうモノたちは、脚注の RandomAccessCollection
に準拠していたり、それに準拠していなくとも別途計算量がドキュメントに明記されたりしており、いずれも以下の通りとなっています。
-
Array
-
Array
についてはRandomAccessCollection
に準拠しているため、Array.count
は ですO(1)
-
-
Dictionary
-
Dictionary
はRandomAccessCollection
には準拠していませんが、ドキュメントに明記されているとおり、Dictionary.count
は ですO(1)
-
-
Set
-
Set
はRandomAccessCollection
には準拠していませんが、ドキュメントに明記されているとおり、Set.count
は ですO(1)
-
-
Range
-
Range
についてはRandomAccessCollection
に準拠しているため、count
は ですO(1)
-
そのため、「要素数」が必要とされている状況では 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
計算量に注目すると、count
は endIndex
は
脚注
* ただし、RandomAccessCollection
に準拠する場合は
また、型に注目したとき、count
は必ず Int
ですが、endIndex
は Collection
の Associated Type である Index
になります。
定義
Collection
の定義
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
計算量に注目すると、count
は isEmpty
は
定義および実装
Collection
のインデックスを得るには enumerated()
でも zip(_:_:)
でもなく indexed()
を使う
Collection
の要素のインデックスがほしいとき、enumerated()
や zip(_:_:)
を使う方法が紹介されていることがありますが、多くの場合は Swift Algorithms の indexed()
の利用が適していると思っています。
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
から先頭の要素を無くした後の Collection
、tohoku
を作るとどうなるでしょうか。
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]
を得ようとすると実行時にクラッシュしてしまいます。
Array
を dropFirst(_:)
して作った 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 Algorithms の indexed()
を用います。
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 Algorithms の product(_:_:)
を使います。
import Algorithms
for (year, month) in product(years, months) {
print(year, month)
}
これにより、ネストを1段階減らすことができますし、product(_:_:)
に渡した Sequence
・Collection
の一方が空だった場合、無駄なループ処理が発生しません。
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)処理 | できる | できない |
実装
Sequence
のデフォルト実装
enum
を使う
名前空間(Namespaces)には 名前空間(Namespaces)がほしいときは、enum
を用います。
enum BreakfastMenu {
static let rice = "ご飯"
static let bread = "パン"
// ...
}
print(BreakfastMenu.rice) // ご飯
struct
や class
などでも実現できますが、デフォルトで 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
でなかったときのみ実行する処理を記述できます。
実装
Optional
の実装
var
にしたり Optional
にしたりせず、let
にして 非Optional にする
「get するときまでには必ず set する」プロパティを 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
)したとき、まだその段階ではインスタンスプロパティに何も入っていないが、それを使うタイミングでは必ず初期化済みである… ということがあります。
下記の例では、collectionView
は ViewController
の init
段階ではまだ準備していないものの、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
の絡む考慮を行わずに済みます。
UIViewController
では @ViewLoading
が利用できる
iOS 16.4 以降などの 当日の発表を聞いてくださった方によるツイートから、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
に準拠する構造体 X
・Y
があります。メソッド P.f()
はプロトコル側の定義で @MainActor
となっており、X
を P
に準拠させるための extension 内で f()
を実装すると、X.f()
は暗黙的に @MainActor
になります。
しかし、Y
の P
への準拠を別なところで記述し、そのための 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 の例で見てみます。上記の ContentView
は View
に準拠しています。 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
(イベント(スライド資料)では省略した内容)
Result
とスロー関数(Throwing Functions)の相互変換
【Swift】【Swift】Delegate パターンやクロージャパターン(completionHandler など)を Swift Concurrency で使う
【Foundation】NotificationCenter を Swift Concurrency で使う
【Combine】【RxSwift】Combine・RxSwift を Swift Concurrency で使う
-
ただし、
Array
やRange
などのRandomAccessCollection
に準拠している場合は となる(O(1) Dictionary
やSet
などはRandomAccessCollection
に準拠していないが である) ↩︎O(1) -
ただし、
Array
やRange
などのRandomAccessCollection
に準拠している場合は となる(O(1) Dictionary
やSet
などはRandomAccessCollection
に準拠していないが である) ↩︎O(1) -
Array
やContiguousArray
など ↩︎
Discussion
素晴らしい記事ありがとうございます!
一点だけ気になったのでコメントさせてください。
こちらの節の説明では、「要素数を得たいとき
endIndex
を使えば良いんだ」という誤解を招くのではないかと思いました(実際、この記事を読んだであろう方からそのような誤用をしたコードが上がってきました)。count
とendIndex
が異なる値を返す場合があることや、そもそも要素数を得るためにendIndex
を利用するのはセマンティクス的にも間違っているということをふまえ、下記のような説明が適切かと思います。endIndex
の方が適しているケースがあるindices
を使うべきかなどの論点もありますが)count
とendIndex
は異なる値を取る場合があるcount
を利用すべきコメントいただきありがとうございます。また、本記事の内容を利用いただいたことに感謝申し上げるとともに、一部の章において誤解を招きやすい文章・記事構成となってしまったことについてお詫び申し上げます。ご指摘の章については、いろいろな場所で多くのリアクションをいただいており、またその中で私が意図しない内容・理解についても含まれてしまっていました。
これらを受けて、当該章の内容をアップデートし、またアップデートに至った経緯についても合わせて記しました。jrsaruo さんのおっしゃる
Collection
のcount
とendIndex
についての説明に私も同意いたします。改めまして、コメントにてご指摘いただき誠にありがとうございました。
アップデートありがとうございます!
こちらこそいつも treastrain さんの分かりやすい記事やツイートから学ばせていただいてます。これからも楽しみにしています!