[Swift]「構造体」

この記事では、「構造体」の基本概念や使い方、構造体クラスの比較や初期化方法、拡張とプロトコル適合による機能追加など解説します。

目次

構造体とは?

構造体は、Swift言語において、関連するデータを一つのまとまりとして扱うための重要な仕組みとなります。
構造体を利用することで、複数のデータを一つのオブジェクトとしてまとめて管理することができます。

構造体の利点は多岐に渡ります。

構造体の利点
  • 構造体は値型であるため、データのコピーが作成されます。これにより、データの独立性が保たれ、意図しない副作用を回避することができます。
  • 構造体はイニシャライザメソッドを持つことができ、データの初期化や操作が容易になります。
  • 構造体は拡張プロトコル適合を活用することで機能を追加し、共有することができます。これにより、既存の構造体に新しいプロパティメソッドを追加することが可能となり、柔軟な機能拡張が実現します。

基本構文とインスタンス作成

STEP
基本構文

まず、構造体の基本構文は以下のようになります。

struct StructName {
    // プロパティやメソッドの定義
}
  • 構造体は、structキーワードを使用して定義されます。
  • StructNameの部分は、構造体の名前を指定します。このブロック内には、プロパティやメソッドの定義が含まれます。
STEP
プロパティ・メソッドの定義

構造体内でプロパティメソッドを定義するには、以下のようにします。

struct Rectangle {
    var width: Double
    var height: Double

    func calculateArea() -> Double {
        return width * height
    }
}
  • 上記の例では、Rectangleという構造体を定義しています。
  • widthheightという2つのプロパティがあります。
  • calculateAreaというメソッドは、構造体内のプロパティを使用して面積を計算します。
STEP
インスタンスの作成

構造体のインスタンスを作成するには、以下のようにします。

let square = Rectangle(width: 10, height: 5)
print(square.calculateArea())

//出力結果
50.0
  • Rectangle構造体のインスタンスrectangleという定数に代入しています。
  • widthには10、heightには5が設定されています。
  • calculateAreaというメソッドで、10 * 5の計算がされ、結果を返します
  • print文でメソッドの計算結果50.0を出力しています。

構造体とクラスの比較

構造体クラスは、Swift言語におけるデータ型を定義するための重要な要素です。
構造体クラスの違いとそれぞれの使い分けについて詳しく解説します。

STEP
init()メソッド
構造体クラス
init()メソッドが不要:
デフォルト値を削除して、データ型を指定する場合、構造体はinit()メソッドは必要ありません。
init()メソッドが必要:
デフォルト値を削除して、データ型を指定する場合、クラスはinit()メソッドが必要です。
構造体
struct Enemy {
    var health: Int
    var attackStrength: Int
}
クラス
class Enemy {
    var health: Int
    var attackStrength: Int

//init()メソッド必要   
    init(health: Int, attackStrength: Int) {
        self.health = health
        self.attackStrength = attackStrength
    }
}
STEP
値型/参照型、プロパティの変更
構造体クラス
値型:
構造体は値型であり、代入や引数の渡し方などが値そのもので行われます。
参照型:
クラスは参照型であり、インスタンスが格納されるのはメモリ上のアドレスです。
コピーが作成される:
構造体はコピーが作成されるため、一方のインスタンスの変更が他方に影響を与えることはありません。
参照が共有される:
クラスのインスタンスは参照が共有されるため、一方のインスタンスの変更が他方に反映されます。
プロパティの変更に制限がある:
メソッドの前にmutatingを付ける必要あり
var オブジェクト
プロパティの変更ができる:
→let オブジェクト
構造体
struct Enemy {
    var health: Int
    var attackStrength: Int
    
    mutating func takeDamage(amount: Int) {
        health = health - amount
    }
}

var skelton1 = Enemy(health: 100, attackStrength: 10)
let skelton2 = skelton1

skelton1.takeDamage(amount: 10)
print(skelton2.health)

出力結果:skelton2のhealthは、100
 →skelton1skelton2は別物であるから、skelton1が攻撃されてもskelton2の体力は減少しない

healthプロパティ
healthプロパティが変更されるtakeDamageメソッドの前にmutatingを付す必要がある。
さらに、オブジェクトの前にvarskelton1)を付す必要がある

クラス
class Enemy {
    var health: Int
    var attackStrength: Int
    
    init(health: Int, attackStrength: Int) {
        self.health = health
        self.attackStrength = attackStrength
    }
    
    func takeDamage(amount: Int) {
        health = health - amount
    }
}


let skelton1 = Enemy(health: 100, attackStrength: 10)
let skelton2 = skelton1

skelton1.takeDamage(amount: 10)
print(skelton2.health)

出力結果:skelton2のhealthは、90
 →skelton1skelton2は 同じであるから、skelton1(=skelton2)が攻撃されるとskelton2の体力は減少する。

healthプロパティ
healthプロパティが変更されても、修正事項はない。

初期化とデフォルト値の設定方法

構造体を使用する際には、適切な初期化デフォルト値の設定が必要です。

