Swiftのインスタンスメソッド(Instance Methods)

SwiftのInstance Methods

インスタンスメソッド(Instance Methods)です。その名前の通り、ある型(クラス、構造体、列挙型)のインスタンスが保有するメソッドです。メソッドの機能は、基本的には関数と同じです。

ここでは、インスタンスメソッドとは何か?から始めて、その具体例、インスタンスメソッドのローカル・外部パラメータ名、さらにselfプロパティ、mutating methodsを使ったメソッド内部におけるプロパティの変更、について詳しく説明していきます。

インスタンスメソッド(Instance Methods)って何?

インスタンスメソッドは、特定の型のインスタンスが持つ関数

インスタンスというのは、ある独自型(例えばクラス)の実体のことです。ですから、インスタンスメソッド(Instance Methods)とは、

ある特定のクラスや構造体、または列挙型インスタンスが保有する関数

と言えます。

したがって、その構文は関数と全く同じです。インスタンスメソッドが提供するのは、

といった機能性(functionality)です。

インスタンスメソッドの具体例

早速インスタンスメソッドの具体例を見てみます。ここでは、与えられた分布(色々な数字)の平均値を計算するメソッドを作ってみます。基本的な構造は、計算プロパティ(computed properties)のページで使用した平均値を計算する関数と同じです。

// 平均値を計算するメソッド
class Distribution {
    var numbers = [Double]()

    func mean() -> Double {
        guard !numbers.isEmpty else {
            return 0.0
        }

        var sum = 0.0
        for number in numbers {
            sum += number
        }

        return sum / Double(numbers.count)
    }

    func reset() {
        numbers = []
    }
}

// デフォルトでは配列は空
let distribution = Distribution()
print(distribution.mean())
//結果:0.0

distribution.numbers = [1.0,2.0,3.0,4.0,5.0]
print(distribution.mean())
//結果:3.0

// 配列をリセット
distribution.reset()
print(distribution.mean())
//結果:0.0

クラスのclass Distributionは、プロパティとして配列(arrays)numbersを持っています(初期値は空です)。配列が空の場合は、平均値を計算できないので、制御転送guard0.0を返す仕様になっています。

適当に配列をセットして(ここでは[1.0,2.0,3.0,4.0,5.0])平均値を計算していますが、ちゃんと平均値3.0を返しているのが分かります。また、メソッドreset()は配列を空にするメソッドです。したがって、reset()を実行した後は平均値が0.0に戻ります。

メソッド呼び出しには、(プロパティと同様に)dot syntaxを使います。

let distribution = Distribution() // クラスインスタンスの生成
print(distribution.mean()) // インスタンスメソッドmean()の呼び出し

メソッドのラベル(Argument Label)、内部パラメータ名(Parameter Name)

しつこいですが、メソッドは型に付随した関数という扱いです。したがって、メソッドにも

呼び出し用パラメータ名のラベル(argument labels)
メソッド定義(実装)時に仕様する内部専用パラメータ名(parameter names)

という2つのパラメータ名があります。

関数のページで、ラベルとパラメータ名に関して詳しく説明しましたが、メソッドでも全く同様です。

ラベルとパラメータ名が分かる具体例 | メソッド編

では、先程作った平均値を計算するメソッドを少し改良してみます。ラベルとパラメータ名の違いを説明するために、プロパティを1つ足します。

// 平均値を計算するメソッド(改良版)
class Distribution {
    var numbers = [Double]()
    var weights = [Double]()
    
    func add(_ number: Double, w weight: Double) {
        numbers.append(number)
        weights.append(weight)
    }
    
    func mean() -> Double {
        guard !numbers.isEmpty else {
            return 0.0
        }
        
        var sum = 0.0
        var denominator = 0.0
        for (index, number) in numbers.enumerated() {
            sum += number * weights[index]
            denominator += weights[index]
        }
        
        return denominator == 0.0 ? 0.0 : sum / denominator
    }
    
    func reset() {
        numbers = []
        weights = []
    }
}

let distribution = Distribution()
print(distribution.mean())
//0.0

distribution.add(1.0, w:1.0)
distribution.add(2.0, w:1.0)
distribution.add(3.0, w:1.0)
distribution.add(4.0, w:1.0)
distribution.add(5.0, w:1.0)

print(distribution.mean())
//3.0

distribution.reset()
print(distribution.mean())
//0.0

