Swift 文法その7 - Struct(構造体) -

Structは(構造体)はクラスと同様データを保持するクラスですが、その挙動には違いがいくつかあります。最近出た機能の SwiftUI、Codable などで Struct を使う機会が多くなっているので、使い分けを把握しておきましょう。

使い方

struct User {
    var id: String?
    var name: String
    var email: String
    var createdAt: Date
}

let user = User(name: "Taro", email: "test@example.com", createdAt: Date())

前回のクラスの時と同じようなデータを Struct で定義してみました。class キーワードを struct キーワードに変えたのと、イニシャライザの説明のためにプロパティの Optional 宣言も少し変えています。

Struct では、プロパティを非Optional で宣言した場合は自動的にイニシャライザが生成され、自分で init() を定義する必要がありません。自分でイニシャライザを書くこともできますが、その場合は自動生成されるはずの init() が使えなくなるので注意しましょう。

また、クラスでは定義されていた以下のメソッドが今回書いてありません。

func register(name: String?, email: String) {
    self.name = name
    self.email = email
    self.createdAt = Date()
}

Struct では「データは基本的に書き換えない」という特徴があります。なので、上記のようなプロパティの値を上書きするようなメソッドはエラーになります。どうしてもメソッドで自身のプロパティの値を上書きしたい場合は、mutating キーワードを func の前につけます。

mutating func register(name: String, email: String) {
    self.name = name
    self.email = email
    self.createdAt = Date()
}

プロパティに関しても、上記の例で let user = ... と、user を let で宣言していますが、この場合 user 内のプロパティ宣言をたとえ var で行っていたとしても、そのプロパティは上書きできません。

let user = User(name: "Sato", email: "test@example.com", createdAt: Date())
user.name = "Suzuki" // エラー

上書きしたい場合は user を var で宣言します。

var user = User(name: "Sato", email: "test@example.com", createdAt: Date())
user.name = "Suzuki"

デフォルト値のあるプロパティのイニシャライザの挙動

Swift 5.1 で仕様が変更になったので少しご紹介します。Swift5.1以前では、下記のようにデフォルト値のあるプロパティでもイニシャライザでそのプロパティを引数に渡すのが必須でした。

struct User {
    var email: String
    var name: String = ""
}


let userA = User(email: "a@example.com", name: "Sato")

Swift 5.1 では、デフォルト値のあるプロパティに関しては自動生成されるイニシャライザで引数に自動的に値がセットされるようになったので、name を渡してあげる必要がなくなりました。

struct User {
    var email: String
    var name: String = ""
}

// init(email: String, name: String = "") が自動生成されている
let userA = User(email: "a@example.com")

複数のクラスで Struct を使い回す場合の注意

Struct はどこかに代入されるときに元のデータをそのまま渡すのではなくコピーが渡され、元のインスタンスとは違うインスタンスとして使用することになります。下記の例で、AとBというクラスが同じ User の Struct を保持しているように見えますが、実際はそれぞれ異なるデータを参照しているので、最後の name の出力では異なる値になっています。

struct User {
    var name: String
}

class A {
    var user: User
   
    init(user: User) {
        self.user = user
    }
}

class B {
    var user: User
   
    init(user: User) {
        self.user = user
    }
}

var user = User(name: "")

let a = A(user: user)
let b = B(user: user)

a.user.name = "A"

print(a.user.name) // "A"
print(b.user.name) // ""

User が Struct ではなくクラスの場合は同じデータを参照したままになるので、最後の b.user.name に "A" が入っています。

class User {
    var name: String
   
    init(name: String) {
        self.name = name
    }
}

...

var user = User(name: "")

let a = A(user: user)
let b = B(user: user)

a.user.name = "A"

print(a.user.name) // "A"
print(b.user.name) // "A"

継承関係は持たない

クラスと違って Struct は継承関係を持ちません。ただし、protocol に準拠することは可能です(protocol については後日)。

おさらい

イニシャライザが自動生成される点は良いですが、あとはクラスではできて Struct ではできない点ばかりなので結局「クラスで良いのでは」と思うかもしれません。1つ経験談を言うと、クラスは代入されても参照するデータが同じなので、複数のファイル間で共有した時に知らないところで意図しない変更が加えられていて、デバッグに時間がかかった、というのがあります。必ずこっちじゃないとダメ、ということはないので、ご自身から見て都合の良い方を採用しましょう。

次回は「enum」についてご紹介します。

この記事が気に入ったらサポートをしてみませんか?