SwiftのExtensions(機能の拡張)

Swiftのextensions

Extensions(エクステンション)です。Extension(s)は和訳すると「拡張」「延長」「拡大」という意味です。Swiftでのextensionsの役割は「機能の拡張」ですから、元の意味から取ると「拡張」が一番近いかもしれません。

ここでは、extensionsとは何か?ということから始めて、extensionsの定義構文、プロパティやメソッド等々にどうやって適用するか、ということを詳しく説明します。

Extensionsって何?

Extensions = 機能の拡張

Swiftのextensionsを使うと、クラス、構造体列挙型、またはプロトコルの機能を拡張することが出来ます。「機能の拡張」と書きましたが、ここでの「機能」というのは、

のことを指します。各項目については、後で1つずつサンプルコードを使って見ていきます。

この機能拡張というのは、オリジナルソースコードにアクセス出来ないような型(Swiftの基本型とか)にも適用できるみたいで、こういう機構を「retroactive modeling」と呼びます。

Extensionsの最大の利点は大元のソースコードを改変しなくても良いということだと思います。

プロトコル拡張 | Protocol Extensions

プロトコル(Protocols)というのは、独自型(クラス、構造体、または列挙型)が持つプロパティメソッドの雛形を提供する機能です。プロトコルを使うことにより共通のインターフェイスを提供出来るので便利です。ここではプロトコルの詳細については割愛しますが、extensionsとプロトコルを組み合わせると、大きく分けて2つの機能拡張が可能です。

1つは既存の型に対して新たにプロトコルを採用することです。これは次のセクション「Extension構文」で簡単に紹介します。

もう1つはプロトコル拡張(Protocol Extensions)です。これは

  • プロトコルに具体的な機能を実装
  • 新しいインターフェイスを実装

という感じで、(名前の通り)プロトコル自体の機能を拡張するためのものです。プロトコル拡張についてはプロトコルのセクションで詳しく説明します。

公式マニュアルに注意書きがありますが、extensionsで出来ることは飽くまで新しい機能の追加で、既存のメソッド等をオーバーライドすることは出来ません。

“NOTE

Extensions can add new functionality to a type, but they cannot override existing functionality.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

Extension構文 | Extension Syntax

Extensionの定義構文

Extensionsを宣言する時はextensionキーワードを使います。

extension SomeType {
    ....
}

SomeTypeに入るのは当然既存の型でなければいけません。また、繰り返しになりますが、自分が作った独自型だけではなく、例えばSwiftの基本型(IntStringなど)に対しても、extensionsを使った機能拡張が可能です。

Extensionを使ったプロトコル準拠 | Protocol Conformance with an Extension

先程

1つは既存の型に対して新たにプロトコルを採用することです

と書きました。プロトコルのページで詳しく説明しますが、プロトコルに書かれたプロパティやメソッドの雛形は通常、採用したクラス等で必ず実装しないといけません。したがって、extensionを使って新たにプロトコルを採用することは、機能の拡張と言っても良いと思います。

Extensionを使って既存の独自型にプロトコルを採用する構文は以下の通りです。

extension SomeType: SomeProtocol, AnotherProtocol {
    ....
}

プロトコル採用の定義構文と比較すると分かりますが、先頭にextensionと付いていること以外は全く同じで、型の名前(クラスや構造体など)の後ろにプロトコル名を続けます。

“If you define an extension to add new functionality to an existing type, the new functionality will be available on all existing instances of that type, even if they were created before the extension was defined.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

と書いてあるように、extensionを定義してある型に新しい機能を追加すると、extension宣言より前に定義したその型のインスタンスでも、extensionで定義した新しい機能を使うことができるようです。後で試してみます。

計算プロパティ | Computed Properties

計算プロパティには、インスタンス計算プロパティ(computed instance properties)と型計算プロパティ(computed type properties)がありますが、extensionではどちらの機能拡張も可能です。

英単語の並び順を考えると「計算インスタンスプロパティ」や「計算型プロパティ」が良いかもしれませんが、計算プロパティであることを強調したかったので、「インスタンス計算プロパティ」のように訳しています

インスタンス計算プロパティ | Computed Instance Properties

公式ではDoubleの機能拡張をしていましたが、ここではIntでやってみます。

let someInt = 123
extension Int {
    // 文字列として取り出す
    var string: String { return "\(self)" }
}

