Swift 構造体の使い方 - 基本定義, イニシャライザ, メソッドを扱う

はじめに

構造体とは値型のデータを構造的に持つものです。
特にSwiftの構造体は、その中でメソッドを作成出来たりと、クラスと何が違うねん!と思いますが、

構造体はクラスと違って代入や関数呼び出しの際、データの実態がコピーされ、新しいインスタンスが渡されるのが大きな特徴です。
つまり、構造体のデータの実態は、常にプログラム内のどこか1箇所から参照されているだけとなります。

この特性上、冒頭でかいた通り、構造体はデータという考え方で設計すると考えやすいです。

逆に、複数の箇所から同じ実体を参照したい場合はクラスを用いると良いでしょう。

今回は、構造体の使い方について取り上げます。

構造体を定義する

以下は構造体の定義の例です。

/// プレイヤーデータ
struct Player {
    let name: String  // 名前
    var hp: Int       // ヒットポイント
    var mp: Int       // マジックポイント
    var atk: Int      // 攻撃力
}

上記のように構造体は「struct」を使って表現します。
また、上記例で言う「name」などはプロパティと呼ばれ、変数「var」の場合は変更可能ですし、「let」の場合は一度値がセットされたら変更不可です。
ここは、通常の変数と定数と同じ扱いですね。

構造体をインスタンス化する

上記で記載した構造体を実際にインスタンス化して活用する例です。

/// プレイヤーデータ
struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
}

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
let priest = Player(name: "僧侶", hp: 60, mp: 50, atk: 10)

print(hero.name) // 「勇者」が出力
print(priest.name) // 「僧侶」が出力

インスタンス化は上述の通り

定数 or 変数 = 構造体名(イニシャライザで指定する引数)

で指定します。
ここで注意が必要なのは、先程構造体のプロパティで定数「let」が指定されていたら、1度セットした値は書き換えられなく、変数「var」が指定されていたら、後で書き換えることが可能とかきましたが、

インスタンス化するときに、定数で構造体を宣言すると、例え構造体のプロパティが変数であっても外部から書き換えはできません。

/// プレイヤーデータ
struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
}

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
let priest = Player(name: "僧侶", hp: 60, mp: 50, atk: 10)

hero.atk = 100 // ここでエラー

書き換えが発生する場合は、変数でインスタンス化をしておきましょう。

var hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)
hero.ark = 100 // これは問題なし

構造体の初期化 (イニシャライザ)

インスタンス生成のタイミングで値を全てセットする

構造体をインスタンス化するで記載した例だと、イニシャライザである「init」がありません。

この場合、構造体をインスタンス化する際に構造体のプロパティ名に対して値を渡してセットする事ができます。
例で言うと、

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)

上記の「勇者」「100」「20」「30」の部分がそうです。

構造体に予め値をセットしておく

構造体のプロパティに予め値をセットすることも可能で、以下がその例です。

struct Monk {
    let name: String = "モンク"
    var hp: Int  = 200
    var mp: Int  = 0
    var atk: Int  = 40
}

let monk = Monk()
print(monk.name)  // モンクが出力

データの初期値が定まっている場合は、このように初めから値をセットしておくと便利ですね。
また上述しましたが構造体のプロパティは、変数で定義しているものは後から変更が可能なので、必要に応じて書き換えれます。

monk.hp = 100
print(monk.hp) // 100が出力

構造体のプロパティに予め値をセットしていても、インスタンス化のタイミングでプロパティに値を別にセットすることも可能です。

struct Monk {
    let name: String = "モンク"
    var hp: Int  = 200
    var mp: Int  = 0
    var atk: Int  = 40
}

let monk = Monk(hp: 100, mp: 20, atk: 30)
print(monk.hp)  // 100が出力

上記例では、nameは定数で宣言しているので、変数部分を設定しています。

イニシャライザの定義 (init)

これまでは、予め値をセットするや、インスタンス化のタイミングで全項目の値をセットする形でしたが、
initを使うことで柔軟に初期値設定ができます。

以下の例では、initでキャラクターの設定を行っていますが、hp, mp, atkはランダムな値を加算するようにしています。
(ゲームでよくあるキャラクリエイトのイメージ

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init() {
        name = "勇者"
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }
}

このように「init」を用いる事で、初期化のタイミングで色々とやれることが増えます。

また、initで他のメソッドと同じように値を受け付けることも可能です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name  // selfは不要だが、引数と混合するのでつけてます
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")

ただ注意が必要なのは、「init」を用いると構造体をインスタンス化するの例に出した様に、

let hero = Player(name: "勇者", hp: 100, mp: 20, atk: 30)

この書き方をしたい場合は、自分でinitを用意しないといけません。
なので、イニシャライザのオーバーロードを行って、全てのプロパティの値を受け付けるようにすれば、これまでの書き方ができます。
以下はイニシャライザのオーバーロードを行った例です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name  // selfは不要だが、引数と混合するのでつけてます
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

