Swiftのサブスクリプト(Subscripts)

Swiftのサブスクリプト(Subscripts)

サブスクリプト(Subscripts)です。これまでコレクション型へのアクセスで何気なく使っていました。独自型(クラスなど)で定義できる構文で、メソッドを定義せずに演算を返したりできるので、大変便利な機能になります。

ここでは、サブスクリプトとは何か?から始めて、その定義構文、サブスクリプトの使い所、さらに複数のパラメータを持つサブスクリプトの具体例について説明していきます。

サブスクリプト(Subscripts)って何?

いきなり脱線しますが、英語で「subscript」(カタカナで当て字するとサブスクリプトでしょうか?)と言う時は、普通「下付き文字」のことを指します。例えば、水を表す化学式H2Oの2は下付き文字です。ちなみに、上付き文字は「superscript」(スーパースクリプト)と言います。べき乗(例えば103とか)で良く使います。

プログラミング言語のサブスクリプトとは?

Swiftでのsubscriptは、角括弧(かくかっこ)[]で定義される特殊な構文で、コレクション型の要素にアクセスする際に使います。これは、Swift限定という訳ではなくて、角括弧でコレクション型にアクセスする構文を、プログラミング言語ではサブスクリプト(Subscripts)と呼んでいます。サブスクリプトは、クラスと構造体、さらに列挙型で定義できます。

例えば、コレクション型である配列(Array)辞書(Dictionary)では、サブスクリプトを使ってその要素へアクセスできます。

// コレクション型でサブスクリプトを使用
// 配列
let someArray = [1, 2, 3]
print(someArray[2])
//"3"と表示

// 辞書
let someDictionary = ["いち":1, "に":2]
print(someDictionary["に"]!)
//"2"と表示

Dictionaryの後ろのビックリマーク!は、オプショナル型(optionals)のforced unwrappingです。Dictionaryのサブスクリプトがoptionalsを返すため、forced unwrappingで強制的にoptionalsを外しています。

Swiftのサブスクリプトで出来ること(概要)

この後詳しく見ていきますが、Swiftでは、1つの型に対して複数のサブスクリプトを定義できます。サブスクリプトは関数等と違って特定の名前がありません。ですから、複数のサブスクリプトを定義した場合、ユーザが使う時に混乱しそうですが、そこは型推論です。指定したパラメータ(インデックス)の型によって、自動的に適切なサブスクリプトをコールするようです。また、サブスクリプトには複数のインデックスを定義することができます。これも後で具体例を使って説明します。

サブスクリプト構文(Subscript Syntax)

サブスクリプト定義構文は、インスタンスメソッド(instance methods)計算プロパティ(computed properties)の定義構文を組み合わせたような構文になっています。以下、通常の定義構文(読み書き両用)と読み出し専用の場合に分けて見ていきます。

サブスクリプトの定義構文(Subscript Definition Syntax)

サブスクリプトの定義構文は、インスタンスメソッドと計算プロパティの構文と似ています。

// Subscriptの定義構文
subscript(index name: type) -> type {
    get {
        ....   
    }
    set(newValue) {
        ....
    }
}

サブスクリプトの定義にはsubscriptキーワードを使います。インスタンスメソッドと同様に、1つまたは複数のパラメータ(上記の例では1つのパラメータ)と戻り値を指定できます。インスタンスメソッドとの違いは、

サブスクリプトが「読み書き両用(read-write)」または「読み出し専用(read-only)」のどちらかを選択できること

です。この機能はgetterとsetterで実装できますが、これはまさに計算プロパティみたいなものです。

Setterの「パラメータ」newValueは、サブスクリプトの戻り値の型と同じになります。計算プロパティの場合と同様に、newValueは指定しなくても構いません。その場合、デフォルトの名前newValueが使われます。

先ずは、読み出し専用のサブスクリプトについて詳しく見ていきます。

読み出し専用サブスクリプトの定義構文と具体例

読み出し専用のサブスクリプトの場合、getキーワードを省略することが出来て、

// Read-only subscriptの定義構文
subscript(index name: type) -> type {
    // 指定した戻り値を返す本体をここに記述
}

という具合になります。これもread-only computed propertyと同じ定義構文です。

全く実用的ではないですが、以下指定されたインデックス付きの文字列を返すサブスクリプトを作ってみました。

// Read-only subscriptの具体例
struct Vector {
    let name: String
    subscript(index: Int) -> String {
        return name + "_\(index)"
    }
}

let a = Vector(name: "a")
let b = Vector(name: "b")
print("\(a[0]), \(b[2])")
//"a_0, b_2"と表示

print("\(a[1]), \(b[5])")
//"a_1, b_5"と表示

上記の例では、Vector型の新しいインスタンスを2つ(ab)作っています。Vector内部で定義されているサブスクリプトは、整数Intのインデックス(index)を持ち、文字列Stringを返す、読み出し専用サブスクリプトです。サブスクリプトを見てもらえれば分かると思いますが、プロパティnameにインデックスを連結して返しているだけです。

