Swiftの型プロパティ(Type Properties)

SwiftのType Properties

型プロパティ(Type Properties)です。これは、型自体に付随する特殊なプロパティです。このプロパティは実体が1つだけですので、ある特定の型の中で共通の値を持ちたい場合に便利です。C言語で言うと、静的変数(static variable)などに該当します。

ここでは、型プロパティとは何か?から始めて、型プロパティの定義構文、型プロパティをどうやって使うか、に関して説明していきます。

型プロパティ(Type Properties)って何?

インスタンスプロパティと型プロパティの違い

これまで紹介した格納プロパティ(stored properties)計算プロパティ(computed properties)は、インスタンスプロパティ(instance properties)と呼ばれています。これはその名前の通り、

(ある特定の型を持った)インスタンスが保有するプロパティ

です。インスタンスプロパティの値は、インスタンスに付随しますので、(基本的には)新しいインスタンスが生成される度に複製されます。したがって、異なるインスタンスが2つあった場合、それぞれ別々のインスタンスプロパティが存在し得ます。

それに対して、型プロパティ(Type Properties)という、ちょっと特殊なプロパティがあります。

型プロパティは、インスタンスではなくて、型自身に付随するプロパティ

になります。この型プロパティは、何個インスタンスが生成されようが、実体は常に一つしか存在しないようなプロパティです。

Stored Type PropertiesとComputed Type Properties

型プロパティも、インスタンスプロパティと同様に、型に付随する格納プロパティ(stored type properties)型に付随する計算プロパティ(computed type properties)があります。Stored type propertiesは定数・変数の両方となり得ますが、computed type propertiesは変数として宣言するしかありません。

公式マニュアルに注意書きがありますが、stored type propertiesは常に初期値を与えないといけません。これは、型自体には初期化機能がないからです。また、stored type propertiesは最初にアクセスするまでは初期化されません(lazyプロパティと同じ)。Stored type propertiesは、マルチスレッドによる同時アクセスにおいても、一度しか初期化されないということが保証されています。Lazyプロパティのように初期化されますが、明示的にlazy指定する必要はないようです。

“NOTE

Unlike stored instance properties, you must always give stored type properties a default value. This is because the type itself does not have an initializer that can assign a value to a stored type property at initialization time.

Stored type properties are lazily initialized on their first access. They are guaranteed to be initialized only once, even when accessed by multiple threads simultaneously, and they do not need to be marked with the lazy modifier.”

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

型プロパティ定義構文(Type Property Syntax)

staticキーワードで型プロパティを定義

型プロパティの定義構文では、staticキーワードをプロパティ宣言の直前に付けます。

// 型プロパティの定義構文
struct SomeStructure {
    static var storedTypeVariable = "some variable"
    static let storedTypeConstant = "some constant"
    static var computedTypeProperty: String {
        return "some computed property"
    }
}

print(SomeStructure.storedTypeVariable)
//"some variable"と表示

// 変数なので変更可能
SomeStructure.storedTypeVariable = "stored type properties"
print(SomeStructure.storedTypeVariable)
//"stored type properties"と表示

// 定数は変更できない
SomeStructure.storedTypeConstant = "constant stored type properties"
//error: cannot assign to property: 'storedTypeConstant' is a 'let' constant

上記の例では、構造体SomeStructureという型が持つ型プロパティを、変数、定数、計算プロパティで作ってみました。

上記の例と同様に、列挙型やクラスでも型プロパティを作成することが可能です。型プロパティは、その名前の通り型が保有するプロパティです。したがって、type.propertyのように、型自体にdot syntaxを使ってプロパティにアクセスします。型プロパティへのアクセスは、後で詳しく説明します。

classキーワードでcomputed type propertiesを定義

クラスのcomputed type propertiesだけは特殊で、classキーワードを指定することで、継承先のサブクラス(subclass)でスーパークラス(superclass)の仕様を上書き(override)することが可能です。

// classキーワードでcomputed type properties
class SuperClass {
    static var computedTypeProperty: String {
        return "super class"
    }
    class var overrideableComputedTypeProperty: String {
        return "super class"
    }
}

class SubClass : SuperClass {
    override class var overrideableComputedTypeProperty: String {
        return "sub class"
    }
}
print(SubClass.computedTypeProperty)
//"super class"と表示

print(SubClass.overrideableComputedTypeProperty)
//"sub class"と表示

スーパークラスであるSuperClassは2つのcomputed type propertiesを持っています。1つはstaticで宣言されていますが、もう1つはclassキーワードで宣言されています。この場合、派生先のSubClassでは、基本的にはスーパークラスの仕様が引き継がれます。しかしながら、class指定した計算プロパティは、派生先で中身を置き換えることができます。ちなみに、通常の型プロパティ指定staticでは、オーバーライドすることができません。オーバーライドしようとするとコンパイルエラーになります。

型プロパティの取り出し・値渡し

Dot syntaxによる型プロパティへのアクセス

インスタンスプロパティでは、インスタンスの名前にdot syntaxを使ってプロパティにアクセスしました。一方で、(先程少し紹介しましたが)型プロパティへのアクセスには

// 型プロパティへのアクセス
type.property // 値を取り出す
type.property = value // 値を代入

という具合に、直接型の名前にdot syntaxを使います。繰り返しになりますが、型プロパティは型自体が保有するプロパティですから、このような構文になります。

型プロパティの具体的な使用例 | オーディオチャンネル構造体

公式マニュアルにある、オーディオチャンネルの実例が分かりやすいので、流用して説明したいと思います。分かりやすいとは言え、少し複雑ですので、1つずつ詳しく説明していきます。公式マニュアルにあるオーディオの絵はすごく分かりやすいですね。

