見出し画像

Swiftでプログラミング。-Properties 2

Property Wrappers

Property Wrapperは、プロパティの保存方法を管理するコードと、プロパティを定義するコードとを分けて考えます。例えば、スレッド・セーフティ・チェックを行うプロパティや、基礎となるデータをデータベースに保存するプロパティがある場合、そのつどコードをすべてのプロパティに記述する必要があります。プロパティ・ラッパーを使用すると、ラッパーの定義時に管理コードを一度だけ記述し、その管理コードを複数のプロパティに適用して再利用することができます。

Property Wrapperを定義するには、wrappedValueプロパティを定義する構造体、列挙体、またはクラスを作ります。以下のコードでは,構造体TwelveOrLessは,ラップされる値が常に12以下の数値を含むようにしています。より大きな数値を格納するように指示すると、代わりに12を格納します。

   @propertyWrapper
   struct TwelveOrLess {
       private var number = 0
       var wrappedValue: Int {
           get { return number }
           set { number = min(newValue, 12) }
       }
   }

setterは新しい値が12より小さいことを保証し、getterは保存された値を返します。

上の例のnumberの宣言では、変数をprivateとしてマークしており、これによりnumberはTwelveOrLessの実装でのみ使用されます。それ以外の場所で書かれたコードは、wrappedValueのゲッターとセッターを使用して値にアクセスし、numberを直接使用することはできません。

Property Wrapperを適用するには,ラッパーの名前をプロパティの前に属性として記述します。ここでは,Property WrapperとしてTwelveOrLessを使用して,寸法が常に12以下であることを保証する長方形を格納する構造体を示します。

   struct SmallRectangle {
       @TwelveOrLess var height: Int
       @TwelveOrLess var width: Int
   }
   
   var rectangle = SmallRectangle()
   print(rectangle.height)
   // Prints "0"
   rectangle.height = 10
   print(rectangle.height)
   // Prints "10"
   rectangle.height = 24
   print(rectangle.height)
   // Prints "12"

heightプロパティとwidthプロパティの初期値は、TwelveOrLessの定義から得られます。TwelveOrLess.numberは0に設定されます。TwelveOrLessのsetterは10を有効な値として扱うので、rectangle.heightに10という数字を格納することは、書かれている通りに進みます。しかし、24はTwelveOrLessが許容する値よりも大きいため、24を格納しようとすると、rectangle.heightが許容される最大値である12に設定されてしまいます。

Property Wrapperを適用すると、コンパイラーはラッパーの格納場所を提供するコードと、ラッパーを介してプロパティにアクセスするコードを合成します(Property Wrapperはラップされた値を格納する責任があるので、そのためのコードは合成されません)。特別な属性構文を利用しなくても、Property Wrapperの動作を利用するコードを書くことができます。例えば、前のコードリストにあるSmallRectangleのバージョンは、@TwelveOrLessを属性として書く代わりに、プロパティを明示的に構造体TwelveOrLessでラップしています。

    struct SmallRectangle {
       private var _height = TwelveOrLess()
       private var _width = TwelveOrLess()
       var height: Int {
           get { return _height.wrappedValue }
           set { _height.wrappedValue = newValue }
       }
       var width: Int {
           get { return _width.wrappedValue }
           set { _width.wrappedValue = newValue }
       }
   }

_heightと_widthのプロパティには、Property WrapperであるTwelveOrLessのインスタンスが格納されています。height」と「width」のゲッターとセッターは、「wrappedValue」プロパティへのアクセスを行います。

Setting Initial Values for Wrapped Properties

上の例のコードでは、TwelveOrLessの定義でnumberに初期値を与えることで、ラップされたプロパティに初期値を設定しています。このTwelveOrLess でProperty Wrapperを使用するプロパティに違う新しい初期値を指定することはできません。初期値を設定するにはProperty Wrapperにイニシャライザを追加する必要があります。ここでは、SmallNumberというTwelveOrLessの拡張バージョンで、ラップされた値と最大値を設定するイニシャライザを定義しています。

   @propertyWrapper
   struct SmallNumber {
       private var maximum: Int
       private var number: Int
       var wrappedValue: Int {
           get { return number }
           set { number = min(newValue, maximum) }
       }
       init() {
           maximum = 12
           number = 0
       }
       init(wrappedValue: Int) {
           maximum = 12
           number = min(wrappedValue, maximum)
       }
       init(wrappedValue: Int, maximum: Int) {
           self.maximum = maximum
           number = min(wrappedValue, maximum)
       }
   }

