2019/12/31 Swift プロパティ パート1

プロパティはクラス・構造体・列挙型に値を結びつけます。ストアドプロパティは定数や変数の値をインスタンスの一部として格納し、一方コンピューテドプロパティは値を計算します。コンピューテドプロパティはクラス・構造体・列挙型に存在し、ストアドプロパティはクラスと構造体に存在します。

ストアドプロパティとコンピューテドプロパティは通常特定の型のインスタンスに結びつけられます。しかしインスタンスではなく型そのものに結びつけられるプロパティもあります。タイププロパティとして知られています。

さらにプロパティの値の変化を検知するプロパティオブザーバと、変化に対応する独自の挙動を定義することができます。プロパティオブザーバーはあなたが定義したストアドプロパティに対して用意できます。さらにサブクラスがスーパークラスから継承したプロパティに対しても用意できます。

さらにゲッター・セッターとして用意したコードを、複数のプロパティに対して利用できる「プロパティラッパー」もあります。

ストアドプロパティ

ストアドプロパティは特定のクラス・構造体のインスタンスの一部としての定数・変数です。ストアドプロパティは変数のストアドプロパティ(varキーワードを使用)、定数のストアドプロパティ(letキーワードを使用)どちらも使用できます。

ストアドプロパティを宣言と同時に初期値を設定することができます。

下のサンプルはFixedLengthRangeという名の構造体を定義しています。整数の範囲を表す構造体で、インスタンス生成後範囲の変更はできないものです。

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8

変数のプロパティと定数のプロパティがある。ここでは定数のプロパティだから生成後変更できない、と言っている。それはそれでいいのだが、

変数へのインスタンス代入

定数へのインスタンス代入

でプロパティの値の変更ができるか(値型と参照型)

→構造体は不可(エラー)。クラスはできる。クラスは参照型なのでプロパティの値を変えても参照を変えたわけではない、みたいなことだった、はず。

FixedLengthRangeのインスタンスは変数のストアドプロパティfirstValueと定数のストアドプロパティlengthを持っています。上のサンプルではlengthは定数のプロパティとして定義されているので、新しいFixedLengthRangeのインスタンスを生成してlengthを初期化した後、lengthの値を変更することはできません。

定数の構造体インスタンスのストアドプロパティ

構造体のインスタンスを生成して定数に代入した時、たとえプロパティが変数として定義されていても、インスタンスのプロパティの値を変更することはできません。

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property

rangeOfFourItemsは定数として宣言されている(letキーワードが使われている)ので、firstValueプロパティは変数のプロパティですが、firstValueプロパティの値を変更できません。

この挙動は構造体が値型であることが原因です。値型のインスタンスが定数に代入されたとき、そのインスタンスのプロパティも同様に定数となります。

クラスは参照型なので、構造体のこの挙動とは異なります。参照型のインスタンスを定数に代入した時、そのインスタンスの変数プロパティの値を変更できます。

Lazy Stored Properties(遅延ストアドプロパティ)

遅延ストアドプロパティは、初めてアクセスされる時まで初期値が計算されないプロパティです。遅延ストアドプロパティはプロパティの宣言の前にlazyキーワードを置くことで定義します。

 NOTE

遅延ストアドプロパティは必ず変数として宣言します。

プロパティの初期値が外部要因によるもの、そのインスタンスの初期化完了までその値がわからない場合に、遅延ストアドプロパティが使えます。また、プロパティの初期値の算出に複雑で機械的な負荷が大きい作業が必要な場合にも遅延ストアドプロパティが役立ちます。このような場合、負荷が大きい作業は必要なタイミングで初めて行われるべきもので、必要なタイミングより早く行われるべきではありません。

下のサンプルは複雑なクラスの不必要な初期化を、遅延ストアドプロパティを使うことで避けています。このサンプルは二つのクラス、DataImporter とDataManagerを定義しています(部分的に)。

class DataImporter {
    /*
    DataImporter is a class to import data from an external file.
    The class is assumed to take a nontrivial amount of time to initialize.
    */
    var filename = "data.txt"
    // the DataImporter class would provide data importing functionality here
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // the DataManager class would provide data management functionality here
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property has not yet been created

DataManagerクラスはdataという名のストアドプロパティを持っています。それはString型の空の配列で初期化されます。DataManagerクラスの目的はこのString型の配列のデータを管理し、String型の配列のデータにアクセスすることです。

DataManagerクラスの機能の一つとして、ファイルからデータをインポートすることがあります。この機能はDataImporterクラスにより提供され、DataImporterクラスのインスタンスの初期化には少なくない時間がかかることが想定されます。DataImporterインスタンスの初期化時にファイルを開いて中のコンテンツをメモリに読み込む必要があるからです。

DataManagerインスタンスはファイルからデータをインポートせずに、データを管理することはできます。ですので、DataManagerクラスのインスタンス生成時、DataImporterインスタンスの初期化作業は必須ではありません。DataImporterインスタンスを初めて使う時にDataImporterインスタンスを生成する方が合理的です。

lazyキーワードをつけていますので、importerプロパティのDataImporterインスタンスは、importerプロパティに初めてアクセスされた時にのみ生成されます。それは例えばimporterプロパティのfilenameプロパティにアクセスされた時などです。

print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"

NOTE

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

 

コンピューテドプロパティ

ストアドプロパティに加えてクラス・構造体・列挙型はコンピューテドプロパティを定義することができます。これは実際に値を格納しません。代わりに、ゲッターとセッター(無くてもよい)により、間接的に他のプロパティに値をセットしたり、他のプロパティの値を取得したりします。

 

 

このサンプルは三つの構造体幾何学的図形を操作する構造体を定義しています。