少し複雑なように見えるので、詳しく説明します。

分布の作成 | 数字と重みを1つずつ加える

改良版ではプロパティが1つ増えています。後で詳しく説明しますが、「加重平均」と呼ばれる平均値を計算しています。そのために必要な「重み(weights)」を、プロパティとして加えています。

class Distribution {
    var numbers = [Double]()
    var weights = [Double]()

    func add(_ number: Double, w weight: Double) {
        numbers.append(number)
        weights.append(weight)
    }
    ....

    func reset() {
        numbers = []
        weights = []
    }
}

また、メソッドadd(_:w:)では、プロパティであるnumbersweights配列に要素を加えています。

関数add(_:w:)の定義を見ると分かりますが、それぞれのパラメータにラベルが指定されています。したがって、例えば関数add(_:w:)を通じて数字の2.0を重み1.0で加える場合、

distribution.add(2.0, w:1.0)

と書きます。第一引数のnumberには、ラベルにアンダースコア_が指定されているので、上記の例のようにラベル名を省略して値を指定することが可能です。

今回は敢えて両方のパラメータ名にラベルを付けましたが、2番目のパラメータ名はweightのままの方が分かりやすいので、

    func add(_ number: Double, weight: Double) {

とするのがベストかもしれません

平均値を計算するメソッド | 加重平均

次に、平均値を計算するメソッドの中身を見ていきます。

func mean() -> Double {
    ....
    var sum = 0.0
    var denominator = 0.0
    for (index, number) in numbers.enumerated() {
        sum += number * weights[index]
        denominator += weights[index]
    }

    return denominator == 0.0 ? 0.0 : sum / denominator
}

戻り値では、三項演算子を使って、分母が0.0の場合の条件分岐を行っています。先程述べた「重み」を入れたので、平均値の計算が少し変わっています。式で見ると、下のようになってます。
重み付き平均

これは「重み付き平均」もしくは「加重平均」と呼ばれる計算になっています(参考:平均、Wikipedia)。これまで計算した単純な「算術平均」は、上記の式でw=1とした特別なケースになっています。

selfプロパティ

selfプロパティって何?

全てのインスタンスは、selfという暗黙の(implicit)プロパティを持っています。このselfプロパティは、インスタンスそのものを指します。

先程の、add(_:w:)メソッドを、selfプロパティを使って書き直してみます。

class Distribution {
    ....
    func add(_ number: Double, w weight: Double) {
        self.numbers.append(number)
        self.weights.append(weight)
    }
    ....
}

この例では明示的にselfを挿入しましたが、selfを書く必要は(実は)それほどありません。次のセクションで説明しますが、selfが必須なのはプロパティとパラメータ名が一致している場合です。

明示的にselfを書かなくても、「メソッド内部の、これとあれは、このプロパティやあのメソッドを参照しているだろう」、とSwiftは仮定してくれます。これまでの例を見て頂ければ分かると思いますが、一度もselfとは書いていないコードでも、Swiftは適切にプロパティやメソッドの判定をしています。

selfプロパティが必要なケース | プロパティ名とパラメータ名が同じ場合

selfプロパティが必須なケースがあります。インスタンスが持っているプロパティ名と、インスタンスメソッドのパラメータ名が同じ場合です。簡単な例で見てみると

// selfプロパティが必要な場合
struct Vector {
    var x = 0.0, y = 0.0
    func product(_ x: Double, _ y: Double) -> Double {
        return self.x*x + self.y*y
    }
}

var vector = Vector(x:1.0, y:1.0)
print(vector.product(2.0, 3.0))

という感じで、パラメータ名とプロパティ名(selfを付けた方)をはっきりと分ける必要があります。優先順位はパラメータ名の方が高いので、もしselfを書き忘れた場合、上記の例ではパラメータ名のx,yが採用されてしまいます。

プロパティを使う必要があるのにselfを書き忘れた場合、コンパイルエラーにはならずに、意図したものと異なる結果になるので注意が必要です。

Mutating指定されたメソッド(Mutating Methods)

メソッド内部でのプロパティの変更に関して

実は、値型である構造体や列挙型では、インスタンスメソッド内部でプロパティの変更ができません。試しに先程のVector構造体に、プロパティを変更するようなインスタンスメソッドを追加してみます。

// プロパティを変更するインスタンスメソッドを追加
import Darwin

struct Vector {
    var x = 0.0, y = 0.0
    func product(x: Double, _ y: Double) -> Double {
        return self.x*x + self.y*y
    }

    func rotate(phi: Double) {
        x = x*cos(phi) - y*sin(phi)
        y = x*sin(phi) + y*cos(phi)
    }
}
// コンパイルエラー
//error: cannot assign to property: 'self' is immutable
//note: mark method 'mutating' to make 'self' mutable

そうすると、上記のように怒られます。「なぜ変更できないのか?」という疑問がわきますが、正直理由は分かりません。というよりも「そういう仕様である」くらいの理解で良いかと思います。これは、メソッド(関数)のパラメータが、デフォルトで定数になっているのと似ています。

C言語で例えると(分からない人はすいません)、デフォルトでクラスの関数にconst指定されている状態です(データメンバーが変更不可能)。

mutatingキーワードで、プロパティを変更できるインスタンスメソッドを定義

値型が持つインスタンスメソッド内部でプロパティを変更したい場合は、メソッド定義の前にmutatingキーワードを付けます。「Mutate」というのは、「変化させる(する)」とか「変異させる(する)」という意味です。Changeだと思えば分かりやすいかもしれません。

メソッドのmutating指定は、日本語にすると

このメソッドでは、(値型である)この型が保有するプロパティを、メソッド内部から変更(mutate or change)できます

と宣言していることになります。

先程の例ですと、

// プロパティを変更するインスタンスメソッドを追加
import Darwin

struct Vector {
    ....
    mutating func rotate(phi: Double) {
        x = x*cos(phi) - y*sin(phi)
        y = x*sin(phi) + y*cos(phi)
    }
}

とすれば解決です。この場合、メソッドrotate(phi:)は、現在のインスタンスが保有するプロパティを変更します。新しいVectorインスタンスを生成しているわけではありません。また、公式マニュアルに注意書きがありますが、インスタンスを定数にした場合は、mutating methodを呼び出してプロパティを変更することはできません。

蛇足ですが、このメソッドは2次元座標空間の回転を表しています。また、self指定がありませんが、パラメータ名とプロパティ名がかぶっていませんので、問題ありません。気持ち悪かったら、明示的にself指定しても構わないと思います。

Mutating Methodsでselfに新しいインスタンスを割り当てる

先程の例ではインスタンスメソッド内部でプロパティの変更を行いました。しかしながら、selfプロパティ自体に変更を加えることも可能です。

上記の回転メソッドを少し書き換えてみます。

// selfプロパティを変更
import Darwin

struct Vector {
    ....
    mutating func rotate(phi: Double) {
        self = Vector(x: x*cos(phi) - y*sin(phi), y: x*sin(phi) + y*cos(phi))
    }
}

このように、selfを使って、新しいインスタンスを割り当てることも可能です。最終結果はどちらの場合も同じです。

列挙型でも同様に、mutating method内部でselfを使うことが可能です。公式マニュアルにある、列挙型の例(tristate switch)は非常に分かりやすいので、流用して載せます。

// 公式マニュアルにあるtristate switchの例
enum TriStateSwitch {
    case off, low, high
    mutating func next() {
        switch self {
        case .off:
            self = .low
            print("Off -> Low")
        case .low:
            self = .high
            print("Low -> High")
        case .high:
            self = .off
            print("High -> Off")
        }
    }
}

var overLight = TriStateSwitch.low
overLight.next()
overLight.next()

分かりやすいように、print(_:)関数を挟んでいます。この列挙型では、next()メソッドを呼び出す度に、現在の状態が遷移していくような形になっています。

「Tristate」というのは「トライステート」とか「トリステート」と読み、直訳では「3つの状態」でしょうか?「Tri」というのは3を表し(三角形triangleの、triと一緒です)、stateは「状態」のことです。

まとめ

  • インスタンスメソッド(Instance Methods)は、特定の型(クラスなど)のインスタンスが保有するメソッド
  • メソッドの基本的な機能は関数と同じ。例)パラメータ名(呼び出し用ラベル、内部パラメータ名)
  • selfプロパティは、インスタンス自身を指す
  • インスタンスメソッドのパラメータ名と、プロパティ名が同じ場合はself指定が必要
  • 値型(構造体と列挙型)のプロパティは、デフォルトではインスタンスメソッド内部での変更ができない
  • mutatingキーワードで、インスタンスメソッド内部でのプロパティ変更が可能。インスタンス自体selfの変更も可能