SmallNumber の定義には、init()、init(wrappedValue:)、および init(wrappedValue:maximum:) の 3 つの初期化子が含まれています。これらの初期化子は、以下の例でラップされた値と最大値を設定するために使用されます。 

Property Wrapperを適用し、初期値を指定しない場合、Swift は init() 初期化子を使用してラッパーを設定します。 例えば:

   struct ZeroRectangle {
       @SmallNumber var height: Int
       @SmallNumber var width: Int
   }
   var zeroRectangle = ZeroRectangle()
   print(zeroRectangle.height, zeroRectangle.width)
   // Prints "0 0"’

高さと幅をラップする SmallNumber のインスタンスは、SmallNumber() を呼び出すことによって作成されます。 そのイニシャライザ内のコードは、ゼロと 12 のデフォルト値を使用して、ラップされた初期値と最大値の初期値を設定します。Property Wrapperは、SmallRectangle で TwelveOrLess を使用した以前の例のように、引き続きすべての初期値を提供します。 その例とは異なり、SmallNumber は、プロパティの宣言の一部としてこれらの初期値を書き込むこともサポートしています。

プロパティの初期値を指定すると、Swift は init(wrappedValue:) 初期化子を使用してラッパーを設定します。 例えば:

    struct UnitRectangle {
       @SmallNumber var height: Int = 1
       @SmallNumber var width: Int = 1
   }
   var unitRectangle = UnitRectangle()
   print(unitRectangle.height, unitRectangle.width)
   // Prints "1 1"

ラッパーを使用してプロパティに = 1 を書き込むと、それは init(wrappedValue:) 初期化子の呼び出しに変換されます。 高さと幅をラップする SmallNumber のインスタンスは、SmallNumber(wrappedValue: 1) を呼び出すことによって作成されます。 イニシャライザは、ここで指定されたラップされた値を使用し、デフォルトの最大値である 12 を使用します。

カスタム属性の後に括弧内に引数を記述すると、Swift はそれらの引数を受け入れる初期化子を使用してラッパーを設定します。 たとえば、初期値と最大値を指定すると、Swift は init(wrappedValue:maximum:) 初期化子を使用します。

   struct NarrowRectangle {
       @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
       @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
   }
   var narrowRectangle = NarrowRectangle()
   print(narrowRectangle.height, narrowRectangle.width)
   // Prints "2 3"
   narrowRectangle.height = 100
   narrowRectangle.width = 100
   print(narrowRectangle.height, narrowRectangle.width)
   // Prints "5 4"

高さをラップする SmallNumber のインスタンスは SmallNumber(wrappedValue: 2、最大: 5) を呼び出すことで作成され、幅をラップするインスタンスは SmallNumber(wrappedValue: 3、最大: 4) を呼び出すことで作成されます。

Property Wrapperに引数を含めることで、ラッパーの初期状態を設定したり、ラッパーの作成時に他のオプションをラッパーに渡すことができます。 この構文は、Property Wrapperを使用する最も一般的な方法です。 必要な引数を属性に指定でき、それらは初期化子に渡されます。

Property Wrapper引数を含める場合、割り当てを使用して初期値を指定することもできます。 Swift は、割り当てを WrappedValue 引数のように扱い、含まれる引数を受け入れる初期化子を使用します。 例えば:

    struct MixedRectangle {
       @SmallNumber var height: Int = 1
       @SmallNumber(maximum: 9) var width: Int = 2
   }
   var mixedRectangle = MixedRectangle()
   print(mixedRectangle.height)
   // Prints "1"
   mixedRectangle.height = 20
   print(mixedRectangle.height)
   // Prints "12"

