値型と参照型
どちらを選べばいいか、結論から言うと、
データを 独立させたいのか、 参照・共有したいのか で決定すればいい。
ただ正直、基本的なことをするなら class
でも struct
でもいい気がしている。
たとえば、内部のプロパティは同じで class
,struct
別で定義してみる。
struct StructSample{
var person:String
var age:Int
}
class ClassSample{
var person:String
var age:Int
init(person: String, age: Int) {
self.person = person
self.age = age
}
}
その後、インスタンスを作成 + 値の変更を行うとする。
var structSample1 = StructSample(person: "Taro", age: 10)
var structSample2 = StructSample(person: "Jiro", age: 12)
structSample1.person = "Kyoko"
structSample2.person = "Hanako"
print(structSample1.person) // Kyoko
print(structSample2.person) // Hanako
var classSample1 = ClassSample(person: "Taro", age: 10)
var classSample2 = ClassSample(person: "Jiro", age: 12)
classSample1.person = "Kyoko"
classSample2.person = "Hanako"
print(classSample1.person) // Kyoko
print(classSample2.person) // Hanako
得られる結果は struct
, class
ともに同じである。
簡単なことをするなら、どちらでも同じ結果が出るため、あまり気を使って選定しなくていいが、より複雑なことを想定しているなら、意図して struct
, class
の選定が必要になってくる。
値型と参照型で違いが出てくるのは、以下のような場合である。
値型と参照型の挙動の違い
var structSample1 = StructSample(person: "Taro", age: 10)
var structSample2 = structSample1
structSample2.person = "Hanako"
print(structSample1.person) // Taro(影響なし)
Struct
は値型である。これは、コピー である。
var structSample2 = structSample1
とした時、sample2
は sample1
をコピー&ペーストされた状態であり、sample1
と sample2
は独立している。 sample2
を書き換えても、 sample1
には影響しないし、逆の操作でも同じく影響しない。
つまり、両者は全くの別物で、それぞれの値を持つため 値型 とされており、結びつきもなく、お互い影響し合わない。
var classSample1 = ClassSample(person: "Taro", age: 10)
var classSample2 = classSample1
classSample2.person = "Hanako"
print(classSample1.person) // Hanako(影響される)
Class
は参照型である。これはエイリアスである。
var classSample2 = classSample1
とした時、sample2
は sample1
を参照した状態であり、 sample1
と sample2
は結びついている。
-
classSample1
:ClassSample
クラスのインスタンス -
classSample2
:インスタンスであるclassSample1
を参照している。
英語の "alias" =「通称」「別名」と言う意味で、まさしく sample2
は sample1
の別名であり、これは sample2
にアクセスされると、 sample1
を参照することと同じである。
そのため、 sample2
を変更とすると、 sample1
インスタンスが変更されることになる。
struct
を採用すべき時
- 値をコピーして独立して使いたい場合
詳細
ケース | 説明 |
---|---|
独立したデータを持ちたいとき | ユーザー情報、位置情報、商品のデータなど |
変更が共有されると困るとき | 他の部分に影響を与えたくないとき |
たとえば、アカウントのように沢山のインスタンスを想定し、それぞれ独立した値を持たせたい場合は、
struct User{
var name:String
var age:Int
}
のようにする。アカウントなどの個別で独立した情報は、値型である struct
を用いるといい。
var user1 = User(name: "Taro", age: 10)
var user2 = user1
user2.name = "Hanako"
print(user1.name) // Taro
print(user2.name) // Hanako
新たに Hanako
さんのデータを作成したいときは、素直に var user2 = User(name: "Hanako", age: 10)
とすればいいが、var user2 = user1
とした場合でも、値がコピーされるだけなので、意図せず user2
が変更されても、独立したデータ同士は影響を及ぼし合わないため、user1
には影響しない。安全である。
class
を採用すべき時
- 状態を共有したい
- ライフサイクルや参照が重要
- 継承を使いたい
詳細
ケース | 説明 |
---|---|
インスタンスの状態を共有したい | UIコンポーネント、データマネージャ、キャッシュなど |
ライフサイクル管理が必要 | メモリ管理(ARC)やタイミングを制御したいとき |
継承が必要 | あるクラスを基にして派生クラスを作りたいとき |
他からアクセスされる状態を持ちたい | 監視されるオブジェクトなど(例:ObservableObject) |
アプリの状態を表す UserManager
を想定してみた。現在ログイン中のアカウントを管理するなど、1つのアプリで1つだけ値を持つ場合などに参照型は有効である。
class UserMangaer{
static let shared = UserMangaer()
var currentUser:String = "Taro"
private init(){}
}
たとえばログイン中のユーザが2人いるというのはおかしい。そのため、このクラスはシングルトンで作成した。
シングルトンとは
1つのクラスに対して、アプリ内で1つしかインスタンスを作らせないようにする設計パターン。- 現在ログイン中のユーザー情報
- 設定の状態
- ネットワークマネージャー
- サウンドコントローラー
など、これらは「アプリ全体でひとつだけ」でいい。むしろ複数インスタンス作られても困る。
そのような時に活用できる設計パターンが「シングルトン」である。
インスタンスの作成はクラス名()
でできるが、複数のインスタンスを許可しないためにも、init()
に制限をかける。
class UserManager{
private init () { }
}
private
とすることで、UserManager
クラス外ではinit()
を使用できなくする。
次に、UserManager
クラス中に 自身のインスタンスを一つ作成しておく。
class UserManager {
static let shared = UserManager() // インスタンスは1つだけ。staticで定義
private init() {} // 外からは初期化できないようにprivate化
}
UserManager.shared
で、たった一つのインスタンス shared
にアクセスできる。
let sharedManager = UserManager.shared
このような設計にすることで、アプリ内であれば、どこからでも shared
で呼べ、どこからアクセスしても同じ情報を取得でき・変更があればアプリ全体でその変更が反映されるようになる。
static キーワード
static
キーワードを用いてshared
を定義するが、static
は「全インスタンスで共通の情報」を定義する際に便利である。また、static
を用いてプロパティを定義すると、アクセス時、インスタンス化が不要になる。
struct Roster{
static var company = "Apple.inc"
var name:String
}
社員個票用のクラスを作成した。
この時、company
,name
にアクセスしてみよう。
通常の定義をしたname
プロパティは、インスタンス化をしてから.name
でアクセスする。
let employee = Roster(name: "Sam")
let employeeName = employee.name
一方、static
で定義したcompany
プロパティはインスタンス化が不要である。
company
プロパティは全インスタンスで共通の値であるため、インスタンスごとに用意するのではなく、クラス自体にただ一つ用意されたプロパティとして扱う。
アクセス時はクラス名.プロパティ
でアクセスする。
let companyName = Roster.company
staticはクラスにくっついてるって分かってるけど、共通であればインスタンスからアクセスできても自然では?
たとえば以下は許可されていない。
let employee = Roster(name: "Sam")
let companyName = employee.company //インスタンスからアクセス->(エラー)
Samの例を見てみよう。
let sam = Employee(name: "Sam")
print(sam.companyName) // ← 「Samの会社の名前」-> うーん (ちなみにこう書くとエラーが出る)
これは一見できるが、「会社はSamのもの」と勘違いされる可能性がある。
実際は、全社員で共有する会社名を表しているため、クラス名からアクセスするべき。(swiftらしい)
さらに参照型である class
を使用しているため、
var sharedManager = UserMangaer.shared
var anotherReference = UserMangaer.shared
としてインスタンスにアクセスしたとして、anotheReference
にのみ値の変更をかけてみることにする。
anotherReference.currentUser = "Hanako"
くどいほど書いているが、class
は参照型なので、shared
に変更を加えると、sharedManager
にも変更がかかったように見える。かかったように見えるというのは、実際 sharedManager
を変更しているのではなく、sharedManager
は shared
を参照しているから、変更された shared
にアクセスしているだけである。
print(sharedManager.currentUser) // Hanako
print(anotherReference.currentUser) // Hanako
安全に class
を使う
class
を利用する際の怖いポイントは「意図せず書き換えられてしまう」ところである。
もし、struct
で定義すべきアカウント情報を class
で定義した場合、どのような危険性があるか。
struct User{
var name:String
var age:Int
}
これを
class User{
var name:String
var age:Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
としてみる。
このとき、struct
の時と同じように値のコピーを試みたとする。
var user3 = User(name: "Taro", age: 10)
var user4 = user3
user4.name = "Hanako"
出力結果は以下のようになる。
print(user3.name) // Taroとなって欲しいのに、Hanako
print(user4.name) // Hanako
UIKit
をメインに扱う場合、class
が基本になっているため、何気に class
を使用してしまいがちだが、
何気に行った操作が、意図していない結果を生む場合がある。
工夫する点
外部から直接プロパティ変更されたくない → private(set)
class
に存在するプロパティで、
勝手に書き換えられると困るが、見られてもOKなプロパティは、
外部から読み取りを許可し、書き込みを禁止するprivate(set)
を利用すると良い。
class Counter {
private(set) var count = 0
func increment() {
count += 1
}
}
これをCounter
クラスとは別のTest
クラスを用意して、count
プロパティへの読み込み・書き込みを試してみる。
class Test{
let counter = Counter()
func countCheck(){
print(counter.count) // 読み取りなのでOK
}
func changeCount(_ number:Int){
counter.count = number // エラー:書き込みは禁止
}
}
外部からは読み取りは可能なので、countCheck()
は呼び出し可能だが。changeCount()
はprivate(set)
で定義した読み取り専用プロパティに書き込むことはできないので、エラーが発生する。
このように、不要な・意図しない書き込みを防ぐため、外部からは読み取り専用にしてしまうことができる。
明示的にコピーしたいなら「手動コピー」する
class
では struct
のように値をコピーすることは容易でない。
直感的に書くと、以下のように
var instance2 = instance1
としてしまいがちだが、参照型ではコピーできないことを忘れてはいけない。
コピーを実現するには少し工夫が必要である。以下のように関数を用意することで実現可能である。
class User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func copy() -> User {
return User(name: self.name, age: self.age)
}
}
let user = User(name:"Taro",age:12)
let copyUser = user.copy()
まとめ
独立させたいのか , 参照・共有したいのかに応じて、static
,class
の選定を適切に行いたい。