Swift4.2で導入された機能 Dynamic Member LoopupでSwiftのコードをよりCOOLにしよう

概要

Swift4.2で導入されたDynamic Member Lookupの機能について紹介します。

といっても使い方は@dynamicMemberLookupアノテーションを付与すれば使えるようになりますが、この機能でSwiftのコードがどれだけ可読性が上がるのかを理解できるようになります。

参考記事

この辺りが参考になります。
今後、Custom subscriptは機械学習の発展によって活用の場が増えてくると思いますのでこれを機に流れだけでも把握するのはアリだと思います。

また、こちらは下記の記事を翻訳したものです。

Custom Subscripts in Swift

では始めて行きます!

導入

独自の型を添字で拡張する方法を学び、ネイティブの配列や辞書と同じように単純な構文でそれらに添字を付けることができるようにします。

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]をクリックします。

デフォルトのテキストを次のように置き換えます。

Subscripts.playground.swift
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次元配列です。

次にこちらを追加します。

Subscripts.playground.swift
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の下部に以下の行を入力します。

Subscripts.playground.swift
var checkerboard = Checkerboard()
print(checkerboard)

このコードはCheckerboardインスタンスを初期化します。 その後、CustomStringConvertible実装を使用してdescriptionプロパティをコンソールに出力します。

[Execute Playground]ボタンを押すと、コンソールの出力は次のようになります。

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️🔴▪️🔴▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

Getting and Setting Pieces (ピースの取得と設定)

コンソールを見ると、どの部分が特定の正方形を占めているかを知るのは非常に簡単ですが、あなたのプログラムはまだそのような力を持っていません。 squares配列はprivate(非公開)なので、どのプレイヤーが指定された座標にいるのかわかりません。

ここで重要なポイントがあります:正方形の配列はボードの実装です。 ただし、Checkerboardのユーザーは、このタイプの実装については何も知りません。

型はその内部実装の詳細からユーザを保護するべきです。 そのため、正方形配列がprivateになります。

squares配列を割り当てる場所の後に、Checkerboardに次のメソッドを追加します。

Subscripts.playground.swift
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の定義方法を見てください。

Subscripts.playground.swift
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に置き換えます。

Subscripts.playground.swift
subscript(coordinate: Coordinate) -> Square {
  get {
    return squares[coordinate.y][coordinate.x]
  }
  set {
    squares[coordinate.y][coordinate.x] = newValue
  }
}

このsubscriptのgetterおよびsetterは、それらが置き換えるメソッドを実装するのとまったく同じ方法で実装します。

  • Coordinateを指定すると、ゲッターは列と行に正方形を返します。
  • Coordinatevalueが与えられると、セッターは列と行の四角形にアクセスしてその値を置き換えます。

playgroundの最後に次のコードを追加して、新しいsubscriptにテストドライブを付けます。

Subscripts.playground.swift
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)なので、短くてきれいにしてください。

checkers.png

computed propertiesとの大きな違いは、subscripts自体にはプロパティ名がないことです。 演算子オーバーライドと同様に、subscriptsを使用すると、コレクションの要素にアクセスするために通常使用される言語レベルの角かっこ[]を上書きできます。

Subscriptsもパラメータリストと戻り値の型を持つという点で関数と似ていますが、次の点で異なります。

  • Subscriptパラメータには、デフォルトでは引数ラベルがありません。 それらを使用したい場合は、それらを明示的に追加する必要があります。
  • Subscriptsには、inoutまたはdefaultパラメータを使用できません。 ただし、可変長(...)パラメータは使用できます。
  • Subscriptsはエラーをスローできません。 つまり、subscriptゲッターはその戻り値を通じてエラーを報告する必要があり、subscriptセッターはエラーをスローまたは返すことはできません。

Adding a Second Subscript (2番目の添え字を追加する)