高さをラップする SmallNumber のインスタンスは、デフォルトの最大値 12 を使用する SmallNumber(wrappedValue: 1) を呼び出すことによって作成されます。幅をラップするインスタンスは、SmallNumber(wrappedValue: 2、最大: 9) を呼び出すことによって作成されます。

Projecting a Value From a Property Wrapper

Property Wrapperは、ラップされた値に加えて、ProjectedValueを定義することで追加機能を公開できます。たとえば、データベースへのアクセスを管理するProperty Wrapperは、そのProjectedValueを使い flushDatabaseConnection() メソッドを公開できます。 予測された値の名前は、ドル記号 ($) で始まることを除いて、ラップされた値と同じです。 コードでは $ で始まるプロパティを定義できないため、ProjectedValueが定義したプロパティに干渉することはありません。

上記の SmallNumber の例では、プロパティに大きすぎる数値を設定しようとすると、プロパティ ラッパーは数値を格納する前に調整します。 以下のコードは、ProjectedValue プロパティを SmallNumber 構造に追加して、プロパティラッパーが新しい値を格納する前にプロパティの新しい値を調整したかどうかを追跡します。

   @propertyWrapper
   struct SmallNumber {
       private var number = 0
       var projectedValue = false
       var wrappedValue: Int {
           get { return number }
           set {
               if newValue > 12 {
                   number = 12
                   projectedValue = true
               } else {
                   number = newValue
                   projectedValue = false
               }
           }
       }
   }
   
   struct SomeStructure {
       @SmallNumber var someNumber: Int
   }
   var someStructure = SomeStructure()
   someStructure.someNumber = 4
   print(someStructure.$someNumber)
   // Prints "false"
   someStructure.someNumber = 55
   print(someStructure.$someNumber)
   // Prints "true"

someStructure.$someNumberを書くと、ラッパーのProjectedValueにアクセスします。4のような小さな数字を格納すると、someStructure.$someNumberの値は偽になります。しかし、55のような大きすぎる数字を格納しようとすると、ProjectedValueはtrueになります。

Property Wrapperは、任意の型の値をそのProjectedValueとして返すことができます。この例では、Property Wrapperが公開している情報は、数字が調整されているかどうかという1つの情報だけなので、そのブール値をProjectedValueとして公開しています。より多くの情報を公開する必要があるラッパーは、他のデータ型のインスタンスを返したり、selfを返してラッパーのインスタンスをそのProjectedValueとして公開したりすることができます。

プロパティ・ゲッターやインスタンス・メソッドなど、データ型の一部であるコードからProjectedValueにアクセスする場合は、他のプロパティにアクセスする場合と同様に、プロパティ名の前に self.を省略することができます。次の例のコードでは、height と width を囲むラッパーのProjectedValueを $height と $width としています。

    enum Size {
       case small, large
   }
   struct SizedRectangle {
       @SmallNumber var height: Int
       @SmallNumber var width: Int
       mutating func resize(to size: Size) -> Bool {
           switch size {
           case .small:
               height = 10
               width = 20
           case .large:
               height = 100
               width = 100
           }
           return $height || $width
       }
   }

Property Wrapperの構文は、getterとsetterを持つプロパティの単なる構文記号なので、height と width へのアクセスは、他のプロパティへのアクセスと同じように動作します。たとえば、resize(to:)のコードでは、プロパティのラッパーを使って height と width にアクセスしています。resize(to: .large)を呼び出した場合、.largeのスイッチケースでは、矩形の高さと幅が100に設定されます。ラッパーは、これらのプロパティの値が12より大きくなることを防ぎ、値を調整した事実を記録するために、ProjectedValueをtrueに設定します。resize(to:)の最後の return 文では、$height と $width をチェックして、プロパティのラッパーが高さと幅のどちらを調整したかを判断します。

Global and Local Variables Global and Local 変数

上述したプロパティの計算と観察の機能は、グローバル変数とローカル変数にも利用できます。グローバル変数とは、関数、メソッド、クロージャ、または型のコンテキストの外で定義される変数です。ローカル変数は、関数、メソッド、クロージャのコンテキスト内で定義される変数です。

