Swift 言語ガイド – 10. プロパティ

swift

はじめに

公式ページのSwift 言語ガイド「The Swift Programming Language Swift 5.3」に基づき記載いたします。本ページでは、「プロパティ」について記載いたします。
記載内容に誤り等ございましたら、ご連絡をいただければ幸いです。

10. プロパティ

プロパティは、値を特定のクラス、構造体、または列挙型に関連付けます。格納型(Stored)プロパティはインスタンスの一部として定数値と変数値を格納しますが、計算型(Computed)プロパティは(格納するのではなく)値を計算します。計算型プロパティは、クラス、構造体、および列挙型によって提供されます。格納型プロパティは、クラスと構造体によってのみ提供されます。

格納型および計算型プロパティは通常、特定の型のインスタンスに関連付けられています。ただし、プロパティは型自体に関連付けることもできます。このようなプロパティは、型プロパティと呼ばれます。

さらに、プロパティオブザーバーを定義して、プロパティの値の変化を監視できます。これには、カスタムアクションで応答できます。プロパティオブザーバーは、自分で定義した格納型プロパティや、サブクラスがそのスーパークラスから継承するプロパティに追加できます。

プロパティラッパーを使用して、複数のプロパティのゲッターとセッターでコードを再利用することもできます。

10.1. 格納型プロパティ

最も単純な形式では、格納型プロパティは、特定のクラスまたは構造体のインスタンスの一部として格納される定数または変数です。格納型プロパティは、可変の格納型プロパティ(varキーワードによって導入された)または定数の格納型プロパティ(letキーワードによって導入された)のいずれかです。

デフォルトのプロパティ値で説明されているように、定義の一部として格納型プロパティのデフォルト値を指定できます。初期化中に、格納型プロパティの初期値を設定および変更することもできます。これは、初期化中の定数プロパティの割り当てで説明されているように、定数に格納されたプロパティにも当てはまります。

以下の例では、FixedLengthRangeと呼ばれる構造体を定義しています。これは、作成後に範囲の長さを変更できない整数の範囲を記述します。

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 範囲は整数値0、1、および2を表します
rangeOfThreeItems.firstValue = 6
// 範囲は整数値6、7、および8を表すようになりました

FixedLengthRangeのインスタンスには、firstValueと呼ばれる可変の格納型プロパティとlengthと呼ばれる定数の格納型プロパティがあります。上記の例では、lengthは新しい範囲が作成されたときに初期化され、定数プロパティであるため、その後は変更できません。

10.2. 定数構造体インスタンスの格納型プロパティ

構造体のインスタンスを作成し、そのインスタンスを定数に割り当てる場合、変数プロパティとして宣言されていても、インスタンスのプロパティを変更することはできません。

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// この範囲は、0、1、2、および3の整数値を表します
rangeOfFourItems.firstValue = 6
// firstValueが変数プロパティであっても、これはエラーを報告します

rangeOfFourItemsは(letキーワードを使用して)定数として宣言されているため、firstValueが変数プロパティであっても、firstValueプロパティを変更することはできません。

この動作は、構造体が値型であるためです。値型のインスタンスが定数としてマークされると、そのすべてのプロパティも同様にマークされます。

同じことは、参照型であるクラスには当てはまりません。参照型のインスタンスを定数に割り当てた場合でも、そのインスタンスの変数プロパティを変更できます。

10.3. レイジー格納型プロパティ

レイジー格納型プロパティは、最初に使用されるまで初期値が計算されないプロパティです。レイジー格納型プロパティは、宣言の前にlazy修飾子を記述することで示します。

注意
インスタンスの初期化が完了するまで初期値が取得されない可能性があるため、常にレイジープロパティを変数として宣言する必要があります(varキーワードを使用)。定数プロパティは、初期化が完了する前に常に値を持っている必要があるため、lazyとして宣言することはできません。

レイジープロパティは、プロパティの初期値が、インスタンスの初期化が完了するまで値がわからない外部要因に依存している場合に役立ちます。レイジープロパティは、プロパティの初期値が複雑または計算コストの高いセットアップを必要とする場合にも役立ちます。セットアップは、必要でない限り、または必要になるまで実行しないでください。

