John さんの Swiftならこう書くシリーズ 10選 を拝見して、こう書いたらもっと楽しくなりそうと思ったところがあったので、補足もしつつ紹介してみようと思います。
この記事は上記のブログみたいに「Objective-C と対比して分かりやすく」みたいには書いていないので少し難しい感も漂いますけど、ブログを読んだ後の次のステップとして読んでもらえたら幸いです。
10. 配列の操作ならSwiftの SequenceType メソッドを使用する
Swift の配列は SequenceType
という仕組みに則って作られています。それと同時により配列らしい性質を提供する CollectionType
という仕組みにも則っています。せっかくの配列なら CollectionType
も合わせて注目すると、もっと楽しくなりそうです。例題に出てくる indices
も CollectionType
が担う機能のひとつです。
すべての index をループする
すべてのインデックスを取得したい時は、ブログに書かれていた通り、次のようにすることができます。とてもきれいな書き方ですよね。
for index in array.indices {
}
enumerate を使う方法
そしてこのとき、多くの場合、インデックスから配列の要素を取り出して使うと思います。すべての要素をインデックスを加味して処理したい場合は、次のように enumerate
メソッドを使う方法があります。この例では、インデックスと要素のセットをタプル型で item
変数に取得し、ループないでそれを使っています。
for item in array.enumerate() {
let index = item.index
let element = item.element
}
このとき注意したいのが enumerate
メソッドの場合、取得できるインデックス番号は 0 から始まる通し番号 になるところです。
通常の Array
であれば開始インデックスは 0 になるのが普通なので問題ないですが、たとえば array[3..<5]
みたいに ArraySlice
を切り出したりすると開始インデックスが 0 以外になるので、それに対して indices
で回したときと、今回みたいに enumerate
で回したときとで、得られるインデックス番号に違いが出てくるところは注意する必要があるかもしれません。
enumerate を使う方法(改)
そして enumerate
で取得できるのは『インデックスと要素のタプル』なので @usatie さんが教えてくださったみたいに for
ループ内の処理に移る前にタプルを展開するともっとスマートに書けます。
for (index, element) in array.enumerate() {
}
Array を直接使う方法
ループ内でインデックス番号を考慮した処理をしないのであれば enumerate
を使わずに、シンプルに配列を in
の後に指定してあげれば、要素を順番に取得して処理をすることができます。
for element in array {
}
個人的な印象では、配列の順次操作は案外これで済んでしまう場合が多いので、まずは『インデックス番号を取得しなくても目的を達成できそうか』を検討してみると良いかもしれません。
最初・最後の index を取得するには
配列の最初と最後のインデックスを取得するのは、ブログにあった方法だと Int?
なオプショナル型で取得できてしまうので、それを扱おうとした時に面倒ごとが少し増えそうに思います。
let firstIndex: Int? = array.indices.first
let lastIndex: Int? = array.indices.last
配列の場合 CollectionType
が(厳密には Indexable
が)持っている startIndex
と endIndex
を使うと、普通の Int
型で目的のインデックスを取得できるのでオススメです。
コードも何をしているのかも最初の例より明確になるので嬉しいです。ただし注意点として、最後の要素を取得できる有効なインデックス番号として lastIndex
を取得したい場合、配列の endIndex
は最後の次のインデックスを取得する性質があるので predecessor
メソッドを使ってひとつ手前のインデックスを取得する必要があります。
let firstIndex = array.startIndex
let lastIndex = array.endIndex.predecessor()
endIndex の扱いに注意
ちなみに Swift ではインデックスを startIndex ..< endIndex
のように 左側を含んで、右側は含まない みたいに扱うのが一般的なので、最後のインデックスとした時に 実際に有効な値が取れるインデックスの次 が取れることが一般的に思うので、その辺りも注意しておきたいところです。
自分も最初、ここを勘違いしたままこの記事を書いていて @bricklife さんに間違いを教えてもらって気がつきました。うっかりすると間違ってしまって、しかも気づきにくいところに思うので、怖いところかもしれません。
last メソッドで最後の値を取得する方法
インデックスに着目しなくても、最後の要素を取得したいという場面なら、配列にも last
メソッドがあるので、それを使って値をとることもできます。
if let element = array.last {
}
ただしこのとき、空の配列みたいに最後の要素が取得できない(そもそも要素がない)場合もあるので、結果は Optional 型で返ってきます。そのため、値が取れたかを判断して使う必要が出てきます。これについては array.indices.last
で、配列のインデックスが揃えられた Range
型から、最後の値を取り出すときと同じです。
最後の n の index 以外を取得するには
最後の要素以外を取得する方法はいくつかありそうですね。ブログに紹介されていた方法はインデックスに着目した方法です。
for index in array.indices.dropLast(n) {
}
インデックスの計算なら処理的に軽そうな気もして、もしかして大きい配列の時に処理効率が良かったりするのかもしれません。
enumerate と dropLast を合わせた書き方
そして、自分が上で紹介してみた array のループ方法も加味してみると、たとえばインデックスと要素が欲しい時には、次のようなループの書き方もできたりします。
for item in array.enumerate().dropLast(n) {
}
もちろん enumerate
を使うので、先ほど挙げたように開始インデックスが 0 以外のものに対して使うと、思っているのと違うインデックス番号が取得されるかもしれないところは注意です。
Array に直接 dropLast を使う書き方
配列の要素さえあれば目的を達成できるなら、次のように書けたりもします。コード的にはこれがいちばんわかりやすいかなと思います。内部的には dropLast
メソッドによって、最後から n 個の要素を捨てた配列を作って、それに対してループを行っています。
for item in array.dropLast(n) {
}
特にこれで支障がなければ、この書き方がいちばん直感的でわかりやすいかなと感じました。
偶数番目の要素を処理するには [NEW]
せっかくなので『配列の先頭から偶数番目の要素を取得して処理する』みたいなことをする方法も紹介します。Objective-C で偶数ごとにの場合は、次のように書けていました。
for (NSInteger i = 0; i < array.length; i+=2) {
}
ストライドで刻み幅を指定する方法
これを Swift で書く場合、まず、インデックスに着目するなら次のような書き方ができます。
for index in array.startIndex.stride(to: array.endIndex, by: 2) {
}
この方法は、配列のインデックス RandomAccessIndexType
が Strideable
に対応していることを利用して、任意の刻みで取得できる SequenceType
を取得して、それを for ... in
ループで順番に処理する流れになります。
こうしてみると、なんかすごく長くなってて読みにくいかもとか感じるんですけど、もしかしてもっとスマートな書き方があったりするでしょうか。そんなことを思いながら、とりあえず紹介してみました。でもこの書き方、コードの意味という観点でみれば、Swift に慣れてくると最初の例より汲み取りやすく感じられてくるから興味深いです。
ループ条件で限定する方法
他の書き方としては、ループ内で要素も使うなら、先ほど紹介した enumerate
メソッドと、for ... in
ループで条件指定ができる where
を使って、次のように書く方法もあります。
インデックスが index % 2 == 0
である場合、つまり 2 で割った余りが 0、つまり偶数の場合に限って繰り返し処理を行っています。
for item in array.enumerate() where item.index % 2 == 0 {
let value = item.element
}
こちらも enumerate
を使ってますけど、今回の場合は enumerate
が 0 から始まる通し番号でインデックスを返す性質が功を奏して、どんなインデックス番号で始まる配列であっても同じ計算式で偶数番目か奇数番目かを判定できるようになります。
ただこの方法、個人的には繰り返し条件に指定している式が具体的すぎて読みにくい感じもします。これよりは先ほどの stride(to:,by:)
を使った方法の方が動き的には捉えやすいような気もしますけど、今回の方がコード自体はスマートで嬉しいのかなみたいなことも思ったりします。
ループ条件で限定する方法(改)
条件式の部分が明瞭でないと感じる時には、次のように、関数をローカルにひとつ自分で作って使ってみても良いかもしれません。Swift は関数の中に関数を定義できるので、最初に『どんな場合をどう表現するか』を関数で書いて、それを用いてコードを読みやすくする、ということもできたりしそうです。
// 偶数番目であるというのは、こういうこと。
func isIndexEven(index: Int) -> Bool {
return index % 2 == 0
}
// 配列の要素を繰り返す。ただし偶数番目の要素に限って処理する。
for item in array.enumerate() where isIndexEven(item.index) {
let element = item.element
}
9. シングルトンの代わりに staticの関数やプロパティを使用する enum で作成する
静的プロパティの集約場所として、ブログでは次のようなコードが紹介されていました。enum
を選ぶ着目点が面白いと思ったのですけど、実際のところはどうなのでしょう。その辺りに興味が湧いて考えてみたくなりました。
enum GlobalSettings {
static var isICloudEnabled = false;
static func saveSettings() {
}
}
列挙型に『複数の候補の中からどれかを選択する型』みたいに想像したとすると、このような使われ方が、一瞬なんのことかわからないみたいな、可読性を下げる要因にもなりそうな気もしたりしました。
ブログにあった『空の構造体やクラスだとインスタンス化ができてしまう』という観点で考えたときには、たとえばイニシャライザを private
にするという方法もあります。
struct GlobalSettings {
static var isICloudEnabled = false;
static func saveSettings() {
}
private init() {
}
}
構造体であれば、データを集約している場所という意味合いが出てきて enum
より可読性が上がりそうな気がするのと、こうしてイニシャライザを private
に限定することで、外からのインスタンス化を意図的に防げるメリットも出てきます。クラスについても同様のことができて、データを集約管理しているような印象になって良さそうです。
シングルトンとグローバル変数を使う方法
もう少し思いを巡らせてみると、設定を保存するストレージが欲しい場面なのだから、静的プロパティにこだわらないで、クラスで作ってシングルトンでインスタンス化しても良いのかなとも思いました。ついでに、グローバル設定なのだからインスタンスをグローバルにおいて、イニシャライザを private
で保護してあげれば設定用のインスタンスを2重に作ったり差し替えられたりされなくなるので良さそうです。
var settings = GlobalSettings()
final class GlobalSettings {
var isICloudEnabled = false;
func saveSettings() {
}
private init() {
}
}
こんな感じでグローバル変数を使っても、Swift の場合はモジュール単位で名前空間が作られるので、他と衝突しないところも「こんな書き方もありなのかな?」と感じたところです。
シングルトンを構造体で扱うことの問題点
さて、上の例なのですけど、自分は最初、構造体を使って次のように記載していました。
var settings = GlobalSettings()
struct GlobalSettings {
var isICloudEnabled = false;
func saveSettings() {
}
private init() {
}
}
これについて @yashigani さんから『構造体をグローバル変数に持たせた場合、それを別変数に代入したときに値の複製が作られてしまうから、うっかりすると破綻しかねない。クラスの方が適切だろう』という趣旨の指摘を頂きました。
なるほど、たしかにそうですね。
構造体で定義していたときは、次のように、どんなところからもグローバル変数の settings
を直接操作することを期待していました。
@IBAction func pushICloudEnableButton(sender: AnyObject) {
settings.isICloudEnabled = true
settings.saveSettings()
}
ただし、例えば、次のような何の変哲もないコードを書いたとき、たぶん期待した通りに動きません。
// 設定を更新したつもりが、反映されない。
func saveSettings(settings: GlobalSettings) {
settings.saveSettings()
}
saveSettings(settings)
同じようにこのようなコードも、たぶん期待通りに動きません。
func iCloudEnable(enable: Bool) {
var _settings = settings
_settings.isICloudEnabled = enable
_settings.saveSettings()
}
これは Swift の構造体が、別の変数に代入したときに同一の値を持った別のインスタンスが作られるという性質のためです。たしかに、ここまで考えていくと、シングルトンという観点でいえばシングルトンを保証できていないですね。
ということは、シングルトンを作ろうと思った時に構造体を持ち出すのは根本的に間違っていそうです。グローバルに置いて各々の設定項目を管理するという観点からもクラスが適切なように思いました。クラスであれば、Swift のクラスは他の変数に代入しても必ず同じインスタンスを保持するので、上で書いた、構造体では期待通りに動かなかったコードも、期待通りに動いてくれるので、それを扱う側にとっても間違いのないコードが書けることにつながりそうです。
8. isKindOfClass() の代わりに is を使用する
isKindOfClass: を使うメリット
if dictionary["value"] is String {
}
こちらは補足ですけれど、Swift で isKindOfClass
が使用できるのは NSObject
を継承しているクラスだけに限られるので、原則的には is 演算子を使うことになると思います。
ただし、Swift の is
演算子と NSObject
の isKindOfClass
とは互いに関係しているわけではないようなので、滅多にないこととは思いますけど、もし Objective-C 互換クラスで isKindOfClass
をオーバーライドしてたりすると is
演算子との挙動が変わってきたりする可能性はあるみたいでした。
switch–case にも is 条件が使える
ブログで紹介されていた通り、こうやって switch
で型に応じてふるい分けられるのは手軽で嬉しいところですね。if 文をこれだけ条件分岐をしようとすると読みづらくなってくるので、こんな風に switch
を活用できると嬉しいことが増える気がします。
let dictionary: NSDictionary = ["value" : 100]
switch dictionary["value"] {
case is String:
print("It's a string!")
case is Int:
print("It's a number!")
case is NSObject:
print("It's an object!")
default:
print("others")
}
そしてこのとき、値の型を判定したら、その型として値を使いたいことって多いと思うので、そのようなときには次のように書くとさらに嬉しくなれるかもしれません。
switch dictionary["value"] {
case let value as String:
print("\(value) is a string!")
case let value as Int:
print("\(value) is a number!")
case let value as NSObject:
print("\(value) is an object!")
default:
print("others")
}
このようにすると、型の判定と合わせて値を取得して、その値を使って case
文内で処理が書けます。このときに嬉しいのが、取得した value
変数が判定した型になってくれるので、判定が終わって直ぐに使い始められるところです。
7. self はクロージャー内でも宣言できる
ブログで挙げられていたこれについては、個人的にはちょっと怖いかなって思いました。
session.requestWithCompletion { [weak self] in
guard let `self` = self else {
return
}
self.showDialog()
}
扱い的に self は予約語で、対して `self` は予約語と同じ名前の変数なので、本来はそれぞれ別もののはずに感じます。それをどちらも同じものとして扱えているこのパターンが、どうにも自然なものには見えないのが理由です。
これをするなら、次のように self ではなく `self` を使って showDialog
を呼び出すのが適切かなと思います。これなら仕様的にも妥当なはずです。
session.requestWithCompletion { [weak self] in
guard let `self` = self else {
return
}
`self`.showDialog()
}
3. respondsToSelector: の使用をやめて、Swift の Availability APIを使用する
これは Swift の嬉しいところですね。実装の有無チェックを型に依存することがなくなったので、より確実に区別できるようになった気がします。
それと respondsToSelector:
は isKindOfClass:
と同じように NSObject
が持つ機能なので NSObject
を継承したクラスに限ってしか使えないというのもあります。それ以外の場面では使えないので、そういう面でも Swift の #available
がとても大事な存在になります。
Swift で Objective-C を活かす
そして respondsToSelector:
も、Swift で Objective-C ならではの真価を存分に発揮したい時には便利なように思います。
たとえば Objective-C でお馴染みの respondsToSelector:
と performSelector:
のセットであれば、バージョンに依らずメソッドの有無を判断して対応できるので、OS が目的のバージョンを満たしていなくても、互換機能をカテゴリ拡張や派生クラスで提供されていれば処理を継続できるみたいなメリットもありそうです。
そんな、それぞれの性質の違いを意識して使い分けてみると、さらに楽しいことになるかもわかりません。
2. 可能な限りジェネリクスやプロトコルで実装する
この辺りは、個人的に今すごく興味のあるところで、ジェネリックはすごく便利なんですけど、プロトコル拡張と相まった時に、その辺りの世界観をよく知っていないと思わぬ動きをするかもしれないところがあるので、そこをおさえないまま『可能な限り』使っていくと、思わぬ誤動作を招く恐れもあるかもしれません。型に実装するということについても、大切にしていきたいなと思うところもあったりします。
でもそういうのって、そうやって積極的に使っていって初めて分かるところでもあると思うので、積極的にむしろブログの本題にあるくらいの気概で望むことには超賛同です。ぜひぜひ積極的に、たとえどんな困難があっても、ジェネリックとプロトコルに戯れてみてくれると自分は嬉しいです。
clamp 定義の脱字
さて、ブログの中の clamp
関数ですけれど、たぶん HTML かエディターの仕様の都合で、肝心の型制約が抜けているみたいなので追記しておきます。おそらく <T:Comparable>
が抜けているので、それを補うと次の通りの定義になります。
func clamp<T:Comparable>(minValue: T, _ value: T, _ maxValue: T) -> T {
return min(max(value, minValue), maxValue)
}
ジェネリック関数の引数の場合は『どんな型でもいいけれど、受け取る』みたいな意味合いになるので、それだけだと何もできない型、もうちょっというと何ができるかわからない型になります。今回の例では、せめて min
と max
で大小比較できる型を使いたいので、せめて大小比較が可能な Comparable
対応の型を受け取るみたいに書いてあげる必要があります。
ちなみに min
関数と max
関数は、次のように Comparable
に対応した型に対して動作するように作られています。そのため、少なくとも clamp
関数では Comparable
に対応した型を受け取りさえすれば、これらを使った処理を滞りなく遂行できます。
func min<T : Comparable>(x: T, _ y: T) -> T
func max<T : Comparable>(x: T, _ y: T) -> T
おしまい
そんな感じで Swiftならこう書くシリーズ 10選 を拝見して、思うことを綴ってみました。
Swift ならではの書き方みたいなところってたくさんあって面白いので、元ブログでも模索されていたみたいに、ぜひ Swift に慣れてきたら『Objective-C で書くとこうだけど Swift でそれらしく書くとどんな風になるんだろう』みたいなことも意識しながら Swift と戯れてみると、いろんなものが見えてきて Swift がもっと楽しくなると思います。
そんな風にして Swift と遊んで、いろんな人の『Swift ならこう書くシリーズ』を眺められたら、楽しそうに思いました。