これまでの章で出てきたグローバル変数やローカル変数は、すべて保存のための変数でした。保存のための変数は、保存プロパティと同様に、特定の型の値を格納し、その値を設定したり取得したりすることができます。

しかし、グローバルスコープでもローカルスコープでも、計算変数を定義したり、ストアド変数のオブザーバーを定義することもできます。計算された変数は、値を保存するのではなく、計算して値を算出します。

グローバル定数および変数は、 Lazy Stored Propertieと同様に、常に遅延され計算されます。 Lazy Stored Propertieと違うところは、グローバル定数と変数にはlazy修飾子を付ける必要はありません。
ローカル定数や変数は遅延計算されません。

Property Wrapperは、ローカル変数には適用できますが、グローバル変数や計算済み変数には適用できません。例えば、以下のコードでは、myNumberがSmallNumberをProperty Wrapperとして使用しています。

    func someFunction() {
       @SmallNumber var myNumber: Int = 0
       myNumber = 10
       // now myNumber is 10
       myNumber = 24
       // now myNumber is 12
   }

SmallNumberをプロパティに適用した場合と同様に、myNumberの値を10に設定することは有効です。Property Wrapperは12以上の値を許可しないため、myNumberの値を24ではなく12に設定します。

Type Properties

インスタンス・プロパティは、特定の型のインスタンスに属するプロパティです。その型の新しいインスタンスを作成するたびに、他のインスタンスとは別に、独自のプロパティ値のセットを持つことになります。

また、特定の型のインスタンスではなく、その型自体に属するプロパティを定義することもできます。その型のインスタンスをいくつ作っても、これらのプロパティのコピーは1つしかありません。このような種類のプロパティを型プロパティと呼びます。

型プロパティは、特定の型のすべてのインスタンスに共通する値を定義するのに便利です。たとえば、すべてのインスタンスが使用できる定数プロパティ(C言語のstatic constantのようなもの)や、その型のすべてのインスタンスに共通する値を格納する変数プロパティ(C言語のstatic variableのようなもの)があります。

格納された型のプロパティは、変数でも定数でもよい。計算された型のプロパティは、計算されたインスタンスのプロパティと同じように、常に変数のプロパティとして宣言されます。

保存・インスタンス・プロパティとは異なり、保存タイプ・プロパティには必ずデフォルト値を与える必要があります。これは、型自体が初期化時に保存型プロパティに値を割り当てることができるイニシャライザを持っていないためです。

保存型のプロパティは、最初のアクセス時にレイジーに初期化されます。複数のスレッドから同時にアクセスされた場合でも、一度だけ初期化されることが保証されており、lazy修飾子を付ける必要はありません。

Type Property Syntax

CやObjective-Cでは、型に関連する静的な定数や変数をグローバルな静的変数として定義します。しかし、Swiftでは、型のプロパティは、型の外側の中括弧の中で、型の定義の一部として記述され、各型のプロパティは、サポートする型に明示的にスコープされます。

型プロパティの定義には static キーワードを使用します。クラス型の計算された型プロパティには、代わりに class キーワードを使用して、サブクラスがスーパークラスの実装をオーバーライドできるようにすることができます。以下の例では、保存型と計算型のプロパティの構文を示しています。

    struct SomeStructure {
       static var storedTypeProperty = "Some value."
       static var computedTypeProperty: Int {
           return 1
       }
   }
   enum SomeEnumeration {
       static var storedTypeProperty = "Some value."
       static var computedTypeProperty: Int {
           return 6
       }
   }
   class SomeClass {
       static var storedTypeProperty = "Some value."
       static var computedTypeProperty: Int {
           return 27
       }
       class var overrideableComputedTypeProperty: Int {
           return 107
       }
   }
上記の計算型プロパティの例は、読み取り専用の計算型プロパティの場合ですが、計算インスタンスプロパティと同じ構文で、読み取り・書き込み兼用の計算型プロパティを定義することもできます。

Querying and Setting Type Properties