// オーディオチャンネル構造体
struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // 上限thresholdLevelを超えたので、現在の値を上限で押さえる
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // 現在の保存している最大値を超えたので、新しい最大値をセット
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }

            // 現在の値と、最大値を表示
            print("現在の音量:\(currentLevel)、オーディオレベルの最大値:\(AudioChannel.maxInputLevelForAllChannels)")
        }
    }
}

var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
leftChannel.currentLevel = 7
//"現在の音量:7、オーディオレベルの最大値:7"と表示

rightChannel.currentLevel = 3
//"現在の音量:3、オーディオレベルの最大値:7"と表示

rightChannel.currentLevel = 11
//"現在の音量:10、オーディオレベルの最大値:10"と表示

構造体AudioChannelは、音量計をモデル化したものです。

オーディオチャンネル構造体の骨組み

この構造体は、型プロパティを2つ、インスタンスプロパティを1つ保有しています。骨格だけを抜き出すと、

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        ....
    }
}

となります。定数の型プロパティthresholdLevelは、音量の最大値を規定しています。また、変数の型プロパティmaxInputLevelForAllChannelsは、現在入力されている音量の最大値を保有しているプロパティです。これらは、構造体AudioChannel共通の特性になりますから、型プロパティで定義されています。

補足:thresholdとcurrent

ちなみに、thresholdというのは「閾値(しきいち)」という日本語が良く割り当てられます。余り馴染みがありませんが、境界となる値のことを指す場合に使うことが多いです。読み方は「スレッシュホールド」でしょうか。また、current(カレント)というのは「現在の」とか「最新の」という意味の形容詞で、割りとよく使います。名詞の場合「電流」や「流れ」の意味でも使いますので、物理や工学系では良く聞く英語だと思います。

プロパティ監視(property observer)のdidSetでプロパティの変更に対応

AudioChannelの主要なプロパティは、インスタンスプロパティのcurrentLevelです。ここでは、プロパティ監視のdidSetを使って、しきい値や最大値を超えた場合の制御をしています。

// インスタンスプロパティcurrentLevel
var currentLevel: Int = 0 {
    didSet {
        ....
        // 現在の値と、最大値を表示
        print("現在の音量:\(currentLevel)、オーディオレベルの最大値:\(AudioChannel.maxInputLevelForAllChannels)")
    }
}

didSetですから、プロパティに値渡しされた後の処理を記述しています。最後のprint(_:)関数は、私が追加したもので、currentLevelmaxInputLevelForAllChannelsを表示しています。ここで1つ注意ですが、型プロパティへのアクセスでは、常に型の名前を付けなければいけません。構造体(クラスなど)内部でも同様で、プロパティ名だけのアクセスだとコンパイルエラーになります。

currentLevel内の条件分岐 | しきい値と最大値を使ったチェック

では、didSet内部の条件分岐を詳しく見ていきます。

// しきい値・最大値を使った、現在音量のチェック
if currentLevel > AudioChannel.thresholdLevel {
    // 上限thresholdLevelを超えたので、現在の値を上限で押さえる
    currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
    // 現在の保存している最大値を超えたので、新しい最大値をセット
    AudioChannel.maxInputLevelForAllChannels = currentLevel
}

最初のif文では、現在の音量currentLevelがしきい値thresholdLevelを超えたかどうかを判定しています。もし、しきい値を超えた場合は、現在の値をしきい値で押さえています。

次の条件分岐では、現在の音量が、AudioChannelに保存されている最大値を超えたかどうかをチェックしています。もし最大値を超えた場合、現在の最大値を更新しています。今、上限をしきい値(10)で押さえているので、maxInputLevelForAllChannelsthresholdを超えることはありません。

今回の例を簡単に図示すると、以下のようになります。
[図解]AudioChannelの型プロパティ

今、インスタンスleftChannelrightChannelを生成しましたので、それぞれのインスタンスが別々のcurrentLevelを持っています。一方で、型プロパティは、AudioChannel構造体という「型」に付随している共通のプロパティです。

AudioChannelの具体的な使用例

最後に、AudioChannelインスタンスの出力を見てみます。

var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
leftChannel.currentLevel = 7
//"現在の音量:7、オーディオレベルの最大値:7"と表示

rightChannel.currentLevel = 3
//"現在の音量:3、オーディオレベルの最大値:7"と表示

rightChannel.currentLevel = 11
//"現在の音量:10、オーディオレベルの最大値:10"と表示

先ず、インスタンスを生成して、leftChannelの音量を7に設定します。そうすると、音量の最大値は7になります。次に、rightChannelの音量を3にセットします。今、AudioChannelの音量最大値は7ですから、最大値に変更はありません(7のまま)。最後に、rightChannelの音量を11にセットしました。AudioChannelの音量しきい値は10ですから、rightChannelの音量は10となり、最大値も10です。

少し複雑でしたが、playgroundで色々試してみたり、条件分岐内にprint(_:)を挟むなどすると、型プロパティの動きが理解できると思います。

まとめ

  • 型プロパティ(Type Properties)は、型(構造体、列挙型、クラス)が保有するプロパティ。
  • 型プロパティにはstaticキーワードを使う
  • 型の格納プロパティ(stored type properties)には、必ず初期値を与える
  • 型の計算プロパティ(computed type properties)には、classキーワードも使える。この場合、継承先のサブクラスで、プロパティのオーバーライドが可能
  • 型プロパティでは、型の名前にdot syntaxを使ってプロパティにアクセス