またイニシャライザの処理の書き方にはルールがあり、すべてのプロパティの初期設定が完了するまで、構造体内のメソッドを使うことはできません。
(クラスと同様のルールですね。)

なので、以下はエラーになります。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        calAtk() // ここでエラー
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

上記は、init(name:) でatkの設定が終わっていないのに、calAtk()を呼び出そうとしているのでエラーとなります。
この場合は、以下のように、calAtk()を全ての設定が終わった後の場所に移すだけでエラーは消えます。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

var hero = Player(name: "勇者")
var priest = Player(name: "僧侶", hp: 80, mp: 60, atk: 30)

構造体でメソッドを使う

すでにこれまでの例で登場してきていますが、構造体にメソッドを定義することも可能です。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
}

上記のcalAtk()が構造体内のメソッドです。
ここで注意が必要なのは、構造体のメソッドでは基本はプロパティの値を変更できません。
なので以下のようにかくとエラーが発生します。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        calAtk()
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }

    /// ダメージを受けた時の処理
    func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

上記では、self.hp -= num の所でエラーが発生します。
これは大本の構造体の考え方が、データを格納するものだと言う所からきているのかもしれませんね。
ただし、swiftではメソッドに「mutating」のキーワードを付与することで、構造体のプロパティを変更することができます。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    
    init(name: String) {
        self.name = name 
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
    }

    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }

    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }

    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

構造体でGetter / Setterを使う

構造体でもGetter / Setterを使うことができます。

Getter / Setterを使って、これまでのPlayer構造体に歩数管理の「walk」プロパティを追加します。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    var walk: Int {
        get {
            return self.walk
        }
        set {
            walk += newValue
        }
    }
    
    init(name: String) {
        self.name = name
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        walk = 0
    }
    
    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
    }
    
    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
    
    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}

var hero = Player(name: "勇者")
hero.walk = 10
print(hero.walk) // 10が出力
hero.walk = 10
print(hero.walk) // 20が出力

上記のように

プロパティ名 {
    get {
        // 参照される時の処理
    }
    set {
        // 値がセットされる時の処理
    }
}

と記載する事で、プロパティの値をいじる時に専用の処理を記述することが可能です。
今回の処理はGetter / Setterを記載したかったのでかきましたが、このような計算を行う場合は、後述の通りプロパティオブザーバに書いたほうが良いでしょう。

構造体プロパティのwillSet / didSet (プロパティオブザーバ)

プロパティの値が変更される直前「willSet」、直後「didSet」に処理を呼ぶことができます。
先程のSetterの処理をwillSetに、didSetでは歩いていて、毒状態であればhpを減らすようにします。

struct Player {
    let name: String
    var hp: Int
    var mp: Int
    var atk: Int
    var walk: Int {
        willSet {
            self.walk += newValue
        }
        didSet {
            // 歩いた分だけダメージ
            self.hp -= walk - oldValue
        }
    }
    
    init(name: String) {
        self.name = name
        hp = 100 + Int(arc4random_uniform(10))
        mp = 30  + Int(arc4random_uniform(10))
        atk = 30 + Int(arc4random_uniform(10))
        walk = 0
        calAtk()
    }
    
    init(name: String, hp: Int, mp: Int, atk: Int) {
        self.name = name
        self.hp  = hp
        self.mp = mp
        self.atk = atk
        self.walk = 0
    }
    
    /// ここがメソッド
    /// 実際の攻撃力. 攻撃する際にある程度の乱数を発生させてダメージを算出する
    func calAtk() -> Int {
        return self.atk + Int(arc4random_uniform(10))
    }
    
    /// ダメージを受けた時の処理
    mutating func damage(num: Int) {
        self.hp -= num   // ここでエラー
    }
}


var hero = Player(name: "勇者")
hero.walk = 10
print(hero.walk)

これで歩く時の歩数と、歩いた際に毒のダメージを受ける処理が記載できました。
また上記例の様に、

  • willSetでは
    • 新しくセットされる値はnewValue
    • 既存の値は、walk
  • didSetでは
    • 新しくセットされた値はwalk
    • 既存の値は、oldValue

で表現できます。
ちょっとnewやらoldやらややこしいですが、will / didの言葉の意味で推測できますね。

さいごに

冒頭でも記載しましたが、構造体は一見クラスとほぼ同程度の機能が扱えるので、クラスを使うのか構造体を使うのかの判断がしにくい所があり、
わたし自身混乱してきたので、構造体はデータを取り扱うのであるを主眼にまとめてみました。
これまで記載した例のように、データを主眼に構造体を用いると、物凄く使いやすくプログラム自体がとてもスッキリするので是非マスターしたい所ですね。

written by ゆう@あんのうん