読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(7) - Classes and Structures

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Classes and Structures(クラスと構造体)

Swiftのクラスは基本的にファイル毎に分割する必要はない、とのこと。(原文:Swift does not require you to create separate interface and implementation files for custom classes and structures.)

必要がないだけなので分割してもいいんでしょうけどね。

そして注釈では「クラスのインスタンス=オブジェクトって認識があるだろうけど、Swiftのクラスと構造体は関数型っぽい(much closer in functionality than in other languages)とこがあるからインスタンスって呼ぶね」と書いてあります。関数型プログラミングを信奉している人はこの辺のことにマサカリを投げつけるのが大好き(※個人の印象です)なので、なるべくオブジェクトって言葉は使わないようにします。

Comparing Classes and Structures

まずはクラスと構造体の違いを明確にしましょう。二つが共通して持っている性質は以下の通りです。

  • プロパティを定義し、値を保持することが出来る
  • メソッドを定義し、それを提供することが出来る
  • サブスクリプト(所謂インデクサー)を定義し、アクセスする方法を提供することが出来る
  • イニシャライザーで初期化することが出来る
  • デフォルトの実装を拡張されること(Be extended to expand)が出来る(Extensionのことを言っているのだと思う。考え方はRuby特異クラスあたりが近いが、Swiftは本当に実装そのものを拡張出来るらしい。)
  • プロトコル(interfaceに近いもの。別の章が設けられているのでそこで解説する)に準拠したメソッドや振る舞いを提供する

上記の内容に加え、クラスは更に多くの性質を備えています。

  • 1クラスのみの継承(inherit)が可能であり、その性質を受け継ぐことが出来る
  • 実行時にインスタンスの型をチェックし、キャストすることが出来る
  • リソースの解放のためにインスタンスごとにDeinitializers(ひどい名前だ)を設定出来る
  • 参照カウントによってインスタンスへの複数参照を可能にする(?)

最後の参照カウント云々は訳に自信が全くないです。(原文:Reference counting allows more than one reference to a class instance.)多分、インスタンスを一つ作ると一つ参照カウントが増える、みたいな意味だと思うんですが。

ちなみに構造体の方は参照カウントを一切使わないようです。

宣言方法は以下の通りです。

class SomeClass {
    // class definition goes here
}

struct SomeStructure {
    // structure definition goes here
}

今更ながらコーディングスタイルに関する言及がされているんですが、型に関する名前(Class, Structure, Enum, Protocol, etc…)はUpperCamelCase、プロパティとメソッド(多分変数もだろう)はlowerCamelCaseを使うのが推奨されているそうです。

例としてこんな構造体とクラスが紹介されています。

struct Resolution {
    var width = 0
    var height = 0
}

class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

Swiftではこの手のものをメンバじゃなくてプロパティと呼んでるみたいですね。ってわけでプロパティにアクセスする方法。

// インスタンス取得
let someResolution = Resolution()
let someVideoMode = VideoMode()


println("The width of someResolution is \(someResolution.width)")
// prints "The width of someResolution is 0"