複数のサブスクリプトを持つ場合の具体例

最初に複数のサブスクリプトを持つことも可能、と書きましたので、試しに作ってみます。手抜きですが、先程の例で作ったVectorを少し改造します。

// 複数のsubscriptを持つ場合
struct Vector {
    let name: String
    subscript(index: Int) -> String {
        return name + "_\(index)"
    }
    subscript(index: String) -> String {
        return name + "_{\(index)}"
    }
    subscript(index: Double) -> String {
        return name + "_{\(index)}"
    }
}

let a = Vector(name: "a")

// String -> String
print(a["subscript"])
// "a_{subscript}"と表示

// Double -> String
print(a[3.14])
// "a_{3.14}"と表示

戻り値は全てStringですが、パラメータが違います。上記の例のように、実際に使う場合は、入力したパラメータをSwiftが型推論して適切なサブスクリプトをコールしてくれます。

この例の場合、多分Genericsという機能を使ってコーディングするのがベストだと思います。というのは、サブスクリプトの機能として違うのはパラメータだけですので、パラメータの型を「一般的な型」として定義できれば、サブスクリプト1つだけで良いからです。これに関しては、Genericsの記事でまた触れたいと思います。Genericsというのは、C言語で言う所のTemplate機能です。

サブスクリプトのパラメータと戻り値

公式マニュアルに書いてあるサブスクリプトの仕様をまとめてみます。

  • パラメータの数は何個でも大丈夫
  • パラメータとして可変パラメータも使える。ただし、in-outパラメータは使えないし、デフォルト値をセットすることはできない。
  • 戻り値の型は自由(なんでもいい)

ということですので、テストしてみました。

// サブスクリプトのテスト
struct SomeStruct {
    // 複数のパラメータ
    subscript(index1: Int, index2: Double, index3: String) -> String {
        return "\(index1)_\(index2)_" + index3
    }

    // 可変パラメータ
    subscript(numbers: Int...) -> Int {
        var sum = 0
        for number in numbers {
            sum += number
        }
        return sum
    }

    // 戻り値
    subscript(index: Double) -> (Double, Double, Double) {
        return (index, index*10.0, index*100.0)
    }
}
let someStructure = SomeStruct()
print(someStructure[0, 1.1, "hello"])
//"0_1.1_hello"と表示

print(someStructure[0, 1, 2, 3, 4, 5])
//"15"と表示

print(someStructure[3.1415])
//"(3.1415, 31.415, 314.15)"と表示

面白いですね。戻り値としてtuplesも使えます。また、最後のサブスクリプトのパラメータをIntにすると、コンパイルエラーになります。これは、2つめのサブスクリプトで、可変パラメータInt...を指定しているため、もしIntだけのパラメータを認めてしまうと、この2つは区別がつかなくなるからです。

Swift 3以降では、Swift 2.x系で使えた変数パラメータ(パラメータへのvar指定)は使えなくなっています。関数で使えなくなったのと同じですね。また、in-outパラメータは指定時にコンパイルエラーになる仕様になりました。これまでは定義自体は出来るけど、実行時エラーになるという良く分からない状態だったので、良い変更だと思います。

サブスクリプトの使い所

サブスクリプトは、コレクション型の要素にアクセスする際のショートカットとして使われることが多いです。

コレクション型のページでも見てきましたが、例えば

// Dictionary
var color = ["赤": "ff0000", "red": "ff0000", "あか": "ff0000"]
color["朱"] = "ff0000"

のように、辞書コレクション型Dictionaryを使います。この時、サブスクリプトの角括弧[]内部にkeyを指定して、その要素に値(value)を代入しています。上記の例では、key-value対の型はどちらも文字列Stringですが、Dictionaryではkeyにもvalueにも好きな型を指定することができます。

もちろん、コレクション型に限らず、独自型のクラスなどでサブスクリプトを定義して使うこともできます。この次のセクションで、公式マニュアルに載っている行列(Matrix)を使って、(実用的な)独自型でのサブスクリプトについて見ていきます。

複数のパラメータを持つSubscriptの具体例 | 行列(Matrix)

公式マニュアルにある行列の例が非常に良いので、そのまま流用します。ちなみに、行列は英語では「Matrix」と言います。あの超有名な映画マトリックスのmatrixです。行は「Row」で列は「Column」です。

// 複数のパラメータを持つsubscript、行列
struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows*columns)
    }
    
    func indexIsValidForRow(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0
            && column < columns
    }
    
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValidForRow(row: row, column: column),
                   "index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValidForRow(row: row, column: column),
                   "index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

// 2行2列の行列
var matrix = Matrix(rows: 2, columns: 2)

順番に説明していきます。先ずは行列について少し補足します。

行列って何?

