概要
Swift4.2で導入されたDynamic Member Lookupの機能について紹介します。
といっても使い方は@dynamicMemberLookup
のアノテーションを付与すれば使えるようになりますが、この機能でSwiftのコードがどれだけ可読性が上がるのかを理解できるようになります。
参考記事
- [Xcode10] Swift4.2でパワーアップしたことについてをまとめました[日本語訳]
- iOSDC2018で紹介されていたdynamicMemberLookupに感動したので深掘りする
- [Swift 4.2] Dictionayでプロパティアクセス
この辺りが参考になります。
今後、Custom subscriptは機械学習の発展によって活用の場が増えてくると思いますのでこれを機に流れだけでも把握するのはアリだと思います。
また、こちらは下記の記事を翻訳したものです。翻訳の許可を頂いてます。
では始めて行きます!
導入
独自の型を添字で拡張する方法を学び、ネイティブの配列や辞書と同じように単純な構文でそれらに添字を付けることができるようにします。
Custom subscriptsは、コードの利便性と読みやすさを向上させる強力な言語機能です。
演算子のオーバーライドと同様に、Custom subscriptsを使用すると、ネイティブのSwift構文を使用できます。 より冗長なcheckerBoard.objectAt(x:2、y:3)
ではなく、checkerBoard [2] [3]
のような簡潔なものを使用できます。
このチュートリアルでは、playgroundで基本的なチェッカーゲームの基礎を築くことによってCustom subscriptsを探ります。あなたはそれがボードの周りにピースを動かすために購読を使うことがどれほど簡単であるかわかります。 あなたが終わったら、あなたはあなたの指のすべての暇な時間中にあなたの指を占有し続けるために新しいゲームを作るためにあなたの方法でうまくいくでしょう。
そしてあなたはsubscriptsについてももっともっと知っているでしょう!
このチュートリアルは、あなたがすでにSwift開発の基本を知っていることを前提としています。 もしSwiftを初めて使用するのであれば、私達の初心者向けSwiftチュートリアルをチェックするか、あるいはまずSwift Apprenticeを読んでください。
Getting Started(はじめに)
まず始めに、新しいplaygroundを作りましょう。 File▸New▸Playground…の順に進み、iOS▸Blankのテンプレートを選択し、次へをクリックします。 ファイルにSubscripts.playgroundという名前を付けて、[Create]をクリックします。
デフォルトのテキストを次のように置き換えます。
import Foundation
struct Checkerboard {
enum Square: String {
case empty = "▪️"
case red = "🔴"
case white = "⚪️"
}
typealias Coordinate = (x: Int, y: Int)
private var squares: [[Square]] = [
[ .empty, .red, .empty, .red, .empty, .red, .empty, .red ],
[ .red, .empty, .red, .empty, .red, .empty, .red, .empty ],
[ .empty, .red, .empty, .red, .empty, .red, .empty, .red ],
[ .empty, .empty, .empty, .empty, .empty, .empty, .empty, .empty ],
[ .empty, .empty, .empty, .empty, .empty, .empty, .empty, .empty ],
[ .white, .empty, .white, .empty, .white, .empty, .white, .empty ],
[ .empty, .white, .empty, .white, .empty, .white, .empty, .white ],
[ .white, .empty, .white, .empty, .white, .empty, .white, .empty ]
]
}
Checkerboardには3つの定義があります。
-
Square
はボード上の個々の正方形の状態を表します。.empty
は空の正方形を表し、.red
と.white
はその正方形上の赤または白の部分の存在を表します。 -
Coordinate
は2つの整数のタプルの別名です。 ボード上の正方形にアクセスするには、このタイプを使用します。 -
squares
はボードの状態を格納する2次元配列です。
次にこちらを追加します。
extension Checkerboard: CustomStringConvertible {
var description: String {
return squares.map { row in row.map { $0.rawValue }.joined(separator: "") }
.joined(separator: "\n") + "\n"
}
}
これはCustomStringConvertible
に適合を追加するための拡張です。 カスタムなdescription
を使用すると、checkerboardをコンソールに印刷できます。
View▸Debug Area▸Show Debug Areaの表示の順に選択してコンソールを開き、playgroundの下部に以下の行を入力します。
var checkerboard = Checkerboard()
print(checkerboard)
このコードはCheckerboard
のインスタンスを初期化します。 その後、CustomStringConvertible
実装を使用してdescription
プロパティをコンソールに出力します。
[Execute Playground]ボタンを押すと、コンソールの出力は次のようになります。
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️🔴▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
Getting and Setting Pieces (ピースの取得と設定)
コンソールを見ると、どの部分が特定の正方形を占めているかを知るのは非常に簡単ですが、あなたのプログラムはまだそのような力を持っていません。 squares
配列は**private(非公開)**なので、どのプレイヤーが指定された座標にいるのかわかりません。
ここで重要なポイントがあります:正方形の配列はボードの実装です。 ただし、Checkerboard
のユーザーは、このタイプの実装については何も知りません。
型はその内部実装の詳細からユーザを保護するべきです。 そのため、正方形配列がprivateになります。
squares
配列を割り当てる場所の後に、Checkerboard
に次のメソッドを追加します。
func piece(at coordinate: Coordinate) -> Square {
return squares[coordinate.y][coordinate.x]
}
mutating func setPiece(at coordinate: Coordinate, to newValue: Square) {
squares[coordinate.y][coordinate.x] = newValue
}
配列に直接アクセスするのではなく、Coordinate
タプルを使用して - squares
にアクセスする方法に注目してください。 実際の格納メカニズムは配列の配列です。それはまさにあなたがユーザから保護するべきである実装の詳細の一つです!
Defining Custom Subscripts (Custom Subscriptsを定義する)
お気づきかもしれませんが、これらのメソッドはプロパティのgetterメソッドとsetterメソッドの組み合わせのように非常によく似ています。 多分それらをcomputed propertyとして実装するべきか?
残念ながらそれはうまくいきません。これらのメソッドはCoordinate
パラメータを必要とし、computed propertiesはパラメータを持つことができません。それは方法にこだわっているということですか?
この特別なケースが、まさにsubscriptsの目的です。
subscriptsの定義方法を見てください。
subscript(parameterList) -> ReturnType {
get {
// return someValue of ReturnType
}
set (newValue) {
// set someValue of ReturnType to newValue
}
}
Subscriptの定義は、関数定義とcomputed property定義の両方の構文を混在させます。
- 最初の部分は、パラメータリストと戻り値の型を含む、関数定義とよく似ています。
func
キーワードと関数名の代わりに、特別なsubscript
キーワードを使用します。 - メインは、ゲッターとセッターを持つcomputed propertyによく似ています。
関数とプロパティの構文の組み合わせは、subscriptsの力を際立たせています。 これはインデックス付きコレクションの要素にアクセスするためのショートカットを提供します。 これについてはすぐに詳しく説明しますが、まず、次の例を検討してください。
piece(at :)
とsetPiece(at:to :)
を次のsubscriptに置き換えます。
subscript(coordinate: Coordinate) -> Square {
get {
return squares[coordinate.y][coordinate.x]
}
set {
squares[coordinate.y][coordinate.x] = newValue
}
}
このsubscriptのgetterおよびsetterは、それらが置き換えるメソッドを実装するのとまったく同じ方法で実装します。
-
Coordinate
を指定すると、ゲッターは列と行に正方形を返します。 -
Coordinate
とvalueが与えられると、セッターは列と行の四角形にアクセスしてその値を置き換えます。
playgroundの最後に次のコードを追加して、新しいsubscriptにテストドライブを付けます。
let coordinate = (x: 3, y: 2)
print(checkerboard[coordinate])
checkerboard[coordinate] = .white
print(checkerboard)
playgroundは(3、2)の部分が赤であることをあなたに教えてくれるでしょう。白に変更すると、コンソールの出力は次のようになります。
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
Comparing Subscripts, Properties and Functions (Subscripts、プロパティ、関数の比較)
Subscriptsは多くの点でcomputed propertiesと似ています。
- それらはgetterとsetterから成ります。
- setterはオプションです。つまり、Subscriptsはread-write(読み書き可能)またはread-only(読み取り専用)にすることができます。
- 読み取り専用のSubscriptsには、明示的なgetブロックやsetブロックは必要ありません。 全体がgetterです。
- setterには、subscriptの戻り値の型と等しい型を持つデフォルトパラメータ
newValue
があります。 通常、このパラメータを宣言するのは、名前をnewValue
以外の名前に変更するときだけです。 - ユーザーはsubscriptsが速いことを望み、できればO(1)なので、短くてきれいにしてください。
computed propertiesとの大きな違いは、subscripts自体にはプロパティ名がないことです。 演算子オーバーライドと同様に、subscriptsを使用すると、コレクションの要素にアクセスするために通常使用される言語レベルの角かっこ[]
を上書きできます。
Subscriptsもパラメータリストと戻り値の型を持つという点で関数と似ていますが、次の点で異なります。
- Subscriptパラメータには、デフォルトでは引数ラベルがありません。 それらを使用したい場合は、それらを明示的に追加する必要があります。
- Subscriptsには、
inout
またはdefaultパラメータを使用できません。 ただし、可変長(...
)パラメータは使用できます。 - Subscriptsはエラーをスローできません。 つまり、subscriptゲッターはその戻り値を通じてエラーを報告する必要があり、subscriptセッターはエラーをスローまたは返すことはできません。
Adding a Second Subscript (2番目の添え字を追加する)
subscriptsが関数に似ているもう1つのポイントがあります。それらはオーバーライドされる可能性があるということです。つまり、パラメータリストや戻り値の型が異なる限り、1つの型に複数のsubscriptsを付けることができます。
Checkerboard
の既存のsubscript定義の後に次のコードを追加します。
subscript(x: Int, y: Int) -> Square {
get {
return self[(x: x, y: y)]
}
set {
self[(x: x, y: y)] = newValue
}
}
このコードは、Coordinate
タプルではなく2つの整数を受け入れる2番目のsubscriptをCheckerboard
に追加します。
playgroundの最後に次の行を追加して、この新しいsubscriptを試してください。
print(checkerboard[1, 2])
checkerboard[1, 2] = .white
print(checkerboard)
(1、2)の部分が赤から白に変わります。
Using Dynamic Member Lookup
Swift 4.2の新機能は、dynamic member lookupの言語機能です。これにより、型にランタイムプロパティを定義できます。つまり、ドット(.
)表記を使用して値やオブジェクトにインデックスを付けることができますが、特定のプロパティを事前に定義する必要はありません。
これはデータベースやリモートサーバーのオブジェクトのように、オブジェクトに実行時に定義された内部データ構造がある場合に最も役立ちます。 別の言い方をすればこれはNSObject
サブクラスを必要とせずにSwiftにキー値コーディングをもたらします。
この機能には2つの部分が必要です。@dynamicMemberLookup
アノテーションと特別な形式のsubscript
です。
A Third Subscript
まずsubscriptのオーバーライドをもう1つ追加して、dynamic lookupの基盤を築きます。 これは文字列を使ってcoordinateを定義します。
上記のsubscript定義の下に次のコードを追加します。
private func convert(string: String) -> Coordinate {
let expression = try! NSRegularExpression(pattern: "[xy](\\d+)")
let matches = expression
.matches(in: string,
options: [],
range: NSRange(string.startIndex..., in: string))
let xy = matches.map { String(string[Range($0.range(at: 1), in: string)!]) }
let x = Int(xy[0])!
let y = Int(xy[1])!
return (x: x, y: y)
}
subscript(input: String) -> Square {
get {
let coordinate = convert(string: input)
return self[coordinate]
}
set {
let coordinate = convert(string: input)
self[coordinate] = newValue
}
}
このコードはいくつかのことを追加します。
- 最初に、
convert(string :)
はx#y#(ここで、 '#'は数字です)の形式の文字列を取り、x値とy値を持つCoordinate
を返します。正規表現のパターンが一致しなかった場合は通常エラーをスローしますが、subscriptsはエラーをthrow
することができないため、とにかくクラッシュ以外にできることは多くないため、この場合はtry
が強制されます。 - それから新しく導入されたsubscriptは文字列を取り、それを
Coordinate
に変換し、そして先に定義された最初のCoordinateを再利用します。
次の行をplaygroundに追加して、これを試してみてください
print(checkerboard["x2y5"])
checkerboard["x2y5"] = .red
print(checkerboard)
今回は、6行目の白の1つが赤に変わります。
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️⚪️▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️🔴▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
Implementing Dynamic Member Lookup
これまでのところ、これはdynamic member lookuではなく、特別な形式の単なる文字列インデックスです。 次に、あなたはシンタックスシュガーを振りかけます。
まず、playgroundの最上部、struct
キーワードの直前に次の行を追加します。
@dynamicMemberLookup
それから他のsubscript
定義の下に以下を追加してください。
subscript(dynamicMember input: String) -> Square {
get {
let coordinate = convert(string: input)
return self[coordinate]
}
set {
let coordinate = convert(string: input)
self[coordinate] = newValue
}
}
これは最後のsubscriptと同じですが、特別な引数label:dynamicMemberがあります。このsubscript
シグネチャとタイプのアノテーションを使用すると、ドットシンタックスを使用してCheckerboard
にアクセスできます。
うわー、すぐに文字列インデックスは角かっこ([]
)の中にある必要はありません。 インスタンス上で直接文字列にアクセスできます。
これらの最後の行をplaygroundの一番下に追加することで実際にそれを見てください。
print(checkerboard.x6y7)
checkerboard.x6y7 = .red
print(checkerboard)
playgroundをもう一度走らせると、最後の白い部分が赤に変わります。
▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️⚪️▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️🔴▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️🔴▪️
A Word of Warning
Dynamic lookupは、コードを非常にきれいにする強力な機能、特にサーバーやスクリプトコードです。 ドット表記でアクセスするので、コンパイル時にオブジェクトの構造を定義する必要はもうありません。
それでも、いくつかの危険な欠点があります。
たとえば、@dynamicMemberLookup
アノテーションは基本的に、プロパティ名の有効性をチェックしないようにコンパイラに指示します。型チェックと明示的に定義されたプロパティの補完はまだ得られますが、今度はピリオドの後に何でも置くことができ、コンパイラはエラーを出しません。 あなたがタイプミスをした場合にのみ、実行時に見つけるでしょう。
この行をplaygroundに追加しても、実行するまでエラーにはなりません。
checkerboard.queen
Where to Go From Here?
このチュートリアルの上部または下部にあるDownload Materialsボタンを使用して、最終的なplaygroundをダウンロードできます。
ツールキットにsubscriptsを追加したので、自分のコードでそれらを使用する機会を探しましょう。適切に使用されると、それらはあなたのコードをより読みやすく直観的にします。
そうは言っても、いつもsubscriptsに戻ることを望まないと思います。APIを書いている場合、ユーザーはインデックス付きコレクションの要素にアクセスするためにsubscriptsを使うことに慣れています。 他のものにそれらを使用することは不自然で強制されるでしょう。
subscriptsの詳細については、AppleのThe Swift Programming Languageドキュメントのこの章を参照してください。