楽水

人々の創造が自由に表現できる舞台づくり

Swift

Swiftのプロパティ【プロパティオブザーバーなどをわかりやすく解説】

投稿日:

プロパティは値と特定のクラスや構造体、列挙型を結び付けます。
Swiftのプロパティには、値を保持するプロパティだけではなく様々な種類のプロパティがあります。
今回は、Swiftのプロパティについて以下の観点で解説します。

  • Swiftのプロパティの種類
  • 保持型プロパティ(stored property:ストアドプロパティ)
  • 計算型プロパティ(computed property:コンピューテッドプロパティ)
  • プロパティオブザーバー
  • タイププロパティ

参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

Swiftのプロパティの種類

Swiftのプロパティには以下の種類があります。

この表は、インスタンスに対するプロパティと型に対するプロパティで分けています。
インスタンスと型は、集合を構成する具体的な要素(インスタンス)と、それらを抽象化した集合(型)の違いになります。
インスタンスに対するプロパティには、保持型プロパティ(stored property:ストアドプロパティ)と計算型プロパティ(computed property:コンピューテッドプロパティ)があります。
型に対するプロパティには、タイププロパティがあります。
このうち、列挙型は決まった値を持っているので保持型プロパティは適用されません。
各プロパティについて見ていきましょう。

保持型プロパティ(stored property:ストアドプロパティ)

保持型プロパティは、特定のクラスや構造体のインスタンスの一部として、シンプルな形式で保存される定数または変数のことです。
次の例では、生成後に範囲の長さを変更できない整数値の範囲を持つ、構造体 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 は新しい範囲が生成される時に初期化されますが、定数のプロパティであるため、それ以降は変更できません。

遅延プロパティ

遅延プロパティは、初めて利用されるまで初期値が算出されないプロパティです。
宣言の前に lazy を記述して、遅延プロパティにします。
インスタンスが初期化を完了するまで初期値が取り出されることがないため、遅延プロパティを常に(var キーワードで)変数として宣言する必要があります。
定数のプロパティは、初期化を完了する前に常に値を持つ必要があり、遅延として宣言することはできません。
遅延プロパティは、プロパティの初期値がインスタンスの初期化が完了するまで値がわからない外部の要因に依存しているときに役立ちます。
また、プロパティの初期値が複雑か計算コストが高いセットアップを必要とし、そのセットアップが必要とならないか、必要となるまで実行すべきでないときにも、遅延プロパティは役立ちます。
次の例では、複雑なクラスの不必要な初期化を避けるために遅延プロパティを使用しています。

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 インスタンスはまだ生成されていない

print(manager.importer.fileName)
// importer プロパティの DataImporter インスタンスは生成されている
// “data.txt” と出力

importer プロパティに lazy が付けられているため、fileName プロパティを問い合わせるときのように、importer プロパティに初めてアクセスしたときに DataImporter インスタンスは生成されます。

計算型プロパティ(computed property:コンピューテッドプロパティ)

保持型プロパティに加えて、クラス、構造体および列挙型に、実際には値を保管しない計算型プロパティを定義することができます。
保持する代わりに、他のプロパティの値を間接的に取り出す getter と、設定する任意の setter を提供します。
計算型プロパティの値は固定でないため、var キーワードの変数プロパティとして計算型プロパティを宣言する必要があります。
下の例では、幾何学的図形を扱う 3 つの構造体を定義しています。
Point は、(x, y) 座標を表す。
Size は、width と height で表す。
Rect は、長方形の原点とサイズを定義する。

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)” と出力

構造体 Rect は、計算型プロパティ center も提供しています。
Rect の現在の中心位置は、常に origin と size で特定され、中心位置を Point 値として保持する必要はありません。
代わりに、保持型プロパティのように長方形の center を扱えるよう、Rect は計算型変数 center の getter と setter を定義しています。

簡略 setter 宣言