以下の例では、レイジー格納型プロパティを使用して、複雑なクラスの不要な初期化を回避しています。この例では、DataImporterとDataManagerという2つのクラスを定義していますが、どちらも完全には示されていません。

class DataImporter {
    /*
    DataImporterは、外部ファイルからデータをインポートするためのクラスです。
    クラスの初期化には、自明でない時間がかかると想定されています。
    */
    var filename = "data.txt"
    // DataImporterクラスは、ここでデータインポート機能を提供します
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // DataManagerクラスはここでデータ管理機能を提供します
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// importerプロパティのDataImporterインスタンスはまだ作成されていません

DataManagerクラスには、dataという名前の格納型プロパティがあり、String値の新しい空の配列で初期化されます。残りの機能は示されていませんが、このDataManagerクラスの目的は、この文字列データの配列を管理および提供することです。

DataManagerクラスの機能の一部は、ファイルからデータをインポートする機能です。この機能は、DataImporterクラスによって提供されます。このクラスは、初期化に自明でない時間がかかると想定されています。これは、DataImporterインスタンスが初期化されるときに、DataImporterインスタンスがファイルを開き、その内容をメモリに読み込む必要があるためです。

DataManagerインスタンスは、ファイルからデータをインポートせずにデータを管理できるため、DataManager自体の作成時に新しいDataImporterインスタンスを作成する必要はありません。代わりに、DataImporterインスタンスを最初に使用する場合は、作成する方が理にかなっています。

lazy修飾子でマークされているため、importerプロパティのDataImporterインスタンスは、ファイル名プロパティが照会されたときなど、importerプロパティが最初にアクセスされたときにのみ作成されます。

print(manager.importer.filename)
// これで、importerプロパティのDataImporterインスタンスが作成されました
// "data.txt"を出力します

注意
lazy修飾子でマークされたプロパティが複数のスレッドによって同時にアクセスされ、プロパティがまだ初期化されていない場合、プロパティが1回だけ初期化されるという保証はありません。

10.4. 格納型プロパティとインスタンス変数

Objective-Cの経験がある場合は、クラスインスタンスの一部として値と参照を格納する2つの方法を提供することをご存知かもしれません。プロパティに加えて、プロパティに格納されている値のバッキングストアとしてインスタンス変数を使用できます。

Swiftは、これらの概念を単一のプロパティ宣言に統合します。Swiftのプロパティには、対応するインスタンス変数がなく、プロパティのバッキングストアに直接アクセスすることはありません。このアプローチは、さまざまなコンテキストで値にアクセスする方法についての混乱を回避し、プロパティの宣言を単一の明確なステートメントに単純化します。名前、型、メモリ管理特性など、プロパティに関するすべての情報は、型の定義の一部として1つの場所で定義されます。

10.5. 計算型プロパティ

格納型プロパティに加えて、クラス、構造体、および列挙型は、実際には値を保持しない計算型プロパティを定義できます。代わりに、間接的に他のプロパティと値を取得および設定するためのゲッターとオプションのセッターを提供します。

struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
                            size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// "square.origin is now at (10.0, 10.0)"を出力します

この例では、幾何学的形状を操作するための3つの構造体を定義します。