STEP
デフォルトイニシャライザ

構造体がデフォルトのプロパティ値を持っている場合、自動的にデフォルトイニシャライザが提供されます。
この場合、以下のように構造体のインスタンスを作成できます。

struct Person {
    // プロパティnameのデフォルト値を空の文字列に設定
    var name: String = "" 
    // プロパティageのデフォルト値を0に設定
    var age: Int = 0 
}
// Person構造体のインスタンスをデフォルト値で初期化
let person = Person() 
  • このコードでは、Person構造体内のnameageという2つのプロパティにデフォルト値が設定されています。
  • 具体的には、nameプロパティは空の文字列 "" で初期化され、ageプロパティは整数 0 で初期化されます。
  • その後、let person = Person() というコードで、Person構造体のインスタンスをデフォルト値を使って初期化しています。
  • ここで、引数を指定せずに Person() と書くことで、nameageプロパティはそれぞれデフォルトの空文字列と0で初期化されます。

この方法によって、新しいPersonインスタンスを作成する際にデフォルト値が使用されるため、プロパティの値を明示的に指定しなくてもよい便利な方法です。デフォルト値の設定によって、初期化時に不足したデータがあっても、プログラムがエラーなく進行することが保証されます。

STEP
メンバーワイズイニシャライザ

構造体の定義時にプロパティを指定してインスタンスを作成することができます。
これをメンバーワイズイニシャライザと呼びます。

struct Point {
    var x: Int
    var y: Int
}

let point = Point(x: 10, y: 20)
  1. コードの冒頭で、Point という名前の構造体が定義されています。
  2. この構造体は x と y という2つのプロパティを持っています。これらのプロパティは整数型(Int)です。
  3. let point = Point(x: 10, y: 20) の行によって、Point 構造体のインスタンスが生成されています。
  4. この行では、x には値 10 が、y には値 20 が渡されています。これによって、新しい Point インスタンスが作成されます。
  5. 提供されたコードでは、プロパティに直接デフォルト値が与えられていません。
  6. そのため、イニシャライザを介してプロパティに値を代入することで、インスタンスが適切に初期化されます。

このコードの例では、特定の値で Point インスタンスを初期化していますが、必要に応じて異なる値で初期化することも可能です。例えば、let anotherPoint = Point(x: 5, y: 8) のように異なる座標で新しいインスタンスを作成することができます。

STEP
カスタムイニシャライザ

構造体には、自分自身でカスタムイニシャライザを定義することもできます。カスタムイニシャライザを使用すると、より柔軟に初期化処理を行うことができます。

struct Size {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
}

let size = Size(width: 100.0, height: 200.0)
  1. Size 構造体は、widthheight という2つのプロパティを持っています。
  2. Size 構造体は、イニシャライザを定義しています。イニシャライザは新しい Size インスタンスを作成する際に呼び出される特殊なメソッドです。
  3. このイニシャライザは、widthheight という2つの引数を受け取り、それぞれのプロパティに値を設定しています。self キーワードは、そのインスタンス自体を指します。
  4. 最後の行で、let size = Size(width: 100.0, height: 200.0) として、Size 構造体のインスタンスを作成しています。
  5. ここでイニシャライザが呼び出され、引数として width100.0height200.0 が渡されます。
  6. これにより、新しい Size インスタンスが作成され、変数 size に格納されます。