  • ポイントのx座標とy座標をカプセル化するPoint型
  • 幅と高さ( width and a height)をカプセル化するSize型
  • 原点とサイズで長方形を定義するRect型

構造体Rectにはcenterという名前のコンピューテドプロパティがあります。現在のRectのcenterの値は、常にoriginとsizeから(自動的に)計算されますので、centerの値をあなたが明示的にPoint型の値として指定する必要はありません。代わりにRectにコンピューテドプロパティとして独自にゲッターとセッターを定義することで、まるでストアドプロパティとして扱っているかのように扱うことができます。

上のサンプルではsquareという変数に新しいRectインスタンスを生成し代入しています。変数squareはorigin(0,0)、widthとheightが10で初期化されています。下の画像で青い図形を表しています。

square.centerとして変数squareのcenterプロパティにアクセスすると、現在のcenterプロパティの値を取得するため、centerのゲッターが呼び出されます。上のサンプルではゲッターはcenterの値(5,5)を正しく返しています。

次にcenterプロパティに新しい値(15,15)を代入すると、squareが右上に移動します。新しい位置は下の画像でオレンジ色の図形を表されています。centerプロパティに値を代入するとcenterのセッターが呼び出され、originプロパティのxとyの値が計算されてセットされます。

セッターの簡略記法

コンピューテドプロパティのセッターが新しい値をセットする変数名を定義しない場合、デフォルトの名前として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)
        }
    }
}

 

ゲッターの簡略記法

ゲッターの本文が一文の場合、returnキーワードを省略できます。以下はこの簡略記法を使用した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)
        }
    }
}

returnを省略できる上記の簡略記法は関数の本体が一文の場合にreturnを省略できるルールと同じです。

読み取り専用コンピューテドプロパティ

セッターがないゲッターのみのコンピューテドプロパティは読み取り専用コンビューテドプロパティとして知られています。読み取り専用コンピューテドプロパティはドットによりアクセスすると値を返しますが、別の値をセットすることはできません。

NOTE

読み取り専用コンピューテドプロパティを含め、コンピューテドプロパティはvarキーワードを用いて変数として宣言しなければなりません。値が固定されていないからです。インスタンスの初期化として値がセットされた後、値が変わらないことを示すために、letキーワードは定数のプロパティにのみ使用します。

読み取り専用コンピューテドプロパティの宣言をシンプルにするためにgetキーワードと中括弧を省略することができます。

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    
    var volume1 : Double{
        get{
            return width * height * depth
        }
    }
    
    var volume2 : Double{
        get{
            width * height * depth //getterが一文なのでreturnを省略できる。
        }
    }
    
    var volume3 : Double{
        return width * height * depth   //読み取り専用コンピューテドプロパティをシンプルに
                                        //書くためにgetと{}を省略している。
    }
    
    var volume4 : Double {
        width * height * depth  //一文なのでreturnを省略できる。
    }
    
}
let fourByFiveByTwo = Cuboid(width: 2.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume1)")
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume2)")
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume3)")
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume4)")

//the volume of fourByFiveByTwo is 20.0
//the volume of fourByFiveByTwo is 20.0
//the volume of fourByFiveByTwo is 20.0
//the volume of fourByFiveByTwo is 20.0

このサンプルではCuboidという名の新しい構造体を定義しています。width,height,depthにより3Dの箱を表します。直方体の体積を計算して返すvolumeという名の読み取り専用コンピューテドプロパティもあります。volumeプロパティを書き込み可能にしたところで、width,height,depthとの関係性がはっきりしないので、あまり意味がありません。それよりも読み取り専用コンピューテドプロパティにしてユーザーが計算された体積にアクセスできる方が有用です。

プロパティオブザーバ

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

lazyストアドプロパティ以外のストアドプロパティにプロパティオブザーバを定義できます。サブクラスでオーバーライドして継承したプロパティ(ストアドプロパティでもコンピューテドプロパティでも)にもプロパティオブザーバを定義できます。オーバーライドしたプロパティ以外のコンピューテドプロパティにはプロパティオブザーバを定義する必要はありません。コンピューテドプロパティのセッターで「値の監視と変化への対応」を実装すればよいからです。プロパティのオーバーライドについてはOverriding.