print("number = " + 100.string)
//"number = 100"と表示

print("number = " + someInt.string)
//"number = 123"と表示

実装した機能拡張は1つだけで、整数を文字列変換するような計算プロパティです。

// 文字列として取り出す
var string: String { return "\(self)" }

当然selfInt型のインスタンスを指します。面白いのは、上記例でも書いたように整数リテラルから100.stringという具合に計算プロパティが呼び出せることです。

extensionを定義してある型に新しい機能を追加すると、extension宣言より前に定義したその型のインスタンスでも、extensionで定義した新しい機能を使うことができるようです。

と書きましたが、someIntの結果を見ると分かるように、extensionを宣言する前に作ったインスタンスsomeIntでもstringが使えることが分かります。

extensionを宣言する前に、extensionの機能(ここではstring)を使うことは出来ませんので、例えば

let someInt = 123
someInt.string //<-- コンパイルエラー。extension宣言前ではダメ
extension Int {
    // 文字列として取り出す
    var string: String { return "\(self)" }
}

のようなコードはコンパイルエラーになります。

型計算プロパティ | Computed Type Properties

基本型だと型プロパティの拡張はあまり恩恵がないような気がしますが、例えば

extension Int {
    static var one: Int { return 1 }
}

extension String {
    static var whiteSpace: String { return "" }
}

print(Int.one)
print(String.whiteSpace)

という感じで、本来リテラルで直接返す部分を変数にすることが出来ます。

また、(タイトルから自明ですが)格納プロパティ(stored properties)プロパティオブザーバ(property observers)は機能拡張として追加することが出来ません。

“Extensions can add new computed properties, but they cannot add stored properties, or add property observers to existing properties.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

初期化子 | Initializers

クラスで拡張出来るのはconvenience initializers

初期化子(Initializers)は、独自型が持っている格納プロパティに初期値を与える特別なメソッドです。Extensionを使って拡張できますが、クラスの場合で追加出来るのはconvenience initializersのようです。Designated initializersdeinitializers(終了子)を追加することは出来ません。これは、そもそもクラスを作る時にちゃんと実装しなさい、ということだと思います。

一応本当に作ることが出来ないか確認してみると、

class SomeClass {
    var property: String = ""
}

extension SomeClass {
    init(property: String) {
        self.property = property
    }
      
    deinit {
        
    }
}
//error: designated initializer cannot be declared in an extension of 'SomeClass'; did you mean this to be a convenience initializer?
//Deinitializers may only be declared within a class

という感じで怒られました。Deinitializersのエラーはデバッグエリアには表示されませんが、deinit宣言した部分にエラーメッセージが出て来ます。

値型の初期化子拡張

値型で自作初期化子を作ると、デフォルト初期化子が使えなくなる

値型の初期化子については、値型の初期化子の委譲のページで詳しく説明しました。

少し復習しますが、値型(ここでは構造体)を定義し、全てのプロパティに初期値を与えた場合、

struct Point {
    var x = 0.0, y = 0.0
}

ここでは暗黙的に2つの初期化子が作られています。1つはデフォルト初期化子で、もう1つはmemberwise initializerです。

これら2つの初期化子でPoint型インスタンスを作ると、

let point1 = Point()
let point2 = Point(x: 1.0, y: 2.0)

例えば上記のようになります。最初の初期化がデフォルト初期化子で、構造体宣言時にプロパティにセット(初期化)されている値0.0を使います。一方、2番目の初期化がmemberwise initializerで、与えられた全ての格納プロパティをパラメータとして取る初期化子になります。

ここで構造体に自作の初期化子を実装すると、どうなるでしょうか?

struct Point {
    var x = 0.0, y = 0.0
    // パラメータ1つでxとyを初期化
    init(xy: Double) {
        self.x = xy
        self.y = xy
    }
}

let point1 = Point() // <-- コンパイルエラー
let point2 = Point(x: 1.0, y: 2.0) // <-- コンパイルエラー

実際に自分で作った自作型等で試してもらうと分かりますが、先程作ったデフォルトで用意されている2つの初期化子が動かなくなります。これはSwiftの仕様で、値型で自作(カスタム)の初期化子を作ると、元々用意されているデフォルト初期化子での初期化が出来なくなってしまいます。初期化の委譲のページでは、デフォルト初期化子とmemberwise initializerを手で追加していましたが、新たに構造体などを作った時に毎回この2つを実装するのは手間です。