subscriptsが関数に似ているもう1つのポイントがあります。それらはオーバーライドされる可能性があるということです。つまり、パラメータリストや戻り値の型が異なる限り、1つの型に複数のsubscriptsを付けることができます。

Checkerboardの既存のsubscript定義の後に次のコードを追加します。

Subscripts.playground.swift
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を試してください。

Subscripts.playground.swift
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定義の下に次のコードを追加します。

Subscripts.playground.swift
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
  }
}

このコードはいくつかのことを追加します。

  1. 最初に、convert(string :)はx#y#(ここで、 '#'は数字です)の形式の文字列を取り、x値とy値を持つCoordinateを返します。正規表現のパターンが一致しなかった場合は通常エラーをスローしますが、subscriptsはエラーをthrowすることができないため、とにかくクラッシュ以外にできることは多くないため、この場合はtryが強制されます。
  2. それから新しく導入されたsubscriptは文字列を取り、それをCoordinateに変換し、そして先に定義された最初のCoordinateを再利用します。

次の行をplaygroundに追加して、これを試してみてください

Subscripts.playground.swift
print(checkerboard["x2y5"])
checkerboard["x2y5"] = .red
print(checkerboard)

今回は、6行目の白の1つが赤に変わります。

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️⚪️▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️🔴▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️⚪️▪️

Implementing Dynamic Member Lookup

これまでのところ、これはdynamic member lookuではなく、特別な形式の単なる文字列インデックスです。 次に、あなたはシンタックスシュガーを振りかけます。

まず、playgroundの最上部、structキーワードの直前に次の行を追加します。

Subscripts.playground.swift
@dynamicMemberLookup

それから他のsubscript定義の下に以下を追加してください。

Subscripts.playground.swift
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の一番下に追加することで実際にそれを見てください。

Subscripts.playground.swift
print(checkerboard.x6y7)
checkerboard.x6y7 = .red
print(checkerboard)

playgroundをもう一度走らせると、最後の白い部分が赤に変わります。

▪️🔴▪️🔴▪️🔴▪️🔴
🔴▪️🔴▪️🔴▪️🔴▪️
▪️⚪️▪️⚪️▪️🔴▪️🔴
▪️▪️▪️▪️▪️▪️▪️▪️
▪️▪️▪️▪️▪️▪️▪️▪️
⚪️▪️🔴▪️⚪️▪️⚪️▪️
▪️⚪️▪️⚪️▪️⚪️▪️⚪️
⚪️▪️⚪️▪️⚪️▪️🔴▪️

A Word of Warning

Dynamic lookupは、コードを非常にきれいにする強力な機能、特にサーバーやスクリプトコードです。 ドット表記でアクセスするので、コンパイル時にオブジェクトの構造を定義する必要はもうありません。

それでも、いくつかの危険な欠点があります。

たとえば、@dynamicMemberLookupアノテーションは基本的に、プロパティ名の有効性をチェックしないようにコンパイラに指示します。型チェックと明示的に定義されたプロパティの補完はまだ得られますが、今度はピリオドの後に何でも置くことができ、コンパイラはエラーを出しません。 あなたがタイプミスをした場合にのみ、実行時に見つけるでしょう。

この行をplaygroundに追加しても、実行するまでエラーにはなりません。

Subscripts.playground.swift
checkerboard.queen

Where to Go From Here?

このチュートリアルの上部または下部にあるDownload Materialsボタンを使用して、最終的なplaygroundをダウンロードできます。

ツールキットにsubscriptsを追加したので、自分のコードでそれらを使用する機会を探しましょう。適切に使用されると、それらはあなたのコードをより読みやすく直観的にします。

そうは言っても、いつもsubscriptsに戻ることを望まないと思います。APIを書いている場合、ユーザーはインデックス付きコレクションの要素にアクセスするためにsubscriptsを使うことに慣れています。 他のものにそれらを使用することは不自然で強制されるでしょう。

subscriptsの詳細については、AppleThe Swift Programming Languageドキュメントのこの章を参照してください。