  • Pointは、Pointのx座標とy座標をカプセル化します。
  • Sizeは幅と高さをカプセル化します。
  • Rectは、原点とサイズによって長方形を定義します。

Rect構造体は、centerと呼ばれる計算型プロパティも提供します。長方形の現在の中心位置は、常にそのoriginとsizeから決定できるため、中心点を明示的なPoint値として保存する必要はありません。代わりに、Rectは、centerと呼ばれる計算型の変数のカスタムゲッターとセッターを定義して、実際に格納されているプロパティであるかのように長方形の中心を操作できるようにします。

上記の例では、squareと呼ばれる新しいRect変数を作成します。正方形変数は、原点(0, 0)、幅と高さ10で初期化されます。この正方形は、次の図の青い正方形で表されます。

次に、square変数のcenterプロパティは、ドット構文(square.center)を介してアクセスされます。これにより、centerのゲッターが呼び出され、現在のプロパティ値が取得されます。ゲッターは、既存の値を返すのではなく、実際に新しいポイントを計算して返し、正方形の中心を表します。上で見られるように、ゲッターは(5, 5)の中心点を正しく返します。

次に、centerプロパティを(15, 15)の新しい値に設定します。これにより、正方形が上下に移動し、下の図のオレンジ色の正方形で示されている新しい位置に移動します。centerプロパティを設定すると、centerのセッターが呼び出され、格納されているoriginプロパティのx値とy値が変更され、正方形が新しい位置に移動します。

10.6. 短縮セッター宣言

計算型プロパティのセッターで、設定する新しい値の名前が定義されていない場合は、デフォルトの名前であるnewValueが使用されます。この省略表記を利用したRect構造体の代替バージョンを次に示します。

struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

10.7. 短縮ゲッター宣言

ゲッターの本体全体が単一の式である場合、ゲッターは暗黙的にその式を返します。これは、この短縮表記とセッターの短縮表記を利用するRect構造体の別のバージョンです。

struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                          y: origin.y + (size.height / 2))
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

ゲッターからの戻り値を省略することは、「暗黙の戻り値を持つ関数」で説明されているように、関数からの戻り値を省略することと同じルールに従います。

10.8. 読み取り専用の計算型プロパティ

ゲッターはあるがセッターがない計算型プロパティは、読み取り専用の計算型プロパティと呼ばれます。読み取り専用の計算型プロパティは常に値を返し、ドット構文を介してアクセスできますが、別の値に設定することはできません。

注意
計算型プロパティ(読み取り専用の計算型プロパティを含む)は、値が固定されていないため、varキーワードを使用して変数プロパティとして宣言する必要があります。letキーワードは定数プロパティにのみ使用され、インスタンスの初期化の一部として設定された後は値を変更できないことを示します。

getキーワードとその中括弧を削除することにより、読み取り専用の計算型プロパティの宣言を簡略化できます。

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// "the volume of fourByFiveByTwo is 40.0"を出力します

この例では、Cuboidと呼ばれる新しい構造体を定義します。これは、width,heightおよびdepthのプロパティを持つ3D長方形ボックスを表します。この構造体には、volumeと呼ばれる読み取り専用の計算型プロパティもあります。これは、直方体の現在の量を計算して返します。特定のvolume値にwidth, heightおよびdepthのどの値を使用するかがあいまいになるため、volumeを設定可能にすることは意味がありません。それでも、Cuboidが読み取り専用の計算型プロパティを提供して、外部ユーザーが現在の計算された量を検出できるようにすると便利です。

10.9. プロパティオブザーバー

プロパティオブザーバーは、プロパティの値の変化を観察して対応します。新しい値がプロパティの現在の値と同じであっても、プロパティの値が設定されるたびにプロパティオブザーバーが呼び出されます。

次の場所にプロパティオブザーバーを追加できます。

  • 定義した格納型プロパティ
  • 継承する格納型プロパティ
  • 継承する計算型プロパティ

継承されたプロパティの場合、サブクラスでそのプロパティをオーバーライドすることにより、プロパティオブザーバーを追加します。定義する計算型プロパティの場合、オブザーバーを作成する代わりに、プロパティのセッターを使用して値の変更を監視し、応答します。プロパティのオーバーライドについては、オーバーライドで説明しています。

プロパティにこれらのオブザーバーのいずれかまたは両方を定義するオプションがあります。

  • willSetは、値が格納される直前に呼び出されます。
  • didSetは、新しい値が格納された直後に呼び出されます。

willSetオブザーバーを実装すると、定数パラメータとして新しいプロパティ値が渡されます。willSet実装の一部として、このパラメータの名前を指定できます。実装内にパラメータ名と括弧を記述しない場合、パラメータはデフォルトのパラメータ名newValueで使用可能になります。

同様に、didSetオブザーバーを実装すると、古いプロパティ値を含む定数パラメータが渡されます。パラメータに名前を付けるか、oldValueのデフォルトのパラメータ名を使用できます。独自のdidSetオブザーバー内のプロパティに値を割り当てると、割り当てた新しい値が、設定されたばかりの値に置き換わります。