println("The width of someVideoMode is \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is 0"


someVideoMode.resolution.width = 1280
println("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is now 1280"

数多のC Likeな言語と同じようにSwiftも「.」でアクセスするタイプです。

また、構造体は自動でプロパティに対するイニシャライザ(memberwise initializer)が生成されるそうです。クラスは生成されません。

let vga = Resolution(width: 640, height: 480)

Structures and Enumerations Are Value Types

構造体や列挙体は値型なんだそうです。Swiftのデフォルトで定義されている型(Int, Double, Float, Bool, String, Array, Dictionary)は全て値型(構造体)です。

値型はインスタンスを何らかの変数に代入した後、別の変数に代入してもその時点のインスタンスのコピーを渡すだけです。(つまり、参照を渡すわけではない。)

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

cinema.width = 2048
println("cinema is now \(cinema.width) pixels wide")
// prints "cinema is now 2048 pixels wide"

// cinemaのwidthの値を変更しても、hd.widthの値は変わらない
println("hd is still \(hd.width) pixels wide")
// prints "hd is still 1920 pixels wide"

enum CompassPoint {
    case North, South, East, West
}
var currentDirection = CompassPoint.West
let rememberedDirection = currentDirection
currentDirection = .East

// currentDirectionの値をCompassPoint.EastにしてもrememberedDirectionはCompassPoint.Westのまま
if rememberedDirection == .West {
    println("The remembered direction is still .West")
}
// prints "The remembered direction is still .West"

Classes Are Reference Types

それに対してクラスは参照型です。

参照型はインスタンスを何らかの変数に代入した後、別の変数に代入するとその参照を渡します。

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

// alsoTenEightyにtenEightyの参照を渡す
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

// alsoTenEighty.frameRateの値を変更すると、参照元のtenEigtyの値も変更される
println("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// prints "The frameRate property of tenEighty is now 30.0"

あるインスタンスとあるインスタンスの参照が同じかどうかを判断するために、同値演算子(Identity Operators)が用意されています。

  • Identical to (===)
  • Not identical to (!==)
if tenEighty === alsoTenEighty {
    println("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
// prints "tenEighty and alsoTenEighty refer to the same Resolution instance."

同値演算子と違い、等値演算子(==)はインスタンスの内部の値を比較します。(と言っても、自作のクラスの場合はその挙動をオーバーロードしなきゃいけないわけだが。)

Choosing Between Classes and Structures

クラスを作成するべきか構造体を作成するべきかを判断するガイドラインとして、以下の内容が示されています。

  • 構造体の目的は比較的単純なデータ構造をカプセル化することにある
  • カプセル化された値がコピーされて渡されることを期待している
  • 構造体のプロパティとして保存されている値は(例えそれが参照型だとしても)値型となる(*1)
  • 構造体は他の型を継承することは出来ない

(*1:これはつまり、構造体の中に何らかのクラスを保持していても、ある構造体インスタンスから他の変数に代入する際に参照がコピーされるわけではないと言うことだろう。)

Assignment and Copy Behavior for Collection Types

先ほども言った通り、ArrayもDictionaryも値型です。が、Arrayはちょっと特殊な動きをします。どうもObjective-CのNSArrayとNSDictionaryをベースに書いているのが原因らしいんですが、よく知りません。と言うか、この挙動を見る限り、Arrayはほとんど参照型だと思って使った方がわかりやすいです。

Dictionaryは素直に値型っぽい動きをします。

var ages = ["Peter": 23, "Wei": 35, "Anish": 65, "Katya": 19]
var copiedAges = ages

copiedAges["Peter"] = 24
println(ages["Peter"])
// prints "23"

このようにcopiedAgesの値を書き換えても元のagesの値は変更されません。

何故か参照型のケースはコード例がないので、ちょっと自分で書いてみます。

class Hoge {
    var fuga = "fuga"
}

// Dictionary<String, Hoge>を作成
var fooDic = ["piyo": Hoge()]
println(fooDic["piyo"].fuga)
// print:"fuga"

// fooDicを取得したbarDicを作成
var barDic = fooDic

// barDic["piyo"]のfugaの値を書き換える
barDic["piyo"].fuga = "fugafuga"

// fooDic["piyo"] !== barDic["piyo"]なのでfooDic["piyo"].fuga!の値は変わらない
println(fooDic["piyo"].fuga)
// print:"fuga"

Arrayがどう特殊かと言うと、基本的に参照型っぽい動きをします。

// Array<Int>の作成
var a = [1, 2, 3]
var b = a
var c = a

println(a[0])
// 1
println(b[0])
// 1
println(c[0])
// 1

// a[0]の値を変更するとb[0], c[0]の値まで書き換わってしまう
a[0] = 42
println(a[0])
// 42
println(b[0])
// 42
println(c[0])
// 42

Arrayの要素数を変更すると、この参照が切れます。

// Array<Int> aに要素を一つ追加する
a.append(4)
a[0] = 777
println(a[0])
// 777

// aに要素が追加されたため、bとcの値は変わらない
println(b[0])
// 42
println(c[0])
// 42


この挙動を要素数を変更せず明示的に止めさせるためにArray#unshare()と言うとってつけたようなメソッドが用意されています。(Arrayをletで宣言している場合はunshareを呼び出すことは出来ません。

b.unshare()

b[0] = -105
println(a[0])
// 777
println(b[0])
// -105
println(c[0])
// 42


if b === c {
    println("b and c still share the same array elements.")
} else {
    println("b and c now refer to two independent sets of array elements.")
}
// prints "b and c now refer to two independent sets of array elements."

unshareはArray毎の参照は外せるんですが、Arrayが内部で保持している要素の参照はそのままです。

if b[0...1] === b[0...1] {
    println("These two subarrays share the same elements.")
} else {
    println("These two subarrays do not share the same elements.")
}
// prints "These two subarrays share the same elements."

強制的にArrayの値だけをコピーするArray#copy()と言うそのまんまなメソッドも用意されています。

var names = ["Mohsen", "Hilary", "Justyn", "Amy", "Rich", "Graham", "Vic"]
var copiedNames = names.copy()

copiedNames[0] = "Mo"
println(names[0])
// prints "Mohsen"

Array#copyは既に参照が外されているArrayでもArrayのコピーを作ってしまうので、単に参照を外したいだけならunshareを呼ぶべき、と書かれています。まぁ、そりゃそうだって感じですね。