0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

値型と参照型 どちらを採用すべきか

Last updated at Posted at 2025-03-29

値型と参照型

どちらを選べばいいか、結論から言うと、
データを 独立させたいのか参照・共有したいのか で決定すればいい。

ただ正直、基本的なことをするなら 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
    }
}

その後、インスタンスを作成 + 値の変更を行うとする。

Structの結果
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
Classの結果
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 の選定が必要になってくる。

値型と参照型で違いが出てくるのは、以下のような場合である。

値型と参照型の挙動の違い

struct
var structSample1 = StructSample(person: "Taro", age: 10)
var structSample2 = structSample1
structSample2.person = "Hanako"

print(structSample1.person) // Taro(影響なし)

Struct は値型である。これは、コピー である。
var structSample2 = structSample1 とした時、sample2sample1 をコピー&ペーストされた状態であり、sample1sample2 は独立している。 sample2 を書き換えても、 sample1 には影響しないし、逆の操作でも同じく影響しない。

つまり、両者は全くの別物で、それぞれの値を持つため 値型 とされており、結びつきもなく、お互い影響し合わない。

class
var classSample1 = ClassSample(person: "Taro", age: 10)
var classSample2 = classSample1
classSample2.person = "Hanako"

print(classSample1.person) // Hanako(影響される)

Class は参照型である。これはエイリアスである。
var classSample2 = classSample1 とした時、sample2sample1 を参照した状態であり、 sample1sample2 は結びついている。

  • classSample1ClassSampleクラスのインスタンス
  • classSample2:インスタンスであるclassSample1を参照している。

英語の "alias" =「通称」「別名」と言う意味で、まさしく sample2sample1 の別名であり、これは 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の選定を適切に行いたい。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?