注意
スーパークラスのプロパティのwillSetおよびdidSetオブザーバーは、スーパークラス初期化子が呼び出された後、プロパティがサブクラス初期化子に設定されたときに呼び出されます。スーパークラス初期化子が呼び出される前に、クラスが独自のプロパティを設定している間は呼び出されません。

イニシャライザ委任の詳細については、「値型のイニシャライザ委任」および「クラス型のイニシャライザ委任」を参照してください。

これは、willSetとdidSetの動作例です。以下の例では、StepCounterという新しいクラスを定義しています。このクラスは、歩行中に人が踏んだ歩数の合計を追跡します。このクラスは、歩数計やその他の歩数計からの入力データとともに使用して、日常生活での人の運動を追跡することができます。

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// totalStepsを200に設定しようとしています
// 200ステップを追加
stepCounter.totalSteps = 360
// totalStepsを360に設定しようとしています
// 160ステップを追加
stepCounter.totalSteps = 896
// totalStepsを896に設定しようとしています
// 536ステップを追加

StepCounterクラスは、Int型のtotalStepsプロパティを宣言します。これは、willSetおよびdidSetオブザーバーを持つ格納型プロパティです。

totalStepsのwillSetおよびdidSetオブザーバーは、プロパティに新しい値が割り当てられるたびに呼び出されます。これは、新しい値が現在の値と同じであっても当てはまります。

この例のwillSetオブザーバーは、次の新しい値にnewTotalStepsのカスタムパラメータ名を使用します。この例では、設定しようとしている値を出力するだけです。

totalStepsの値が更新された後、didSetオブザーバーが呼び出されます。totalStepsの新しい値を古い値と比較します。合計ステップ数が増えると、新しいステップがいくつ実行されたかを示すメッセージが出力されます。didSetオブザーバーは、古い値のカスタムパラメータ名を提供せず、代わりにoldValueのデフォルト名が使用されます。

注意
オブザーバーを持つプロパティをin-outパラメータとして関数に渡すと、willSetおよびdidSetオブザーバーが常に呼び出されます。これは、in-outパラメータのコピーインコピーアウトメモリモデルが原因です。値は常に関数の最後にプロパティに書き戻されます。in-outパラメータの動作の詳細については、「In-Outパラメータ」を参照してください。

10.10. プロパティラッパー

プロパティラッパーは、プロパティの格納方法を管理するコードとプロパティを定義するコードの間に分離層を追加します。たとえば、スレッドセーフチェックを提供したり、基になるデータをデータベースに保存したりするプロパティがある場合は、すべてのプロパティにそのコードを記述する必要があります。プロパティラッパーを使用する場合、ラッパーを定義するときに管理コードを1回記述し、それを複数のプロパティに適用してその管理コードを再利用します。

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

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

セッターは新しい値が12未満であることを確認し、ゲッターは保存された値を返します。

注意
上記の例のnumberの宣言は、変数をprivateとしてマークします。これにより、numberはTwelveOrLessの実装でのみ使用されます。他の場所で記述されたコードは、wrappedValueのgetterとsetterを使用して値にアクセスし、数値を直接使用することはできません。privateについては、アクセス制御を参照してください。

プロパティの前にラッパーの名前を属性として書き込むことにより、ラッパーをプロパティに適用します。これは、TwelveOrLessプロパティラッパーによって実装されたのと同じ(かなり恣意的な)「小さい」定義を使用して、小さい長方形を格納する構造です。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// "0"を出力します

rectangle.height = 10
print(rectangle.height)
// "10"を出力します

rectangle.height = 24
print(rectangle.height)
// "12"を出力します

heightとwidthのプロパティは、TwelveOrLess.numberをゼロに設定するTwelveOrLessの定義から初期値を取得します。数値10をrectangle.heightに格納すると、数値が小さいため成功します。24はプロパティ設定者のルールには大きすぎるため、24を格納しようとすると、実際には代わりに12の値が格納されます。

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

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プロパティは、プロパティラッパーTwelveOrLessのインスタンスを格納します。heightとwidthのゲッターとセッターは、wrappedValueプロパティへのアクセスをラップします。

