Closed7

ClassとStructの使い分けについて

ほへとほへと

まずは以下を読む。

https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes


デフォルトで構造体を選択する

構造体を使用すると、アプリ全体の状態を考慮することなく、コードの一部について簡単に判断できます。構造体はクラスとは異なり値型であるため、構造体へのローカルな変更は、アプリのフローの一部として意図的にその変更を伝えない限り、アプリの残りの部分には表示されません。その結果、コードの一部を見て、そのセクションのインスタンスへの変更が、関連する関数呼び出しから目に見えない形で行われるのではなく、明示的に行われることをより確信できます。

値型だから、変なところで変更処理が走らなくて済む。

Objective-C の相互運用性が必要な場合はクラスを使用する

Objective-C フレームワークで定義されている既存のクラス階層に適合させる必要がある場合は、クラスとクラス継承を使用してデータをモデル化する必要がある可能性があります。

Identityを制御する必要がある場合はクラスを使用する

Swift のクラスは参照型であるため、アイデンティティの概念が組み込まれています。つまり、2 つの異なるクラス インスタンスがそれぞれの格納プロパティに同じ値を持つ場合でも、アイデンティティ演算子 (===) によって異なると見なされます。また、クラス インスタンスをアプリ全体で共有する場合、そのインスタンスに加えた変更は、そのインスタンスへの参照を保持するコードのすべての部分に表示されます。インスタンスにこのようなアイデンティティを持たせる必要がある場合は、クラスを使用します。

class DatabaseConnection {
    var status: String

    init(status: String) {
        self.status = status
    }
}

let dbConnection1 = DatabaseConnection(status: "Connected")
let dbConnection2 = dbConnection1

dbConnection2.status = "Disconnected"

print(dbConnection1.status) // 出力: "Disconnected"
print(dbConnection1 === dbConnection2) // 出力: true

参照型の利点を使用した実装を考える場合に、構造体からクラスの可能性を考える。

以下のようにも記載があるので、まずは構造体の可能性を考えてから、
クラスにするべきなのだろうな。

ID は慎重に扱ってください。クラス インスタンスをアプリ全体で共有すると、ロジック エラーが発生する可能性が高くなります。頻繁に共有されるインスタンスを変更した場合の影響は予測できないため、そのようなコードを正しく記述するにはより多くの作業が必要になります。

アイデンティティを制御しない場合は構造体を使用する

struct PenPalRecord {
    let myID: Int
    var myNickname: String
    var recommendedPenPalID: Int
}

var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)

アプリ内でリモートデータベースとやり取りする際のデータの一貫性と安全性を確保する。

構造とプロトコルを使用して継承と共有の動作をモデル化する

クラス継承を使用して構築できる種類の継承階層は、プロトコル継承と構造を使用してモデル化することもできます。継承関係を最初から構築する場合は、プロトコル継承を優先してください。プロトコルでは、クラス、構造、列挙体が継承に参加できますが、クラス継承は他のクラスとのみ互換性があります。データをモデル化する方法を選択するときは、最初にプロトコル継承を使用してデータ型の階層を構築し、次にそれらのプロトコルを構造に採用してみてください。

class Animal {
    func speak() {
        print("Animal sound")
    }
}

class Dog: Animal {
    override func speak() {
        print("Bark")
    }
}

let myDog = Dog()
myDog.speak()  // "Bark"
protocol Animal {
    func speak()
}

struct Dog: Animal {
    func speak() {
        print("Bark")
    }
}

let myDog = Dog()
myDog.speak()  // "Bark"
ほへとほへと

構造体は、スタックで管理され取り出しが早くなり、
クラスは、ヒープで管理され取り出しが遅いとの記載を見たのでもう少し詳しく調べる。


⚠️以下はChatGPTに聞いた内容

Swiftのメモリ管理において、「ヒープ」と「スタック」は異なる種類のメモリ領域を指し、それぞれの役割と特徴があります。

スタック (Stack)

  • 使用目的: 関数の呼び出しやローカル変数の管理に使用されます。
  • 特性:
    • 高速アクセス: メモリアクセスが非常に高速です。
    • 自動管理: 関数が終了すると、自動的にスタックから変数が削除されます。
    • 構造: LIFO(Last In, First Out)方式で、最後に追加されたデータが最初に取り出されます。
    • サイズ制限: スタックのサイズは限られており、非常に大きなデータや長期的に保持する必要のあるデータには適していません。

ヒープ (Heap)

  • 使用目的: オブジェクトのインスタンスや動的に確保されるメモリの管理に使用されます。
  • 特性:
    • 柔軟なサイズ: 必要に応じて動的にメモリを割り当てることができ、大きなデータや長期間にわたるデータの保持に適しています。
    • 手動管理: メモリの解放を手動で行う必要がありますが、SwiftではARC(Automatic Reference Counting)によって自動的に管理されます。
    • 複雑なアクセス: スタックよりもメモリアクセスが遅く、メモリ管理が複雑です。

メモリ管理の実際の使用

  • ローカル変数: 関数内で定義されたローカル変数はスタックに格納されます。
  • オブジェクトのインスタンス: クラスのインスタンスなどの動的に生成されるデータはヒープに格納されます。

Swiftにおけるメモリ管理の特徴

  • ARC(Automatic Reference Counting):
    • SwiftはARCを用いてメモリ管理を自動化しています。ARCはオブジェクトの参照カウントを追跡し、参照がなくなったタイミングで自動的にメモリを解放します。
    • この仕組みにより、プログラマは手動でメモリを管理する負担が軽減されますが、循環参照(参照サイクル)には注意が必要です。

class MyClass {
    var value: Int
    
    init(value: Int) {
        self.value = value
    }
}

func myFunction() {
    let localVar = 10   // スタックに格納される
    let myObject = MyClass(value: 20)   // ヒープに格納される
    print(localVar)
    print(myObject.value)
}

myFunction()

この例では、localVarはスタックに格納され、一時的なデータです。一方、myObjectMyClassのインスタンスであり、ヒープに格納されます。myFunctionが終了すると、localVarはスタックから自動的に削除されますが、myObjectはARCによって管理されます。

ほへとほへと

値型はCopy on Writeで、
変更されるまで参照を保持しており、コピーはされないとの記載があった。

以下を読む。

https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd


つまり、変更操作のタイミングならいつでもデータ領域のコピーを行うわけではなく、そこには更に条件があります。すなわち、変更操作が生じたとき、データ領域が他のオブジェクトと共有されているならば、データ領域のコピーを行う、というのがより正確な CoW の動作になります。

このスクラップは4ヶ月前にクローズされました