タイプ・プロパティは、インスタンス・プロパティと同様に、ドット"."でつなげて問い合わせや設定を行います。ただし、型のプロパティは、その型のインスタンスではなく、その型に対して照会・設定されます。例えば、以下のようになります。

   print(SomeStructure.storedTypeProperty)
   // Prints "Some value."
   SomeStructure.storedTypeProperty = "Another value."
   print(SomeStructure.storedTypeProperty)
   // Prints "Another value."
   print(SomeEnumeration.computedTypeProperty)
   // Prints "6"
   print(SomeClass.computedTypeProperty)
   // Prints "27"

以下の例では、2つの保存プロパティを、複数のオーディオチャンネルのオーディオレベルメーターをモデル化する構造体の一部として使用しています。各チャンネルには、0から10までの整数のオーディオレベルが設定されています。

2つのオーディオチャンネルを組み合わせて、ステレオのオーディオレベルメーターをモデル化したものを例に上げます。あるチャンネルのオーディオレベルが0のとき、そのチャンネルのライトはどれも点灯しません。オーディオレベルが10になると、そのチャンネルのすべてのランプが点灯します。この図では、左チャンネルの電流レベルが9、右チャンネルの電流レベルが7となっています。

上述のオーディオチャンネルは、AudioChannel構造体のインスタンスで表現されます。

    struct AudioChannel {
       static let thresholdLevel = 10
       static var maxInputLevelForAllChannels = 0
       var currentLevel: Int = 0 {
           didSet {
               if currentLevel > AudioChannel.thresholdLevel {
                   // cap the new audio level to the threshold level
                   currentLevel = AudioChannel.thresholdLevel
               }
               if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                   // store this as the new overall maximum input level
                   AudioChannel.maxInputLevelForAllChannels = currentLevel
               }
           }
       }
   }

AudioChannel構造体は、その機能をサポートするために2つの保存型プロパティを定義しています。1つ目の thresholdLevel は、オーディオレベルが取ることのできる最大のしきい値を定義しています。この値は、すべてのAudioChannelインスタンスで10という一定の値です。もしオーディオ信号が10よりも高い値で入ってきた場合、この閾値にキャップされます(後述)。

2つ目のタイプのプロパティは、「maxInputLevelForAllChannels」という変数格納型のプロパティです。任意のAudioChannelインスタンスが受信した最大入力値を記録します。初期値は0から始まります。

また、AudioChannel構造体にはcurrentLevelというインスタンスプロパティが定義されており、これはチャンネルの現在のオーディオレベルを0から10のスケールで表しています。

currentLevelプロパティには、didSetプロパティオブザーバーがあり、currentLevelが設定されるたびにその値をチェックします。このオブザーバーは2つのチェックを行います。

currentLevelの新しい値が、許可されたthresholdLevelよりも大きい場合、プロパティオブザーバはcurrentLevelをthresholdLevelにキャップする。
キャッピング後のcurrentLevelの新しい値が、それまでにAudioChannelインスタンスが受信した値よりも大きい場合、プロパティオブザーバーは新しいcurrentLevelの値を maxInputLevelForAllChannelsタイプのプロパティに格納する。

最初のチェックでは、didSetオブザーバーはcurrentLevelを別の値に設定します。しかし、これでオブザーバーが再度呼び出されることはありません。

AudioChannel構造体を使って、leftChannelとrightChannelという2つの新しいオーディオチャンネルを作り、ステレオサウンドシステムのオーディオレベルを表現することができます。

   var leftChannel = AudioChannel()
   var rightChannel = AudioChannel()

左チャンネルの currentLevel を 7 に設定すると、 maxInputLevelForAllChannels タイププロパティが 7 に更新されていることがわかります。

   leftChannel.currentLevel = 7
   print(leftChannel.currentLevel)
   // Prints "7"
   print(AudioChannel.maxInputLevelForAllChannels)
   // Prints "7"

右チャンネルのcurrentLevelを11に設定しようとすると、右チャンネルのcurrentLevelプロパティが最大値の10にキャップされ、maxInputLevelForAllChannelsタイプのプロパティが10に更新されていることがわかります。

   rightChannel.currentLevel = 11
   print(rightChannel.currentLevel)
   // Prints "10"
   print(AudioChannel.maxInputLevelForAllChannels)
   // Prints "10"


この記事が気に入ったらサポートをしてみませんか?