この記事は、AppleがWWDC2015で行った、値型に関するセッションを要約したものです。日本語のTranscriptが提供されておらず、前回の記事でお約束したので作成しました。簡潔に要約したつもりですが、意味が分からないところを言い方等から推定している部分もあり、誤解があるかもしれません。何かあれば遠慮なくご質問ご指摘をお寄せください。
サンプルコードはSwift5のPlayground(Xcode 10.2.1)で実行できるよう、修正しています。
なお、QiitaにはSwiftの値型に関する素晴らしい記事がいくつもあります。こちらもお勧めします。
純粋値型Swift
Swiftで値型と参照型の違いを理解する
SwiftのArrayが実はすばらしかった
Swiftの値型と参照型、値渡しと参照渡し
Building Better Apps with Value Types in Swift
アップルのDoug Gregor氏:
Swiftで、値型を使ってより良いアプリを作る話をします。
まず、参照型意味論について話し、イミュータビリティ(変更できない事)が参照型がもたらす問題を解決できる、という事をみていきます。
値型と値型意味論について掘り下げて、特にSwiftでどう動くかをみて、値型を実際に使う事について話し、それから参照型と値型を混ぜて使う方法について話します。
参照型意味論 (Reference Semantics)
Swiftで参照型を使うには、クラスを定義します。ここに単純な温度クラスがあります。
class Temperature {
var celsius: Double = 0 // 摂氏(℃)
var fahrenheit: Double { // 華氏(℉)
get { return celsius * 9 / 5 + 32 }
set { celsius = (newValue - 32 ) * 5 / 9 }
}
}
温度を摂氏(celsius)で保持して、華氏(fahrenheit)は計算で出します。温度の単位変換が簡単です。使ってみましょう。
let home = House()
let temp = Temperature()
// 室温を設定
temp.fahrenheit = 75 // 華氏75度は約24℃
home.thermostat.temperature = temp
// オーブンを設定
temp.fahrenheit = 425 // 華氏425度は約218℃
home.oven.temperature = temp
home.oven.bake()
家のインスタンスと、温度のインスタンスを作って、家の温度センサーを24度に設定します。それから、ご飯を作るのでオーブンにも火を入れました。
(みんな笑う)
部屋がひどく暑くなってしまいました。
お気づきと思いますが、この問題は、temp
がthermostat
とoven
の間で意図せず共有されてしまったために起こっています。これを回避するには、temp
のコピーを渡せばよいです。
home.thermostat.temperature = temp.copy()
...
home.oven.temperature = temp.copy()
こうすれば、home.thermostat
には新しいオブジェクトが与えられるので、temp
をオーブンの温度設定に使っても部屋は暑くなりません。
home.oven
へのコピーは技術的にはなくてもよく、効率が悪く時間とメモリの無駄ですが、安全のためにやっています。最初はこのコピーを忘れたために、火傷してしまったわけなので。
こういうのを、**防御的コピー(defensive copy)**といいます。必要だとわかっているからするのではなく、後で必要になるかもしれないからやっています。
さて、こういうバグは見つけるのが難しいし、.copy()
を忘れるのもありがちです。根本対策として、オーブンの方を直しておきましょう。
class Oven {
var _temperature = Temperature(cercius: 0)
var temperature: Temperature {
get { return _temperature }
set { _temperature = newValue.copy() } // 防御的コピー
}
}
サーモスタットもやらなくちゃ。
class Thermostat {
var _temperature = Temperature(cercius: 0)
var temperature: Temperature {
get { return _temperature }
set { _temperature = newValue.copy() } // 防御的コピー
}
}
(ひどい状況になってきました・・・)
Cocoa や Cocoa Touch では、実際にたくさんの防御的コピーをやっています。たとえば辞書は、キーをコピーして保持しています。このシステムでは、コピーが必要です。パフォーマンスを犠牲にして、そういう定型処理をあちこちに実装しました。Objective-Cではcopy()
メソッドで防御的コピーをサポートする事もしました。それらは役にたったけれど、それでも十分ではありませんでした。依然として、バグは無くなりませんでした。
イミュータビリティ(Immutability)は答えになるか
参照型意味論には問題があり、そこにはミュータビリティ(変更できる事)があります。もしかして、問題は参照型意味論にではなく、ミュータビリティそのものにあるのではないでしょうか。
イミュータブルな(変更できない)世界に移行するべきなのでしょうか?
関数型プログラミングのコミュニティ (訳注:関数型プログラミング言語はイミュータブルな世界を提供する) に尋ねたら、「そうだよ。もう10年もやってきたけど、すごく効果あるよ。」と、いうでしょう。その意図しない副作用が存在しない世界では、確かにオーブンでみたようなバグはなくせます。問題は、イミュータビリティには明確な欠点があることです。
-
イミュータビリティは、おかしなインタフェースを生みます。一部は変更不可能な世界で構成され、一部は今まで通りの変更可能で状態を考慮する必要がある、そういうちょっと変な形になってしまいます。
-
マシン語に変換する場合の効果性に疑問があります。プログラムは最終的にはマシン語に落とし込む必要があり、CPUは、状態を持つレジスタと、状態をもつキャッシュと、状態をもつRAMと、状態を持つ記憶域でできています。イミュータブルなアルゴリズムを効果的なままマシン語まで落とし込むのは簡単ではありません。
試してみましょう。
おかしなインタフェース
イミュータブルバージョンの温度クラスはこうなります。
class Temperature {
var celsius: Double = 0
var fahrenheit: Double {
return celsius * 9 / 5 + 32
}
init(celsius: Double) {
self.celsius = celsius
}
init(fahrenheit: Double) {
celsius = (fahrenheit - 32) * 5 / 9
}
}
変更しないのでlet
を使います。セッターもありません。
(訳注: 実際のセッションの例ではlet celsius: Double = 0
とlet
を使って宣言されているので、その事を指しています。Swift5ではcelsius
をlet
にできません。class
のイニシャライザで値を指定するためにはvar
である必要があるため、上記コードはそのように修正しています。class
ではなくstruct
なら、イニシャライザでlet
なcelsius
に値を代入できます。)
オーブンの温度を10度上げてみましょう。
イミュータブル版:
home.oven.temperature.fahrenheit += 10.0
ミュータブル版:
let current = home.oven.temperature
home.oven.temperature = Temperature(fahrenheit: current + 10.0)
ミュータブル版より、コードも処理も増えていますが、それでも、よくみると、home.oven.temperature
の値を =
で「変えて」います。本当にイミュータブルにするには、新しい温度を入れるために新しいオーブンと新しい家を作る必要があります。これは奇妙です。
エラトステネスのふるい (Sieve of Eatosthenes)
論理的で数学的な例として、エラトステネスのふるいをやってみましょう。古典的な、素数をみつけるアルゴリズムです。
ミュータブル版:
func primes(n:Int) -> [Int] {
var numbers = [Int](2 ..< n) // (1)
for i in 0 ..< n-2 {
let prime = numbers[i] // (2)
guard prime > 0 else { continue }
for multiple in stride(from: 2 * prime - 2, to: n - 2, by: prime) { // (3)
numbers[multiple] = 0
}
}
return numbers.filter { $0 > 0 } // (4)
}
(1)で最初の素数である2からnまでの整数の配列をつくり、
(2)で先頭から順に値を取り出し、
(3)でその値の倍数を、倍数だから素数ではないので、0に置き換え、
(4)で残った数値を出力しています。
簡単なアルゴリズムです。ミュータビリティを多用しています。
イミュータブルな例として、純粋関数型言語であるHaskellでの実装をみてみましょう。
Haskel版:
primes = sieve [2..]
sieve [] = []
sieve (p: xs) = p : sieve [x | x <- xs, x 'mod' p > 0]
美しいです。これをSwiftに書き換えてみるとこんな感じです。
func sieve(_ numbers: [Int]) -> [Int] {
if numbers.isEmpty { return [] }
let p = numbers[0] // (1)
return [p] + sieve(numbers[1..<numbers.count].filter { $0 % p > 0}) // (2)
}
(1)で最初の数値を素数として取り出して、
(2)のFilterでその数値の倍数以外を抜き出してから、再帰的に繰り返します。
考え方はミュータブル版によく似ています。
でもこれはふるいじゃない
Melissa O'Neilが"The Genuine Sieve of Eratosthenes"で示したところによると、Haskell版のふるいは実際にはふるいではなく、ミュータブル版よりたくさんの演算をしていて遅くなっています。ミュータブル版は数値の比較を倍数で行えるので、数値が大きくなるほど飛ばしていけるのに対し、イミュータブル版は、配列の全部を舐めていかなくてはなりません。こんな風に、イミュータブル版は効率がよくないのです。
(訳注:2.7GHz Core i5のMacBookProで、Xcode 10.2.1のPlaygroundを使って、n=100として100回実行した平均を求めてみたところ、ミュータブル版が約0.012秒。イミュータブル版が0.023秒でした。n=1000で、それぞれ約0.10秒、約0.71秒でした。)
Cocoa / Cocoa Touch の例
Cocoaにもイミュータブルな型があります。Date
, UIImage
, NSNumber
など。これらは安全性に寄与しましたが、欠点もあります。遅いし無駄が多い。やはりミュータブルな型が必要なので、NSArray
だけじゃなくて、NSMutableArray
があります。全部をイミュータブルにすると気が狂いそうです。
値型意味論 (Value semantics)
そこで、値型意味論です。値型を使うと違うやり方になります。
ミュータブルであることは有用で、簡単です。正しく使っている分には。問題は、共有です。
Objective-CやSwiftを使っていれば、値型はお馴染みですね。必要ですしいつも使っているでしょう。
二つの値型の変数は論理的に独立している
発想は単純です。値型変数が二つあったら、その変数は論理的に独立です。整数Aがあって、整数Bに入れる。AとBはイコールになるけれど、値はコピーされていて、実体が共有されることはない。そのあとBを変えても、Aは変わらない。
Swiftではかなりの変数が値型である
整数は、どんな言語でも値型です。
CGPointはどうでしょう。値型です。CGPointのAがあって、Bに入れて、その後Bを変えたらどうか。Aは変わりません。これまで使ってきて、こうでなかったら驚きますよね。
値型意味論は、このお馴染みの仕組みを、単純な型だけでなくもっと高度な型にまで拡張することに他なりません。
Swiftでは、文字列(String)も値型です。文字列Aを作って、文字列Bからコピーして、その後文字列Bを変えても文字列Aは変わらない。値型なので、AとBは完全に違う変数です。基本型は、整数も浮動小数点も文字列も文字も全て値型です。
同じように、集合も内部に値型を入れる限り値型です。**配列(Array)**も、**集合(Set)**も、**辞書(Dictionary)も値型です。
タプル、構造体、*列挙型も、値型をもつものはどれも値型になるので、値型だけで相当高度な抽象化が可能です。
同値性 (Equality)
値型において重要な特性が、同値性です。値型が同じかどうかは値で決まります。値を保持する変数は関係ありません。
var a: Int = 5
var b: Int = 3 + 2
assert(a == b)
CGPointも同様に値が比較されます。
var a: CGPoint = CGPoint(x: 3, y: 5)
var b: CGPoint = CGPoint(x: 2, y: 3)
b.x += 2
b.y += 2
assert(a == b) // aとbは同じです
文字列も同じです。(訳注:他の言語では違うものが多い)
var a: String = "Hello world!"
var b: String = "Hello"
b += " " + "world!"
assert(a == b) // aとbは同じです
配列もそう。値が比較されます。(訳注:これもほとんどの言語で違う)
var a: [Int] = [1, 2, 3]
var b: [Int] = [3, 2, 1]
b.sort(by: <)
assert(a == b) // aとbは同じです
ソート後のb
の値はa
と同じと判定されます。
Equatable
プロトコル
値型を作る時は、Equatable
プロトコルに準拠することが非常に重要です。
protocol Equatalbe {
static func ==(lhs: Self, rhs: Self) -> Bool
}
==
演算子は、三つの要件(同値律)を満たす必要があります。そうしないと、コードを理解できなくなります。(==
のテストではこの3種類をチェックしましょう)
- (反射律 Reflexive) a == a は常に true
- (対称律 Symmetric) a == b なら、b == a も true
- (推移律 Transitive) a == b かつ b == c なら、 a == c も true
CGPoint
をEquatable
に準拠させる場合、このようにします。
extension CGPoint : Equatable {}
func ==(lhs: CGPoint, rhs:CGPoint) -> Bool {
return (lhs.x == rhs.x) && (lhs.y == rhs.y)
}
==
は、ほとんどの場合、それぞれの値を比べていくだけでよいでしょう。
それでは、Temperature
を値型にしてみましょう。
struct Temperature : Equatable { // classをstructに。Equatableにも対応。
var celsius: Double = 0 // ミュータブル
var fahrenheit: Double {
get { return celsius * 9 / 5 + 32 }
set { celsius = (newValue - 32) * 5 / 9 }
}
}
func ==(lhs: Temperature, rhs: Temperature) -> Bool {
return lhs.celsius == rhs.celsius
}
参照型であるclass
から、値型であるstruct
に変えました。celsius
はvar
に戻し、Equatable
プロトコルにも対応させて、==
演算子を実装しました。
これを最初の例に使うと、もう火傷の心配はありません。
let home = House()
var temp = Temperature() // letをvarに変更する必要がある
temp.fahrenheit = 75 // 華氏75度は約24℃
home.thermostat.temperature = temp
temp.fahrenheit = 425 // 華氏425度は約218℃
home.oven.temperature = temp
home.oven.bake()
値型なので、同じtemp
変数を使っても、home.thermostat.temperature
とhome.oven.temperature
は別の変数のままです。構造体でinline可能なので、メモリ効率もパフォーマンスも良くなりました。
全部を値型にしましょう。
House
を構造体にして、構造体のthermostat
と構造体のoven
をもたせて、ミュータブルな変数として使いましょう。これでなにもかもが簡単になります。
必要なものだけミュータブルにする
let
でイミュータブルな常数を宣言できます。常数を破壊することはできません。
var
でミュータブルな変数が得られます。変更は局所的です。他の何かに影響することがなく、制御が容易です。例えば、スレッドをまたいで値型を渡しても競合したりしません。
競合の心配もない
値型なので、スレッドをまたいで競合したりしません。以下のような、参照型だったら競合が発生する場面でも、値型なら問題なく動作します。
var numbers: [Int] = [1, 2, 3, 4, 5]
scheduler.processNumbersAsynchronously(numbers) // (1)
for i in 0 ..< numbers.count {
numbers[i] = numbers[i] * i
}
sehceduler.processNumbersAsynchronously(numbers) // (2)
パフォーマンス影響は小さい
でもパフォーマンスはどうでしょう。上記の(1)や(2)で発生するコピーによるパフォーマンスの問題です。
このコピーは、基本型や構造体では決まった量のメモリコピーなので軽いです。
StringやArrayなどの可変長変数では、Copy on Writeをします。コピーの瞬間はカウンタを増やすだけなので軽いです。実際に変更が必要なときに、全体をコピーします。つまり、実装上は書き込みが起こるまで複数の変数が値を共有している状態になります。しかし、論理的には、完全に別の変数として扱えます。そしてパフォーマンスロスは小さくて済みます。
それでは、ビルに実際の例を紹介してもらいましょう。
実際の値型 (Value types in practice)
アップルのBill Dudney氏:
実例で値型の使い方を示します。値型を使って、円やポリゴンからなる図形を作りましょう。
円はこうです。全部値型です。
struct Circle : Equatable {
var center: CGPoint
var radius: Double
init(center: CGPoint, radius: Double) {
self.center = center
self.radius = radius
}
}
func ==(lhs: Circle, rhs: Circle) -> Bool {
return lhs.center == rhs.center && lhs.center == rhs.center
}
ポリゴンはこうです。もちろん値型です。
struct Polygon: Equatable {
var corners: [CGPoint] = []
}
func ==(lhs: Polygon, rhs: Polygon) -> Bool {
return lhs.corners == rhs.corners
}
次はこれらを組み合わせる図形型Diagram
を作りましょう。Diagram
では、Circle
もPolygon
も区別なく一つの配列に保持したいですね。どうしたら、Circle
とPolygon
を同じ配列に格納する事ができるでしょうか。
プロトコルで抽象化する
Swiftにおいて、その答えはプロトコルです。Swiftでは、値型を抽象化するのにプロトコルを使います。このようなプロトコルの使い方については、プロトコル指向のセッションもご参照ください。
Drawable
というプロトコルを作って、Circle
とPolygon
をそれに適合させれば、同じものとして扱えるようになります。
Drawable
は簡単です。draw()
メソッドがあるだけです。
protocol Drawable {
func draw()
}
Polygon
とCircle
に適用しましょう。draw()
でCore Graphicsに描画する処理を書きます。
extension Polygon : Drawable {
func draw() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.move(to: corners.last!)
for point in corners {
ctx.addLine(to: point)
}
ctx.closePath()
ctx.strokePath()
}
}
extension Circle : Drawable {
func draw() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
let path = CGMutablePath()
path.addArc(center: center, radius: CGFloat(radius), startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
ctx.addPath(path)
ctx.strokePath()
}
}
Diagram
は、Drawable
の配列を値型として持ち、Circle
やPolygon
を所有します。描画はそれぞれの描画を呼び出します。Diagram
自身もDrawable
です。
struct Diagram : Drawable {
var items: [Drawable] = []
mutating func add(item: Drawable) {
items.append(item)
}
func draw() {
for i in items {
i.draw()
}
}
}
add(item:)
メソッドは自分を変更するので、mutating
が必要です。
ここで、Diagram
の==
を作ろうとすると、コンパイラが許しません。
extension Diagram : Equatable {}
func ==(lhs: Diagram, rhs: Diagram) -> Bool {
return lhs.items == rhs.items // エラー:[Drawable]の==は未定義
}
この問題の解決方法については、プロトコル指向のセッションで解説しています。
さて、図形を作ってみましょう。
let points: [CGPoint] = [CGPoint(x: 50, y: 10), CGPoint(x: 10, y: 90), CGPoint(x: 90, y: 90)]
let polygon = Polygon(points: points)
let circle = Circle(center: CGPoint(x: 50, y: 50), radius: 20)
var d1 = Diagram()
d1.add(item: polygon)
d1.add(item: circle)
var d2 = d1 // d2はコピーされて新しい値型になっている。
d2.add(item:d1) // Diagramも入れられる。
d2.add(d2) // 自分を入れることさえできる。値型なので再帰にならない。
d2
を作成したとき、d1
の値がコピーされます。d1
とd2
は独立です。
d2
にd1
をadd()
すると、足されるのでポリゴンと円が2回ずつ描画されます。
d2
にd2
をadd()
すると、値型なのでd2
の値が足されます。この場合、d2.draw()
ではポリゴンと円が4回描画されるようになります。もし参照型だったら、d2.draw()
で無限に再帰呼び出しが発生してしまうでしょう。
値型と参照型を混ぜて使う (Mixing value types and reference types)
ここまでは、値型だけで構成されていました。次は、値型と参照型を混ぜて使う事を考えます。
ほとんどのケースで、参照型に基本型を入れて使っていたと思います。
class Button : UIControl {
var label: UILabel
var enabled: Bool
}
反対に、値型に参照型を入れる事もありました。
これはコピーすると参照をコピーするため、インスタンスを共有してしまいます。
struct ButtonWrapper {
var button: Button
}
このような内部に参照型を含む値型変数を扱うとき、考えることが二つあります。
- 参照型の変更をどうするか
- 参照型の比較をどうするか
参照型を含んでいても、値型としてきちんと振る舞うようにしたい。二つの値型が、内部で一つのインスタンスを参照しているかもしれないという状況を、どう処理すればいいでしょうか。
また、同値の判定にはどのような影響がでるでしょうか。
参照型がイミュータブルな場合は問題ない
イミュータブルなUIImage
を例に考えます。
struct Image : Drawable {
var topLeft: CGPoint
var image: UIImage
func draw() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.draw(image.cgImage!, in: CGRect(origin: topLeft, size: image.size))
}
}
このImage
構造体を使ってリソースを扱ってみます。
let image1 = Image(topLeft: CGPoint(x: 0, y: 0), image: UIImage(imageLiteralResourceName: "image.png"))
let image2 = image1
image1
とimage2
は値型なので独立していますが、内部でそれぞれ同じUIImageへの参照を保持していて、UIImageを共有しています。これは最初にみたオーブンの問題と同じ構造です。問題になりそうですね。
しかし、そうではありません。UIImageはイミュータブルで変更できないので、この状態でも問題は生じません。
参照型の同値判定には注意が必要
イコールの判定はどうでしょうか。
extension Image : Equatable {}
func ==(lhs: Image, rhs: Image) -> Bool {
return lhs.topLeft == rhs.topLeft && lhs.image === rhs.image
}
===
を使って、UIImageが同じオブジェクトかどうかを調べてみました。この例ではこれでも問題ありませんね。
しかし、もし、同じリソースからUIImageを二つそれぞれに作って持っていたらどうでしょう。別のオブジェクトなので、同値とはみなされなくなります。これは望ましい結果ではありませんね。
extension Image : Equatable {}
func ==(lhs: Image, rhs: Image) -> Bool {
return lhs.topLeft == rhs.topLeft && lhs.image.isEqual(rhs.image)
}
UIImage
については、NSObject
から継承したisEqual()
メソッドを使って、より適切な同値判定を行うことができます。
参照型のオブジェクトの同値判定は、それぞれに適切な処理が必要です。(値型はだいたい==
で比較すればいい)
参照型がミュータブルな場合はコピーが必要
ミュータブルなベジェ曲線を作りましょう。
struct BezierPath : Drawable {
var path = UIBezierPath()
var isEmpty: Bool { return path.isEmpty }
func addLine(to point: CGPoint) {
if isEmpty {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
func close() {
path.close()
}
func draw() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.addPath(path.cgPath)
ctx.strokePath()
}
}
これはうまくいきません。
addLine()
は自身を変更する事が明白ですが、mutating
をつけなくてもエラーになっていません。これは、変更するpath
が参照型だからです。これが異常のサインです。
BezierPath
変数を別の変数にコピーしたら、それらはUIBezierPath
を共有します。片方がaddLine()
をすると、参照しているUIBeierPath
を書き換えるので、もう一方の値も変わってしまいます。これは問題です。値型意味論になっていません。修正しなければなりません。
Copy on Write
修正するには、Copy on Writeをします。書き換える前に、値をコピーするようにします。
struct BezierPath : Drawable {
private var _path = UIBezierPath() // privateに変更
var pathForReading: UIBezierPath { return _path } // 読み出し用ゲッター
var pathForWriting: UIBezierPath { // 書き込み用ゲッター
mutating get {
_path = _path.copy() as! UIBezierPath
return _path
}
}
var isEmpty: Bool { return pathForReading.isEmpty } // 読み出し用を使う
mutating func addLine(to point: CGPoint) { // mutatingがないとエラーになる
if isEmpty {
pathForWriting.move(to: point) // 書き込み用を使う
} else {
pathForWriting.addLine(to: point) // 書き込み用を使う
}
}
mutating func close() { // mutatingがないとエラーになる
pathForWriting.close()
}
func draw() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.addPath(pathForReading.cgPath)
ctx.strokePath() // 読み出し用を使う
}
}
path
をprivate
にします。そして、ゲッターを2種類用意します。変更しないものと、変更するものです。変更する方は、変更してもいいように、コピーを行って新しい値を返します。これが、Copy on Writeです。
正しく実装したので、addLine()
にmutating
が必要だ、とコンパイラがエラーを出すようになります。素晴らしい仕組みですね。コンパイラに従いましょう。
使い方にも注意が必要です。例えばポリゴンをベジェ曲線に変換したくなったとします。
extension Polygon {
var path: BezierPath {
var result = BezierPath()
result.addLine(to: corners.last!)
for i in corners {
result.addLine(to: i)
}
result.close()
return result
}
}
ループの中にあるaddLine()
を呼ぶ度に不必要なコピーが行われるため、パフォーマンスがよくありません。こういうときは、参照型のUIBezierPath
を作り、最後に値型のBezierPath
にして返すようにします。
extension Polygon {
var path: BezierPath {
let result = UIBezierPath() // BezierPathをUIBezierPathにする。
result.move(to: corners.last!) // moveに直す
for i in corners {
result.addLine(to: i)
}
result.close()
return BezierPath(path: result) // BeierPathを作る。
}
}
isKnownUniquelyReferenced
Swiftにはユニークな参照かどうかを調べる機構があるので、これを使うとより効率の良いCopy on Writeを実装できます。スタンダードライブラリはこれを多用しています。
struct myWrapper {
var _object: SomeSwiftObject
var objectForWriting {
mutating get {
if !isKnownUniquelyReferenced(&_object) { // これ
_object = _object.copy()
}
return _object
}
}
}
Undoの例
最後にすごい例を紹介します。値型で、Undoを実装しましょう。
var undo: [Diagram] = []
var doc = Diagram()
var undo.append(doc)
doc.add(polygon)
undo.append(doc)
doc.add(circle)
undo.append(doc)
変更するたびに、undo
スタックに現在の値を積んでいきます。スタックに積んだ3つの値はそれぞれ独立していて、同じインスタンスを共有してはいません。
全部を値型で扱う限りは、Undoはundo
スタックの値を取り出して使うだけで済みます。
doc = undo[undo.count - n]
他にいろいろな事を考える必要はなく、非常に簡単にUndoができます。
PhotoshopはHistoryでこれを使っています。Photoshopは画面全体を小さいタイルに分割し、それぞれを値として扱います。変更されたタイルの情報だけが変更を示す値として保存されます。
まとめ
値型について話し、それが素晴らしい機能をもたらすという事をお伝えしました。参照型と比較して、値型が問題を解決する事をおみせしました。例題を使い、値型でみなさんのアプリに実装できるクールな機能をご紹介しました。これらがみなさんのアプリにどんな効果をもたらすか、それをみるのを楽しみにしています。
関連するセッションとして、プロトコル指向のセッションも是非ご覧ください。
感想
この記事を書くまで、プロトコル型の配列[Drawable]
は、てっきり参照型の配列のように思っていましたが、間違いでした。個々の変数が値型であれば、値型の配列として扱えます。なかなか凄い仕組みです。
プロトコル指向はなかなか面白いですね。
Swiftがますます気に入りました。