Help us understand the problem. What is going on with this article?

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

型を定義する

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

構造体

構造体の定義

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

struct 型名 {
   ...
}

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

Company.swift
struct Company {
}

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

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

メンバ変数

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

Company.swift
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: 値, ...)です。

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

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

ViewController.swift
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の配列で定義します。

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

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

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

メソッド

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

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

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

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

Company.swift
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として定義されているというわけです。

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

ViewController.swift
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の状態を更新できましたが、そんなことをしなくても更新できるのでは?と思う人もいるかもしれません。
そうです。先程紹介したメンバ変数にアクセスして更新する方法でも当然更新することができます。

ViewController.swift
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)が何度も出てきます。
このような共通した処理が複数行に渡るとコード自体がかなり冗長になります。
メソッド化しておくと一度書いておくだけで何度も呼び出せます。

Company.swift
/// 売り上げを計上する
/// - Parameter earnings: 月売り上げ
mutating func sumUp(monthlyEarnings: Int) {
    print("before: \(self.earnings)")
    self.earnings = self.earnings + monthlyEarnings
    print("after: \(self.earnings)")
    print("----------")
}
ViewController.swift
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には全く触れる必要がありません。

Company.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("----------")
}

ViewController.swift
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 クラス名 {
    ...
}

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

Human.swift
class Human {

}
ViewController.swift
var human = Human()

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

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

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

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

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

プロパティ/メソッド

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

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

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

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

Class 'Human' has no initializers

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

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

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

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

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

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

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

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

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

Human.swift
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)です。")
    }
}
ViewController.swift
var human = Human(name: "山田太郎", age: 25)
human.talk()
私の名前は山田太郎です。

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

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

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

最後に

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

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

本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
http://naoyalog.com/

euJcIKfcqwnzDui
フリーランスエンジニア。主にiOSアプリ開発を行う。 現在はReactNativeでiOS/Androidアプリを開発中。 Swift/Objective-Cネイティブ開発 VIPER、MVP、Redux ご連絡はTwitterのDMからお願いします。 本ブログに記述された見解は私個人の見解であり、所属する会社&組織の見解を反映したものではありませんのでご了承ください。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away