行列というのは、表(テーブル)に数字を並べたようなものです。(実際はちょっと違うんですが)イメージとしては、縦横の枠の数があらかじめ決められた表に数字が割り振られたもの、です。もうちょっと具体的には、エクセル(MacならNumbers)の1枠に1つ数字が割り振られているようなもの、だと思えば分かりやすいでしょうか?

表と行列

行列は、連立一次方程式を解く時のテクニックとして発展したようですが、実は現代物理学の根幹をなすような方程式(特殊相対論とか)などにちょこちょこ顔をだします。

構造体の初期化(Initialization)

上記のサンプルコードでは、行列を1次元配列gridで実装しています。

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows*columns)
    }
....

行(rows)と列(columns)は定数になっていますので、構造体Matrixを初期化する時に指定することになります。行列本体がgridという1次元配列(型はDouble)になります。Gridというのは「格子」とか「グリッド、マス目」という意味です。

構造体(クラスや列挙型も)などには、インスタンスを初期化するのに専用の特別な機能(initialization)があります。これはinit()という特別なメソッドを定義することで実装できます。この初期化のための特別な機能init()初期化子(initializer)と呼びます。初期化子に関しては別のページで詳しく説明します。

行列を1次元配列で表現

初期化では、定数プロパティのrowscolumnsにパラメータを渡しています。さらに、行列本体であるgrid配列を初期化しています。

grid = Array(repeating: 0.0, count: rows*columns)

この初期化を見ると、配列Arrayが構造体だということが良く分かると思います。
最初のパラメータrepeatingは各要素に入る値、2番目のパラメータcountが配列要素の数です。

したがって、上記の初期化は、

配列要素を全て0.0で初期化、rows*columns個の要素を持った配列を作る

ということです。

サブスクリプトを使って行列要素にアクセス

配列Arrayを用意したので、後は各配列要素に行列の1成分(行列要素)を割り当てれば良いことになります。行列要素の取り出しと、値の割り当てはサブスクリプトのgetterとsetterで実装しています。分かりやすいように、骨組みだけを取り出すと、

// サブスクリプトで行列用の配列にアクセス
subscript(row: Int, column: Int) -> Double {
    get {
        return grid[(row * columns) + column]
    }
    set {
        grid[(row * columns) + column] = newValue
    }
}

となります。行列要素を指定するには、当然「行」と「列」のインデックスが必要ですので、サブスクリプトのパラメータに行番号(row)と列番号(column)が入っています。

今、行列要素のインデックスをrow * columns + columnで指定しています。したがって、例えば2行2列の行列の場合、
行列要素のインデックス

このような感じで、行列要素のインデックスが割り当てられます。配列要素のインデックスが0から始まるということだけは注意が必要です。

実際にインデックスを指定する時は、次の例のようにコンマで分けて指定します。

// サブスクリプトに値を代入
matrix[0, 0] = 3.14
matrix[1, 1] = 1.618

この例では、左上(0,0)と右下(1,1)の行列要素に値を代入しています。

assert関数によるerror handling

行列要素を指定する際には、間違って配列要素よりも大きなインデックスを指定してしまう場合もあり得ます(余りないとは思いますが、インデックスに負の整数を指定することもあるかもしれません)。公式マニュアルの例では、assert(_:_:)という関数を使って、エラー検出をしています。

func indexIsValidForRow(row: Int, column: Int) -> Bool {
    return row >= 0 && row < rows && column >= 0
        && column < columns
}

subscript(row: Int, column: Int) -> Double {
    get {
        assert(indexIsValidForRow(row: row, column: column),
               "index out of range")
        return grid[(row * columns) + column]
    }
    ....
}

assert(_:_:)という関数は引数にブール値を取り、条件が真である場合のみプログラムを続行し、偽の場合はプログラムを強制終了します。その場合、デバッグするための情報(どこでクラッシュしたか?クラッシュした原因など)を出力します。

上記の例では、行列要素のインデックスをチェックするメソッドindexIsValidForRow()がブール値を返すので、このメソッド自体をassert(_:_:)のパラメータにしています。インデックスをチェックするメソッドは、多分簡単なので説明する必要はないと思いますが、入力した行(row)と列(column)が、設定範囲内に入っているかをチェックしています。

まとめると、

assert(indexIsValidForRow(row: row, column: column),
    "index out of range")

というのは、indexIsValidForRow(row:column:)が真(つまり、入力インデックスが設定された行と列番号の範囲内)ならプログラムを続行、偽ならばプログラムを終了する。という機能になります。実際にXcode上で試してみるとassert(_:_:)の機能が分かると思います。

まとめ

  • サブスクリプト(Subscripts)はクラス、構造体、列挙型等で定義できる
  • サブスクリプトへのアクセスには角括弧[]を使う
  • サブスクリプトの定義にはsubscriptキーワードを使う
  • 定義構文は、インスタンスメソッドと計算プロパティを組み合わせたような構文になる
  • サブスクリプトでは、何個でもパラメータを指定可能で、どんな型の戻り値でも良い