計算型プロパティの setter に、設定される新しい値の名前を定義していない場合、デフォルトの名前 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)
  }
 }
}

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

setter が無く、getter のみの計算型プロパティは、読み取り専用の計算型プロパティです。
読み取り専用の計算型プロパティは、常に値を返し、ドットシンタックスでアクセスでき、異なる値を設定できません。
読み取り専用の計算型プロパティの宣言から、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” と出力

プロパティオブザーバー

プロパティオブザーバは、プロパティ値の変更を監視して反応します。
プロパティオブザーバは、新しい値がプロパティの現在値と同じであっても、プロパティ値が設定されるたびに呼び出されます。
遅延ストアドプロパティは別として、定義した保持型プロパティにプロパティオブザーバを追加することができます。
サブクラス内でプロパティをオーバーライドすることで、(保管型か計算型かを問わず)継承したプロパティにもプロパティオブザーバを追加することができます。
次に示すオブザーバの片方または両方を、プロパティに定義することができます。

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

willSet オブザーバを実装した場合、定数パラメータとして新しいプロパティ値が渡されます。
willSet 実装の一部としてこのパラメータに名前を指定することができます。
実装内にパラメータ名と丸括弧を記述していない場合、デフォルトのパラメータ名 newValueでパラメータを利用できます。
同様に、didSet オブザーバを実装した場合、古いプロパティ値を持つ定数パラメータが渡されます。
パラメータに名前を付けるか、デフォルトのパラメータ名 oldValue を利用することができます。
didSet オブザーバ内でプロパティに値を代入する場合、設定されたばかりの値を、代入する新しい値が置き換えます。
次の例を見てください。

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

StepCounter クラスは Int 型の totalSteps プロパティを宣言しています。
これは保持型プロパティで、willSet と didSet オブザーバがあります。
totalSteps の willSet と didSet オブザーバは、プロパティに新しい値が代入されたときに呼び出されます。
新しい値が現在値と同じであってもそうなります。
この例の willSet オブザーバは、新しい値に newTotalSteps というパラメータ名を使用しています。
この例では、設定されようとしている値を単に出力しています。
didSet オブザーバは、totalSteps の値が更新された後に呼び出されます。
totalStepsの新しい値と古い値を比較しています。ステップ総数が増加している場合、進んだ歩数を示すメッセージが出力されます。didSet オブザーバには、古い値にパラメータ名を付けることはできず、代わりにデフォルト名の oldValue を使用します。

タイププロパティ

型のあるインスタンスにではなく、その型自身に属するプロパティを定義することもできます。
その型のインスタンスをどれほどたくさん生成したとしても、これらのプロパティのコピーは 1 つだけになります。
このようなプロパティのことをタイププロパティと呼びます。
タイププロパティは、すべてのインスタンスが利用できる定数プロパティ、あるいは特定の型のすべてのインスタンスにグローバルな値を保持する変数プロパティのように、その型のすべてのインスタンスに共通の値を定義するのに役に立ちます。
保持型のインスタンスプロパティと違い、保管型のタイププロパティにはデフォルト値が必要です。
これは、初期化時に保持型のタイププロパティに値を代入できるイニシャライザを、型自身が持っていないためです。
保持型のタイププロパティは、遅延して最初のアクセス時に初期化されます。
以下の例のように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
 }
}

タイププロパティには、インスタンスプロパティと同じように、ドットシンタックスでアクセスします。
ですが、タイププロパティは型のインスタンスにではなく、その型に対してアクセスします。

print(SomeStructure.storedTypeProperty)
// “Some value.” と出力
SomeStructure.storedTypeProperty = “Another value.”
print(SomeStructure.storedTypeProperty)
// “Another value.” と出力
print(SomeEnumeration.computedTypeProperty)
// “6” と出力
print(SomeClass.computedTypeProperty)
// “27” と出力

以上、今回は、Swiftのプロパティについて解説しました。

-Swift

執筆者: