【プログラミング初心者】Swift基礎~構造体・クラス~

型を定義する

以前Swiftにおける基本となる型について説明しました。
この基本型とif文とfor文さえあればプログラミングはできるのですが、これだけだとプログラムは煩雑になり管理しづらくなります。
そこである1つのデータや処理のまとまりを新しい型として定義することで全体の見通しをよくすることができます。
その型定義の方法に構造体クラスという手法があります。

構造体

構造体の定義

構造体を定義するための構文は以下となります。

struct 型名 {
   ...
}

もう少し具体的な実装をします。会社の構造を例にしてみましょう。
会社を表す型としてCompanyという構造体を作成します。
Company.swiftというファイルを作成し、構造体を定義します。

struct Company {
}

これでCompanyという構造体が定義されました。
実際にViewController.swiftなどから使ってあげましょう。
Intの実態は数値なので変数には数値を代入すればよかったのですが、構造体の実態はオブジェクトと呼ばれるデータのまとまりになります。
そのためIntとは少し違い作成する構文は構造体名()でオブジェクトを作成します。

override func viewDidLoad() {
    super.viewDidLoad()
    var company: Company = Company()
}

メンバ変数

さらにここからCompanyを表す属性を定義していきます。
会社を表すためにはどんな項目が必要でしょうか?
まず会社名、社長、あとは売り上げあたりを定義してみましょう。

struct Company {
    // 会社名
    var name: String
    // 社長
    var president: String
    // 売り上げ
    var earnings: Int
}

これでCompany型はname, president, earningsという会社を表す属性を持つことになります。
これらの構造体を表すために定義した変数のことをCompanyのメンバ変数、もしくは単にメンバと呼びます。

次にViewController.swiftをもう一度見てみましょう。
初期化の行でMissing arguments for parameters 'name', 'president', 'earnings' in callというエラーが発生しています。
「Companyに必要なメンバの実態がないので教えてください」というようなエラーです。
メンバを初期化時に入れてあげましょう。
エラーの左にある赤い[◎]をクリックし[Fix]を選択してあげると初期化のテンプレートをXcodeが作成してくれます。
メンバがある場合の初期化の方法は構造体(メンバ1: 値, メンバ2: 値, ...)です。

override func viewDidLoad() {
    super.viewDidLoad()
    var company: Company = Company(name: "株式会社山田商事",
                                   president: "山田太郎",
                                   earnings: 1000000)
}

さてこれで「株式会社山田商事」という会社が作られました。
メンバ変数を参照・更新してみましょう。
メンバへアクセスする構文は変数.メンバです。

var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1)
print(company.name)            // "株式会社山田商事"
print(company.president)       // "山田太郎"
print(company.corporateNumber) // 1000000

company.name = "山田次郎"
print(company.president) // "山田次郎"

またメンバ変数はさらに別の構造体でも問題ありません。

会社のすぐ下には部署がぶら下がっていると仮定し、Departmentという構造体をDepartment.swiftに定義します。
部署はとりあえず部署名と部長がいるだけにしましょうか。

会社の部署は複数の可能性があるのでDepartmentの配列で定義します。

struct Department {
    // 部署名
    var name: String
    // 部長
    var manager: String
}
struct Company {
    // 会社名
    var name: String
    // 社長
    var president: String
    // 法人番号
    var corporateNumber: Int
    // 部署
    var departments: [Department]
}
// 部署の作成
let developDepartment: Department = Department(name: "開発部",
                                               manager: "開発太郎")
let salesDepartment: Department = Department(name: "営業部",
                                             manager: "営業太郎")
// 会社の作成
var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [developDepartment, salesDepartment])

部署へのアクセスはメンバのメンバにアクセスするように.を繋げていきます。

// 登録した部署にアクセス
print(company.departments[0].name) // "開発部"

メソッド

構造体には振る舞い、つまり構造体が行う処理を定義することができます。
この振る舞いをメソッドと呼びます。メソッド定義の構文は以下となります。
(正確ではありませんが関数と呼ぶこともあります)

func メソッド名(引数) {
    ...
}

メソッドには引数と呼ばれるパラメータを与えることができます。
引数は引数名: 型というように(arg1: String, arg2: Int, ...)と定義します。

会社の振る舞いを考えてみましょう。
まず会社は売り上げをあげていくものなので、月の売り上げを加算するメソッドを定義します。

struct Company {
    // 会社名
    var name: String
    // 社長
    var president: String
    // 売り上げ
    var earnings: Int
    // 部署
    var departments: [Department]

    /// 売り上げを計上する
    /// - Parameter earnings: 月売り上げ
    mutating func sumUp(monthlyEarnings: Int) {
        self.earnings = self.earnings + monthlyEarnings
    }
}

少し注意が必要なのがfuncの前にmutatingというものがあります。
これは構造体特有のものですが、今回sumUpの中でearningsというメンバ変数を更新しています。
このようなメンバ変数を更新するメソッドを定義する場合は頭にmutatingをつけます。
mutatingがついたメソッドは構造体が変数、つまりvarを使って宣言されていなければならないというルールがあります。
配列appendremoveと同じです。
少し脱線しますが実は配列も構造体として定義されています。
appendなどの定義を見るとmutatingがついていることを確認できます。
配列構造体は配列自体はメンバ変数として保持しています。そのためappendを呼び出すとメンバ変数が更新されるためmutatingとして定義されているというわけです。

さてそれでは定義したメソッドを呼び出してみましょう。

var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [])

print(company.earnings) // 1000000

company.sumUp(monthlyEarnings: 1000000)
print(company.earnings) // 2000000

company.sumUp(monthlyEarnings: 200000)
print(company.earnings) // 2200000

これでメソッドを呼び出し値を更新できました。

メソッド化する意味

メソッド呼び出したことでcompanyの状態を更新できましたが、そんなことをしなくても更新できるのでは?と思う人もいるかもしれません。
そうです。先程紹介したメンバ変数にアクセスして更新する方法でも当然更新することができます。

print(company.earnings) // 1000000

company.earnings = company.earnings + 1000000
print(company.earnings) // 2000000

company.earnings = company.earnings + 200000
print(company.earnings) // 2200000

ではメソッド化することでどんなメリットがあるのでしょうか?

まず1つ目は処理に名前を付けられるという点があります。
メソッドとして定義することでsumUp(monthlyEarnings: Int)という処理ができ、メソッド名自体がどんな処理をするのか想像することができます。
これは変数に関しても同様のことが言えます。

2つ目は処理をまとめられるという点です。
先程のコードを見てください。簡単な処理ではありますがprint(company.earnings)が何度も出てきます。
このような共通した処理が複数行に渡るとコード自体がかなり冗長になります。
メソッド化しておくと一度書いておくだけで何度も呼び出せます。

/// 売り上げを計上する
/// - Parameter earnings: 月売り上げ
mutating func sumUp(monthlyEarnings: Int) {
    print("before: \(self.earnings)")
    self.earnings = self.earnings + monthlyEarnings
    print("after: \(self.earnings)")
    print("----------")
}
var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [])

company.sumUp(monthlyEarnings: 1000000)
company.sumUp(monthlyEarnings: 200000)
before: 1000000
after: 2000000
----------
before: 2000000
after: 2200000
----------

3つ目はコードの変更に強くなるということです。
結局は2つ目のメリットと同じようなものですが、例えば今は単純に売り上げを足しているところを消費税も合計に入れたいという要件が出たとします。
複数箇所で計算していた場合全ての処理を修正しなければいけません。
これをメソッドを呼び出して処理をしておくとsumUpの中身だけを修正すればよく、呼び出し元のViewController.swiftには全く触れる必要がありません。

mutating func sumUp(monthlyEarnings: Int) {
    print("before: \(self.earnings)")
    // ここだけを修正する
    let tax = Int(Double(monthlyEarnings) * 1.1)
    self.earnings = self.earnings + monthlyEarnings + tax
    print("after: \(self.earnings)")
    print("----------")
}
var company: Company = Company(name: "株式会社山田商事",
                               president: "山田太郎",
                               earnings: 1000000,
                               departments: [])

company.sumUp(monthlyEarnings: 1000000)
company.sumUp(monthlyEarnings: 200000)
before: 1000000
after: 3100000
----------
before: 3100000
after: 3520000
----------

このように構造体に処理を持たせておくことで、コードの可読性が上がりさらに変更に強いコードにすることができるのです。

クラス

クラスも構造体と似たようなものでメンバ変数とメソッドを持つデータ構造を表します。
違いは後で次項で説明するので本項では定義の仕方について説明します。

クラスの定義

クラスを定義するための構文は以下となります。基本的には構造体と同じです。

class クラス名 {
    ...
}

今度は人間クラスというものを定義し、オブジェクトを作ってみましょう。
作り方も構造体と同じです。

class Human {

}
var human = Human()

これで人間のオブジェクトを作成することができました。

クラスから作成されたオブジェクトのことは特にインスタンスと呼びます。
ですが経験上あまり明確に区別して話す機会はないように思うので基本的にはオブジェクトと言っておけば問題ないでしょう。

一応オブジェクト、インスタンスそれぞれの意味は

  • オブジェクト
    • プログラム上で扱うデータ全般。その実体化したデータを1つのモノとして見る考え方。
  • インスタンス
    • 何らかのクラスに基づいて実体化されたオブジェクト

となりますのでインスタンスもつまりはオブジェクトの一種です。
なのでインスタンスをオブジェクトと言っても誤りではないというわけです。

プロパティ/メソッド

クラスも構造体と同じようにメンバ変数とメソッドを持たせることができます。
厳密には違いますがクラスで定義したメンバ変数をプロパティと呼ぶととりあえずは覚えてください。

定義の仕方も同様です。
まずはプロパティを定義します。

class Human {
    // 名前
    var name: String
    //  年齢
    var age: Int
}

すると以下のようなエラーが発生します。

Class 'Human' has no initializers

「イニシャライザがありません」というようなエラーです。
イニシャライザとは初期化の際に使用する、少し特殊なメソッドです
別の言語ではコンストラクタといい、つまりクラスを構成する人というイメージです。

構造体のときはこのエラーは発生しませんでしたが実は構造体は暗黙的にイニシャライザを自動で持ってくれます。
ですがクラスの場合は明示的に作ってあげる必要があります。

イニシャライザの定義の仕方は色々ありますが今回は最も単純なイニシャライザを定義します。
基本構文はinit(引数)です。

class Human {
    // 名前
    var name: String
    //  年齢
    var age: Int

    /// 初期化
    /// - Parameters:
    ///   - name: 初期値となる名前
    ///   - age: 初期値となる年齢
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

呼び出し方はクラス名(引数)です。
あまりやりませんがクラス名.init(引数)でも呼び出せます。

var human = Human(name: "山田太郎", age: 25)
var humanInit = Human.init(name: "山田太郎", age: 25)

構造体のときと同じ生成の仕方ができました。
クラスも構造体もイニシャライザのみクラス名(引数)という呼び出し方ができます。

メソッドも構造体同様に定義でき、呼び出し方も同様です。

class Human {
    // 名前
    var name: String
    //  年齢
    var age: Int

    /// 初期化
    /// - Parameters:
    ///   - name: 初期値となる名前
    ///   - age: 初期値となる年齢
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    /// 話す
    func talk() {
        print("私の名前は\(self.name)です。")
    }
}
var human = Human(name: "山田太郎", age: 25)
human.talk()
私の名前は山田太郎です。

一点異なることはメソッド内でメンバ変数が更新される場合でもmutatingは不要となります。
ここは構造体と異なる点です。

/// 年齢を更新する
/// - Parameter age: 年齢
func update(age: Int) {
    self.age = age
}

このようにしてアプリで扱うデータをオブジェクトという人間がわかりやすい単位で管理していきます。

最後に

今回は新しく自分で型を定義するために構造体とクラスについて説明しました。
このようにオブジェクトベースで考えていく方法をオブジェクト指向プログラミングといいます。
どんなオブジェクトを作れば実装しやすいか、管理しやすいかを考えながら実装してみてください。

また構造体、クラスどちらも似たようなものですがそれぞれ値型参照型と言われメモリの確保のされ方が異なります。
そのあたりはまた別途記事にしていきます。

コメント

タイトルとURLをコピーしました