10.11. ラップされたプロパティの初期値の設定

上記の例のコードは、TwelveOrLessの定義で数値に初期値を与えることにより、ラップされたプロパティの初期値を設定します。このプロパティラッパーを使用するコードでは、TwelveOrLessでラップされたプロパティに別の初期値を指定することはできません。たとえば、SmallRectangleの定義では、heightまたはwidthの初期値を指定できません。初期値の設定やその他のカスタマイズをサポートするには、プロパティラッパーに初期化子を追加する必要があります。これは、ラップされた最大値を設定する初期化子を定義する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つの初期化子が含まれています。これらの初期化子は、ラップされた値と最大値を設定するために使用されます。初期化と初期化構文の詳細については、初期化を参照してください。

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

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// "0 0"を出力します

heightとwidthをラップするSmallNumberのインスタンスは、SmallNumber()を呼び出すことによって作成されます。そのイニシャライザー内のコードは、デフォルト値のゼロと12を使用して、ラップされた初期値と最大値を設定します。プロパティラッパーは、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)
// "1 1"を出力します

ラッパーを使用してプロパティに= 1と書き込むと、init(wrappedValue:)初期化子の呼び出しに変換されます。heightとwidthをラップする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)
// "2 3"を出力します

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// "5 4"を出力します

heightをラップするSmallNumberのインスタンスは、SmallNumber(wrappedValue:2, maximum:5)を呼び出すことによって作成され、widthをラップするインスタンスは、SmallNumber(wrappedValue:3, maximum:4)を呼び出すことによって作成されます。

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

プロパティラッパー引数を含める場合は、割り当てを使用して初期値を指定することもできます。Swiftは、割り当てをwrappedValue引数のように扱い、含めた引数を受け入れる初期化子を使用します。例えば:

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// "1"を出力します

mixedRectangle.height = 20
print(mixedRectangle.height)
// "12"を出力します

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

10.12. プロパティラッパーからの値の射影

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

@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    init() {
        self.number = 0
        self.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)
// "false"を出力します

someStructure.someNumber = 55
print(someStructure.$someNumber)
// "true"を出力します

someStructure.$someNumberを書き込むと、ラッパーの射影値(projected value)にアクセスします。4のような小さな数値を格納した後、someStructure.$someNumberの値はfalseになります。ただし、55のように大きすぎる数値を格納しようとすると、予測値は真になります。

プロパティラッパーは、任意の型の値をその射影値(projected value)として返すことができます。この例では、プロパティラッパーは、数値が調整されたかどうかに関係なく、1つの情報のみを公開するため、そのブール値を射影値(projected value)として公開します。より多くの情報を公開する必要があるラッパーは、他のデータ型のインスタンスを返すことも、selfを返してラッパーのインスタンスをその射影値(projected value)として公開することもできます。

プロパティゲッターやインスタンスメソッドなど、型の一部であるコードから射影値(projected value)にアクセスする場合は、他のプロパティにアクセスするのと同じように、プロパティ名の前のselfを省略できます。次の例のコードは、heightとwidthの周りのラッパーの射影値(projected value)を$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
    }
}

プロパティラッパー構文は、ゲッターとセッターを持つプロパティの単なる糖衣構文(syntactic sugar)であるため、heightとwidthへのアクセスは、他のプロパティへのアクセスと同じように動作します。たとえば、resize(to:)のコードは、プロパティラッパーを使用してheightとwidthにアクセスします。resize(to:.large)を呼び出すと、.largeのswitchケースにより、長方形のheightとwidthが100に設定されます。ラッパーはこれらのプロパティの値が12を超えないようにし、射影値(projected value)をtrueに設定します。それがそれらの値を調整したという事実を記録します。resize(to:)の最後に、returnステートメントは$heightと$widthをチェックして、プロパティラッパーがheightまたはwidthのどちらを調整したかを判断します。

【補足】
シンタックスシュガーとは、プログラミング言語で、ある構文を別の記法で記述できるようにしたもの。

10.13. グローバル変数とローカル変数