そこで、

自作の初期化子を追加して、かつデフォルトで用意されている初期化子を残せないのか?

というニーズに答えるのがextensionによる初期化子の追加です。

機能拡張を使ってデフォルト初期化子を残す

下のサンプルコードは、値型の初期化子の委譲で使った直線構造体Lineの初期化部分を、extensionを使って書き直したものです。

// 直線構造体
struct Line {
    var start = Point()
    var stop = Point()
}

extension Line {
    init(center: Point, length: Double, theta: Double) {
        let x1 = center.x - 0.5*length*cos(theta)
        let y1 = center.y - 0.5*length*sin(theta)
        let x2 = center.x + 0.5*length*cos(theta)
        let y2 = center.y + 0.5*length*sin(theta)
        
        self.init(start: Point(x:x1,y:y1), stop: Point(x:x2,y:y2))
    }
}

// デフォルト初期化子
let defaultLine = Line()

// Memberwise initializer
let memberwiseLine = Line(start:Point(x:0.0, y:0.0), stop:Point(x:1.0, y:2.0))

// Extensionで実装した自作の初期化子
let customLine = Line(center:Point(x:1.0, y:1.0), length: 2.0, theta:0.0)

中身は全く一緒ですが、大きな違いは、自作の初期化子を新たに追加しているにも関わらず、デフォルト初期化子とmemberwise initializerでインスタンスを作れることです。もう1つの(開発者側の)メリットは、本体の構造体Lineが相当スッキリしたことでしょうか。

メソッド | Methods

型メソッド(type methods)の拡張も出来ますが、ここではインスタンスメソッドについてのみの説明になります。

通常のメソッド拡張 | Non-mutating methods

メソッド(methods)の追加もできます。

extension Int {
    func isOdd() -> Bool { return self%2==1 }
    func isEven() -> Bool { return self%2==0 }
    func isDivisible(_ number: Int) -> Bool {
        return number==0 ? false : self%number==0
    }
}

0.isOdd() // false
0.isEven() // true
24.isDivisible(4) // true
24.isDivisible(5) // false

メソッドではパラメータも取ることが可能なので、計算プロパティに比べると色々面白いことが出来そうです。公式の具体例では、Intのextensionで、クロージャ(closure)をパラメータに取ってそれを繰り返し実行するような機能を追加しています。

Mutating Methodsの場合

自分自身の値を変更したい場合はmutating指定する必要があります。

extension Int {
    mutating func increment(number: Int = 1) {
        self += number
    }
}

var a = 1
a.increment()
a.increment()
print(a)
//"3"と表示

この例では、パラメータとして整数を取り(デフォルトは1)、それを自分自身に加算するメソッドです。デフォルトではインクリメント演算と等価です。自分自身を変えるメソッドの場合は、一度変数に格納してやらないとメソッドを実行出来ません。整数リテラルからこのメソッドを呼びだそうとするとコンパイルエラーになります。

サブスクリプト | Subscripts

サブスクリプトは配列(Arrays)などコレクション型を扱う際には非常に便利な機能です。独自型でも実装できる機能で、extensionに実装することも可能です。ここでは、サブスクリプトを使ってStringの中の一文字を抜き出したり、範囲演算子を使って部分的に文字列を取り出す、という機能を実装しています。

extension String {
    // 文字列のインデックスを取得するメソッドへのショートカット
    func id(index: Int) -> Index {
        return self.index(self.startIndex, offsetBy: index)
    }
    
    // 整数のインデックスで文字を返す
    subscript(index: Int) -> String {
        return String(self[id(index: index)])
    }
    
    // 範囲演算子で文字列を抜き出す
    subscript(indices: CountableRange<Int>) -> String {
        let range = id(index: indices.lowerBound)..<id(index: indices.upperBound)

        // substringメソッド
        return self.substring(with: range)
        
        // またはsubscript
//        return self[range]
    }
}

var string = "hello world"
// String1文字
string[0] //"h"

// 範囲演算子(half-open range operator)で指定
string[1..<3] // "el"

// indexを使って直接指定
string[string.index(string.startIndex, offsetBy: 1)] // "e"