デフォルト値の設定についても考慮されている場合、次のようになります。

    STEP
    カスタムイニシャライザ(デフォルト値あり)
    struct Size {
        var width: Double = 0.0
        var height: Double = 0.0
    
        init(width: Double, height: Double) {
            self.width = width
            self.height = height
        }
    }

    上記のようにデフォルト値を与えることで、引数なしで Size インスタンスを生成することができます

    デフォルト値が与えられている場合、引数を省略してイニシャライズできます。

    let defaultSize = Size()  
    // width = 0.0, height = 0.0

    これにより、引数を指定することなくデフォルト値を持つ Size インスタンスを作成できます。

    構造体の拡張とプロトコル適合

    構造体の拡張(Extension)

    構造体は、既存の機能に対して新しい機能を追加するために、拡張(Extension)を使用することができます。

    拡張(Extension)は、既存の構造体に対してプロパティメソッドを追加することができる仕組みです。

    基本形

    extension SomeType {
    //新機能の追加
    }

    例えば、以下のようなコードを考えてみましょう。

    事例
    構造体の拡張
    struct Point {
        var x: Int
        var y: Int
    }
    
    extension Point {
        func printCoordinates() {
            print("Coordinates: (\(x), \(y))")
        }
    }
    
    let point = Point(x: 10, y: 20)
    point.printCoordinates() 
    // 出力: Coordinates: (10, 20)
    1. コードの最初で、Point という名前の構造体が定義されています。
    2. この構造体は、2つの整数型プロパティ xy を持っています。
    3. extension Point は、既存の Point 構造体に新しい機能を追加するための拡張です。
    4. この拡張では、printCoordinates() というメソッドを定義しています。このメソッドは、構造体の xy プロパティの値を用いて座標を表示します。
    5. コードの最後で、Point 構造体を使用して point インスタンスを作成しています。
    6. このインスタンスの x プロパティには 10 が、y プロパティには 20 が設定されています。
    7. その後、point.printCoordinates() を呼び出すことで、インスタンスのメソッド printCoordinates() が実行されます。
    8. この結果、座標 (10, 20) がコンソールに出力されます。
    事例
    Doubleに小数点n位を四捨五入する機能追加
    extension Double {
        func round(to places: Int) -> Double {
            let precisionNumber = pow(10, Double(places))
            var n = self
            n = n * precisionNumber
            n.round()
            n = n / precisionNumber
            return n
        }
    }
    1. round(to places: Int) メソッドは、小数点以下の桁数を places という引数で指定します。
      例えば、123.4567.round(to: 2) のように使用されることを想定しています。
    2. pow(10, Double(places)) によって、指定された桁数に基づいた10の累乗値を計算します。
      例えば、places が2ならば、10^2 すなわち 100 となります。
    3. 与えられた Double 値(つまり拡張対象の値)を precisionNumber で掛け、その後 round() メソッドで四捨五入します。
      例えば、123.4567×100=12345.6712346
    4. 丸めた値を precisionNumber で割り、元の小数点以下の桁数に戻します。
      例えば、12346/100=123.46
    5. 丸められた数値を返します。
      例えば、123.46
    事例
    UIButtonに形状を円にする機能追加

    このコードは、UIButtonクラスに対して拡張(extension)を行い、新しいメソッドであるmakeCircular()を追加しています。このメソッドを呼び出すことで、UIButtonを円形にする処理が行われます。

    extension UIButton {
        func makeCircular() {
            self.clipsToBounds = True
            self.layer.cornerRadius = self.frame.size.width / 2
        }
    }
    1. extension UIButton { ... }: この行は、UIButtonクラスに対する拡張を宣言しています。
    2. これにより、UIButtonクラス自体のコードを変更することなく、新しい機能を追加できるようになります。
    3. func makeCircular() { ... }: 拡張内で定義されている新しいメソッドです。
    4. self.clipsToBounds = true:
      この行は、UIButtonのクリッピングを有効にするためのプロパティを設定しています。
      クリッピングが有効になると、ボタンの外にはみ出る部分が表示されなくなります。
    5. self.layer.cornerRadius = self.frame.size.width / 2:
      これは、ボタンの角を丸くするためにコーナーラジウスを設定しています。
    6. layer.cornerRadiusは、ボタンの角の半径を指定するプロパティで、ボタンが円形になるように設定しています。具体的には、ボタンの幅の半分を半径として設定しています。

    構造体のプロトコル適合

    また、構造体プロトコル(Protocol)に適合することもできます。

    プロトコル(Protocol)とは、共通のインターフェースを定義するための仕組みであり、他のオブジェクトと連携するための基準を提供します。

    プロトコル適合の基本形

    extension SomeProtocol {
    //メソッドの動作を定義
    }

    • これは、特定のプロトコルで定義されたメソッドやプロパティの実装を提供する拡張です。
    • つまり、SomeProtocol という名前のプロトコルがあり、そのプロトコルで定義されているメソッドやプロパティを拡張内で具体的に実装しています。
    • これにより、SomeProtocol に準拠するすべての型が、その拡張によって新しい機能を獲得します。
    プロトコル適合の基本形(全てのデータ型)

    extension SomeType: SomeProtocol {
    //新機能の追加
    }

    • これは、既存の型 SomeType に対して、SomeProtocol プロトコルを適合させるための拡張です。
    • SomeProtocol で定義されているメソッドやプロパティの実装を、この拡張内で提供しています。
    • この拡張により、SomeType 型は SomeProtocol プロトコルを満たす型として扱われ、SomeProtocol で定義されたインターフェースを持つことになります。

    例えば、以下のようなコードを考えてみましょう。

    protocol Drawable {
        func draw()
    }
    
    extension Point: Drawable {
        func draw() {
            print("Drawing point at (\(x), \(y))")
        }
    }
    
    let point = Point(x: 10, y: 20)
    point.draw() // 出力: Drawing point at (10, 20)
    • 上記の例では、Drawableというプロトコルを定義し、Point構造体にそのプロトコルを適合させています。
    • その結果、Point構造体のインスタンスであるpointは、Drawableプロトコルの要件を満たすようになります。
    • したがって、pointからdraw()メソッドを呼び出すことができます。

    次に、お天気アプリでプロトコル適合した実例を紹介します。こちらから確認ください。

    構造体の拡張プロトコル適合を組み合わせることにより、構造体に新たな機能を追加し、他のオブジェクトとの連携を容易にすることができます。これにより、コードの再利用性や柔軟性が向上し、アプリケーションの設計がより効果的になります。

    この記事が気に入ったら
    いいね または フォローしてね!

    よかったらシェアしてね!
    • URLをコピーしました!
    • URLをコピーしました!

    コメント

    コメントする

    目次