プロパティを計算および監視するための上記の機能は、グローバル変数およびローカル変数でも使用できます。グローバル変数は、関数、メソッド、クロージャ、または型コンテキストの外部で定義される変数です。ローカル変数は、関数、メソッド、またはクロージャコンテキスト内で定義される変数です。

前章で遭遇したグローバル変数とローカル変数はすべて格納型変数です。格納型プロパティと同様に、格納型変数は、特定の型の値のストレージを提供し、その値を設定および取得できるようにします。

ただし、グローバルスコープまたはローカルスコープのいずれかで、計算型変数を定義し、格納型変数のオブザーバーを定義することもできます。計算型変数は、値を格納するのではなく計算し、計算型プロパティと同じ方法で記述されます。

注意
グローバル定数と変数は、レイジー格納型プロパティと同様の方法で、常にレイジーに計算されます。レイジー格納型プロパティとは異なり、グローバル定数と変数は、lazy修飾子でマークする必要はありません。

ローカル定数と変数がレイジー計算されることはありません。

10.14. 型プロパティ

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

型の1つのインスタンスではなく、型自体に属するプロパティを定義することもできます。その型のインスタンスをいくつ作成しても、これらのプロパティのコピーは1つだけになります。これらの種類のプロパティは、型プロパティと呼ばれます。

型プロパティは、すべてのインスタンスが使用できる定数プロパティ(Cの静的定数など)や、その型のすべてのインスタンスに対してグローバルな値を格納する変数プロパティ(Cの静的変数など)など、特定の型のすべてのインスタンスに共通の値を定義するのに役立ちます。

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

注意
格納型インスタンスプロパティとは異なり、格納型型プロパティには常にデフォルト値を指定する必要があります。これは、型自体に、初期化時に格納型型プロパティに値を割り当てることができる初期化子がないためです。

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

10.15. 型プロパティ構文

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
    }
}

注意
上記の計算型型プロパティの例は、読み取り専用の計算型型プロパティ用ですが、計算型インスタンスプロパティの場合と同じ構文で読み取り/書き込み計算型型プロパティを定義することもできます。

10.16. 型プロパティのクエリと設定

型プロパティは、インスタンスプロパティと同様に、ドット構文でクエリおよび設定されます。ただし、型プロパティは、その型のインスタンスではなく、型に対してクエリおよび設定されます。 例えば:

print(SomeStructure.storedTypeProperty)
// "Some value."を出力します
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// "Another value."を出力します
print(SomeEnumeration.computedTypeProperty)
// "6"を出力します
print(SomeClass.computedTypeProperty)
// "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つの格納型プロパティを定義します。最初のthresholdLevelは、オーディオレベルが取ることができる最大の閾値を定義します。これは、すべてのAudioChannelインスタンスの定数値10です。オーディオ信号が10より大きい値で着信した場合、この閾値に制限されます(以下で説明します)。

2番目の型プロパティは、maxInputLevelForAllChannelsと呼ばれる変数に格納されたプロパティです。これにより、AudioChannelインスタンスが受信した最大の入力値が追跡されます。初期値0から始まります。

AudioChannel構造体は、currentLevelと呼ばれる格納型インスタンスプロパティも定義します。これは、チャネルの現在のオーディオレベルを0〜10のスケールで表します。

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

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

注意
これら2つのチェックの最初のチェックで、didSetオブザーバーはcurrentLevelを別の値に設定します。ただし、これによってオブザーバーが再度呼び出されることはありません。

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

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

左チャネルのcurrentLevelを7に設定すると、maxInputLevelForAllChannels型プロパティが7に等しくなるように更新されていることがわかります。
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// "7"を出力します
print(AudioChannel.maxInputLevelForAllChannels)
// "7"を出力します

右チャネルのcurrentLevelを11に設定しようとすると、右チャネルのcurrentLevelプロパティが最大値の10に制限され、maxInputLevelForAllChannels型プロパティが10に等しくなるように更新されていることがわかります。

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// "10"を出力します
print(AudioChannel.maxInputLevelForAllChannels)
// "10"を出力します
タイトルとURLをコピーしました