Stringを構成する文字列の1文字はCharacter型で、そいつらは配列として格納されています。その1文字を取り出す場合、Stringが持っているIndexという型でインデックスを指定しないといけません。

例えば、上記の文字列stringから、その2番目の文字"e"を取り出す場合は、

string[string.index(string.startIndex, offsetBy: 1)]

という感じになります(サンプルコードの一番下の例)。毎回この長いインデックスを書くのが面倒なので、IntからIndex型に変換する部分をメソッド化しています。

1文字をStringとして取り出す場合は、

extension String {
    // 文字列のインデックスを取得するメソッドへのショートカット
    func id(index: Int) -> Index {
        return self.index(self.startIndex, offsetBy: index)
    }
    
    // 整数のインデックスで文字を返す
    subscript(index: Int) -> String {
        return String(self[id(index: index)])
    }
}
var string = "hello world"
string[0] // "h"と表示

こうなります。id(index:)というメソッドが、Stringから指定したインデックスを取り出すメソッドで、それを使ってCharacter1文字をゲットしています。取り出した文字はCharacter型なので、Stringに型変換しています。

範囲演算子を使うと、文字列の中の一部分を切り出して取り出すことが出来ます。

extension String {    
    // 範囲演算子で文字列を抜き出す
    subscript(indices: CountableRange<Int>) -> String {
        let range = id(index: indices.lowerBound)..<id(index: indices.upperBound)

        // substringメソッド
        return self.substring(with: range)
        
        // またはsubscript
//        return self[range]
    }
}

var string = "hello world"
string[1..<3] // "el"と表示

String型はsubstring(range:)というメソッドを持っていますので、それを使ってサブスクリプトでStringを切り出す機能を作っています。そうすると、string[1..<3]という感じで、サブスクリプトを使って部分的に文字列を抜き出すことが可能になります。

substringの引数はRangeという型の範囲演算子で、これはhalf-open range operator(..<)に対応。一方で、closed range operatorはClosedRangeという型に割り当てられています(Swift 3から)。ですので、上記のサンプルコードを単純にclosed range operatorに置き換えるとコンパイルエラーになります。

また、Stringが持つ元々持っているsubscriptの引数の1つとして、範囲演算子の中身がIndexの型(Range<String.Index>)を取れるので、上記サンプルコードのようにsubscriptを使って部分文字列を取り出すことも可能です。

入れ子にした型 | Nested Types

入れ子にした型(nested types)をextension内部に作ることが可能です。

// Nested types
extension Character {
    enum Japanese {
        case hiragana
        case katakana
        case others
    }
    
    var type: Japanese {
        switch self {
        case "あ"..."ん":
            return .hiragana
        case "ア"..."ン":
            return .katakana
        default:
            return .others
        }
    }
}

func countNumberOfJapaneseCharacters(_ string: String) {
    var numberOfHiragana = 0
    var numberOfKatakana = 0
    for character in string.characters {
        switch character.type {
        case .hiragana:
            numberOfHiragana += 1
        case .katakana:
            numberOfKatakana += 1
        default:
            continue
        }
    }
    
    if numberOfHiragana == 0 && numberOfKatakana == 0 { return }
    
    print(string, terminator:" ")
    print("| 平仮名を\(numberOfHiragana)文字、カタカナを\(numberOfKatakana)文字含んでいます")
}

countNumberOfJapaneseCharacters("Swiftのエクステンションで機能を拡張する")
//"Swiftのエクステンションで機能を拡張する | 平仮名を5文字、カタカナを8文字含んでいます"と表示

この例では、文字列に含まれる平仮名とカタカナを、列挙型を使って場合分け出来る機能を追加し、実際にそれを使って少し遊んでみました。平仮名とカタカナの条件分岐には範囲演算子を使っています。これはswitch文の説明の時に使ったものです。また、関数countNumberOfJapaneseCharacters(_:)の中では、入れ子にした列挙型Character.Japaneseの型名を付けずに.hiraganaのような形で使うことが可能です。

まとめ

  • Extensionは既存の独自型(クラス、構造体、列挙型)の機能拡張が可能
  • Extensionで追加できるのは、計算プロパティ、メソッド、初期化子、サブスクリプト、入れ子にした型
  • プロトコルの拡張も可能
  • Extensionの定義構文にはextensionキーワードを使う