エクステンション

エクステンションとは拡張という意味ですが、読んで字の如く、エクステンションを使うと既存のクラスや構造体、列挙型に新たに機能を追加することができます。Objective-Cにもカテゴリーという、クラスを拡張する機能がありますがこれと似ています。

具体的には次のようなことができます。

  • 計算型プロパティと静的な計算型プロパティ(計算型の型プロパティ)を追加できます。
  • インスタンスメソッドと型メソッドを追加できます。
  • イニシャライザを追加できます。(追加できるのはコンビニエンスイニシャライザのみです。指定イニシャライザやデイニシャライザは追加できません。)
  • サブスクリプトを追加できます。
  • ネストした型を定義してそれを使うことができます。
  • 新たなプロトコルに適合させることができます。

エクステンションにより既存のメソッドをオーバーライドすることはできません。

エクステンションを使う場合は、次の様に型名の前にextensionキーワードをつけます。


extension SomeType {
    // 新たなメソッドの追加やプロトコルへの適応
}

エクステンションにより新たなプロトコルへ適応させる場合は、次の様に記述します。


extension SomeType: SomeProtocol, AnotherProtocol {
    // プロトコルへ適応するためのメソッド
}

エクステンションを定義すると、エクステンションで追加した機能をその型の全てのインスタンスで利用できます。これは、エクステンションの定義の前に生成されたインスタンスであっても同様です。

エクステンションを使うと、このように既存の型に計算型プロパティを追加できます。しかし、保持型プロパティを追加したり、既存のプロパティのオブザーバーを追加することはできません。

計算型プロパティの追加

Int型を拡張して、16進数、8進数、2進数の文字列を返すプロパティを実装してみます。


// Int型の拡張
extension Int {
    // 16進数
    var hex: String {
        return String(self, radix: 16)
    }
    // 8進数
    var oct: String {
        return String(self, radix: 8)
    }
    // 2進数
    var bin: String {
        return String(self, radix: 2)
    }
}

print(30.hex)     // 1e
print(10.oct)     // 12
print(6.bin)      // 110

この様に既存の型に簡単にプロパティを追加することができます。 プロパティの中で参照しているselfは、Int型のインスタンスを指しています。

イニシャライザの追加

エクステンションで追加できるイニシャライザはコンビニエンスイニシャライザのみです。指定イニシャライザは追加できません。

次のような構造体があるとします。


// パーソン
struct Person {
    let name: String        // 名前
    var email: String       // メールアドレス
    var age: Int            // 年齢
}

let taro = Person(name: "山田太郎", email: "taro@example.com", age: 35)

この構造体に、カンマ区切りのデータを渡して初期化するイニシャライザを、エクステンションを使って定義してみます。


extension Person {
    init(csv: String) {
        let data = csv.componentsSeparatedByString(",")
        var name: String? = nil
        var email: String? = nil
        var age: Int? = nil
        if data.count > 0 { name = data[0].stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: " \"")) }
        if data.count > 1 { email = data[1].stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: " \"")) }
        if data.count > 2 { age = data[2].stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: " \"")).toInt() }
        self.init(name: name, email: email, age: age)
    }
}

let csv: String = "\"山田太郎\", \"taro@example.com\", 35"

let taro = Person(csv: csv)
print("名前:\(taro.name!) メール:\(taro.email!) 年齢:\(taro.age!)")
/* 実行結果
名前:山田太郎 メール:taro@example.com 年齢:35
*/

エクステンションでイニシャライザを定義する場合もプロパティの初期化には責任を持つ必要があります。上の例では、カンマ区切りの文字列から取り出した値を引数にメンバワイズイニシャライザを呼び出してプロパティを初期化しています。

メソッドの追加

エクステンションを使って、インスタンスメソッドや型メソッドを追加することもできます。

次の例では、String型に文字列を反転するreverseメソッドを追加しています。


extension String {
    mutating func reverse() {
        var str = ""
        for ch in self {
            str = String(ch) + str
        }
        self = str
    }
}

var text = "Hello, Swift"
text.reverse()
print(text)      // tfiwS ,olleH

次の例では、Int型に、受け取った関数型の処理を繰り返すメソッドを追加しています。


extension Int {
    func times(task: () -> ()) {
        for _ in 0..<self {
            task()
        }
    }
}

3.times( { print("Hello, Swift") } )
/* 実行結果
Hello, Swift
Hello, Swift
Hello, Swift
*/

サブスクリプトの追加

既存の型にサブスクリプトを追加することもできます。

次の例では、String型にサブスクリプトを追加し、Int型のインデックスで指定した位置の文字を返すようにしています。


extension String {
    subscript(index: Int) -> Character {
        let idx = advance(self.startIndex, index)
        return self[idx]
    }
}

let text = "吾輩は猫である。"
print(text[3])    // 猫

ネストした型の追加

次の例では、String型に種類を表す列挙型をネストした型として定義して、その種類を返すメソッドを追加します。


extension String {
    enum Type {
        case URL, EMail, Telephone, Other
    }
    // 文字列の種類
    var type: Type {
        var error: NSError?
        // URL
        var regex = NSRegularExpression(pattern:"^(https?|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)$",
            options: NSRegularExpressionOptions.CaseInsensitive,
            error: &error)
        if regex.numberOfMatchesInString(self, options: nil, range: NSRange(location: 0, length: countElements(self))) > 0 {
            return .URL
        }
        // メールアドレス
        regex = NSRegularExpression(pattern:"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$",
            options: NSRegularExpressionOptions.CaseInsensitive,
            error: &error)
        if regex.numberOfMatchesInString(self, options: nil, range: NSRange(location: 0, length: countElements(self))) > 0 {
            return .EMail
        }
        // 電話番号
        regex = NSRegularExpression(pattern:"^\\d{1,4}-?\\d{4}$|\\d{2,5}-?\\d{1,4}-?\\d{4}$",
            options: NSRegularExpressionOptions.CaseInsensitive,
            error: &error)
        if regex.numberOfMatchesInString(self, options: nil, range: NSRange(location: 0, length: countElements(self))) > 0 {
            return .Telephone
        }
            
        return .Other
    }
}

let url = "http://example.com?id=test&num=123"
print(url.type == .URL)           // true

let email = "test@example.com"
print(email.type == .EMail)       // true

let phone = "03-123-9876"
print(phone.type == .Telephone)   // true

let text = "Hello, Swift!"
print(text.type == .Other)        // true

正規表現を使って文字列の書式が、URLか、メールアドレスか、電話番号か、それ以外かの判定をして、メソッド内に定義した列挙型を結果として返しています。

現時点(2014年9月)で、特にSwiftの正規表現のためのクラスや仕様は提供されていません。ここではCocoaのNSRegularExpressionを使っています。また、ここで使用しているパターンも簡易的なもので厳密なものではありません。