プロパティオブザーバには以下の2種類があり、プロパティに対して二つのうちのどちらか、あるいは両方定義できます。

  • willset : 値がプロパティに格納される直前に呼び出される処理
  • didset : 新しい値がプロパティに格納された直後に呼び出される処理

sample-propertyObserver

class StepCounter {
    var totalSteps: Int = 0 {
        
        /*
        //↓newTotalStepsという引数で新しい値を受け取るパターン
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        */
        
        //↓引数を自分で定義せず、デフォルトのnewValueを使うパターン
        willSet {
            print("About to set totalSteps to \(newValue)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

上記はwillSetとdidSetのサンプルです。StepCounterというクラスを定義しています。ウォーキングでの歩数を管理するクラスです。万歩計などの歩数データを外部から読み込んでStepCounterクラスで使用することを想定しています。

willsetオブザーバーを実装する時、プロパティにセットされる新しい値を定数の引数として受け取ります。willset実装時にその引数の名前を指定することができます(上記サンプルではnewTotalSteps)。引数名を自分で指定しなかった時は、デフォルトの引数名としてnewValueという引数を使用します。

willsetと同じように、didsetオブザーバを実装する時、新しくセットされる前の値を定数の引数で受け取ります。その引数に自分で名前をつけて定義することもできますし、自分で定義せずにデフォルトで用意されるoldValueを使用することもできます(上記サンプルではoldValueを使っている)。sample-propertyObserverでいうと、現在totalStepsに200がセットされている時点(23行目)で、totalStepsに新しい値360を代入する(26行目)と、didSetオブザーバ内のtotalStepsに360がセットされ、oldValueに200がセットされます。

StepCounterクラスはInt型のtotalStepsプロパティを宣言しています。これはストアドプロパティでwillSetとdidSetオブザーバが定義されています。

totalStepsのwillSetとdidSetオブザーバは、totalStepsに新しい値が代入される度呼び出されます。新しい値がそれまでの値と同じであってもオブザーバは呼び出されます。

サンプルではwillSetオブザーバではnewTotalStepsという名前の独自の引数を使い、新しい値を受け取っています。サンプルでwillSetオブザーバはシンプルに新しくセットされる値を表示します。

didSetオブザーバはtotalStepsに新しい値がセットされた後に呼び出されます。サンプルでは新しいtotalStepsの値をそれまでの値と比較しています。もし歩数が増えたら、歩数の増分を示すメッセージを表示します。didSetオブザーバはそれまでの値について独自の引数名を定義せず、その代わりにデフォルトのoldValueを使っています。

プロパティラッパー(Property Wrappers)

「プロパティの定義部分の記述」と「プロパティへの値のセットの方法の記述」を明確に分離する記述方法がプロパティラッパー(property wrapper)です。

プロパティラッパーを使えば、ラッパーの定義で「プロパティへの値のセットの方法」を記述し、そのコードを複数のプロパティに適用できます。


プロパティラッパーの定義方法

プロパティラッパーは、wrappedValueプロパティを持つ構造体・列挙型・クラスを定義します。下のコードは、構造体TwelveOrLessは、ラップしている値は常に12以下であることを保証します。12を超える値を代入しようとすると代わりに12を代入します。

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

セッターはセットする値が12以下であることを指定し、ゲッターは格納された値を返します。

NOTE

上のサンプルでnumberの宣言で変数にprivateを付けていますが、これはTwelveOrLessの実装中でのみnumberを使えることを示します。この構造体の定義の外でnumberにアクセスする場合直接numberにアクセスすることはできず、wrappedValueを使わないといけません。


ラッパーをプロパティに適用する方法

属性としてラッパー名(TwelveOrLess)をプロパティの前に記述します。SmallRectangleは小さい長方形を表す構造体です。「小さい」の基準として、プロパティラッパー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の定義により初期値0がセットされます。11行目でrectangle.heightに10を代入する処理は成功します(12以下だから)。15行目で24を代入しようとしていますが12を超えていますので、プロパティラッパーのセッターのルールにより代わりに12が代入されます。

上記のような属性を使用したプロパティラッパーの記述とは違う記述方法で、上記の挙動を実現することもできます。sample-pw-3では

 

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

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

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 50
print(rectangle.height)
// Prints "12"

rectangle.height = 2
print(rectangle.height)
// Prints "2"

_heightプロパティと_widthプロパティにはプロパティラッパーであるTwelveOrLess構造体のインスタンスが格納されています。wrappedValueプロパティへのアクセスをheight、widthのゲッターとセッターがラップして(包んで)います。

ラップされたプロパティに初期値をセットする

 

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/Properties.html

コメントを残す

メールアドレスが公開されることはありません。