Struct và Class trong Swift

Swift Class vs Struct


1) Bức tranh tổng quát

  • Struct: kiểu giá trị (value type). Không kế thừa. Có memberwise initializer. Phương thức có thể mutating để sửa trạng thái. Không có deinit.
  • Class: kiểu tham chiếu (reference type). Kế thừa được. Có ARC, deinit, identity (===). Không có memberwise init tự động.
  • Protocol: cả struct và class đều conform. Kế thừa chỉ dành cho class; struct dùng protocol composition để mở rộng hành vi.

Gợi ý: Swift ưu tiên value semantics để dễ suy luận và an toàn luồng. Chỉ dùng class khi cần chia sẻ trạng thái sống lâu và đa nơi cùng tham chiếu.


2) Bảng so sánh nhanh

Tiêu chíStructClass
SemanticsGiá trịTham chiếu
Khởi tạoMemberwise init tự sinhKhông tự sinh nếu có stored property không default
Kế thừaKhôngCó đa cấp, final để khóa
ARCKhông áp dụng trực tiếp (trừ khi chứa reference)Có. Dễ tạo retain cycle
deinitKhông
mutatingCần khi đổi self hoặc propertyKhông cần
Gán/Truyền hàmSao chép theo giá trị (copy-on-write tối ưu)Sao chép tham chiếu
Đồng nhất (identity)Không có ======/!==
Thread-safetyDễ suy luận hơnCần đồng bộ hóa
Interop Obj‑CKhông trực tiếpTốt (kế thừa từ NSObject, @objc)

3) Giá trị vs Tham chiếu trong thực tế

struct Point { 
  var x: Int 
  var y: Int 
}
var a = Point(x: 0, y: 0)
var b = a        // copy giá trị
b.x = 10         // a không đổi

class Node { 
  var value: Int
  init(_ v: Int) { value = v } 
}
let n1 = Node(1)
let n2 = n1      // cùng tham chiếu
n2.value = 9     // n1.value == 9

  • Với struct, gán cho biến mới là bản sao logic. Swift tối ưu Copy‑on‑Write (CoW) cho các collection chuẩn như Array, Dictionary, String: chỉ copy khi có đột biến.
  • Với class, nhiều biến có thể trỏ tới cùng một đối tượng, thay đổi ở một nơi ảnh hưởng nơi khác.

4) let vs var và từ khóa mutating

struct Counter {
    var count = 0
    mutating func inc() { count += 1 }
}

var s = Counter()
s.inc()          // OK
let s2 = Counter()
// s2.inc()      // Lỗi: s2 là hằng số, không được mutate

class Box { var v = 0 }
let c = Box()
c.v = 1           // OK: tham chiếu hằng, nhưng trạng thái bên trong vẫn đổi

  • let trên struct khóa toàn bộ giá trị. mutating buộc phải khai báo khi method thay đổi self hoặc property.
  • let trên class reference không khóa nội dung; nó chỉ khóa bản thân tham chiếu.

5) ARC, retain cycle, và capture list

  • Class dùng ARC để quản lý vòng đời. Dễ tạo retain cycle khi hai object giữ mạnh lẫn nhau hoặc khi closure giữ self mạnh.
class A {
    var f: (() -> Void)?
    func setup() {
        f = { [weak self] in self?.doWork() }
    }
    func doWork() {}
}

  • Struct không có ARC trực tiếp, nhưng nếu struct chứa property là reference type thì vòng đời vẫn phụ thuộc vào ARC của những phần tử đó.

6) Kế thừa vs Protocol Oriented

  • Class hỗ trợ kế thừa, override, final, required, convenience. Có two‑phase initializationdeinit.
  • Struct không kế thừa nhưng conform protocol rất linh hoạt. Mô hình Protocol‑Oriented Programming khuyến khích value type + extension + default implementation.

7) Khởi tạo

struct User { var id: Int; var name: String } // có memberwise init tự sinh

class Person {
    let id: Int
    var name: String
    init(id: Int, name: String) { self.id = id; self.name = name }
}

  • Struct: có memberwise initializer nếu không tự định nghĩa initializer khác.
  • Class: nếu có stored property không có giá trị mặc định, bạn phải viết designated initializer.

8) So sánh bằng == vs đồng nhất ===

struct S: Equatable { let x: Int }
let s1 = S(x: 1), s2 = S(x: 1)
print(s1 == s2)  // true

class C { let x: Int; init(_ x: Int) { self.x = x } }
let c1 = C(1), c2 = C(1)
print(c1 === c2) // false: khác instance

  • == so sánh giá trị. === so sánh đồng nhất tham chiếu và chỉ có ở class.

9) Hiệu năng và bộ nhớ

  • Value type thường nhỏ, bất biến, dễ tối ưu, dễ suy luận về chi phí copy.
  • Không mặc định đồng nhất “struct trên stack, class trên heap”. Trình biên dịch có thể tối ưu bố trí bộ nhớ. Điều quan trọng là semantics.
  • Collection chuẩn dùng CoW để tránh copy thừa. Mutation mới kích hoạt copy.

10) Khi nào chọn Struct, khi nào chọn Class

Chọn Struct khi:

  • Dữ liệu mô tả giá trị thuần, không cần chia sẻ tham chiếu.
  • Muốn an toàn luồng và dễ test.
  • Trạng thái nhỏ, bất biến, hoặc thay đổi theo copy.
  • Ví dụ: model bất biến, DTO, Point, Range, Money, User thuần dữ liệu.

Chọn Class khi:

  • Cần chia sẻ trạng thái chung giữa nhiều nơi.
  • Cần kế thừa, polymorphism theo class.
  • Cần ARC, deinit, hoặc interop Obj‑C.
  • Ví dụ: UIViewController, nguồn dữ liệu stateful, cache, coordinator.

Lưu ý: SwiftUI dùng struct View để mô tả UI bất biến; state được tách khỏi View qua @State, @ObservedObject (class), @StateObject (class), @EnvironmentObject.


11) Các bẫy thường gặp

  1. Dùng class cho mọi thứ → khó kiểm soát side‑effect, dễ race condition.
  2. Quên [weak self] trong closure giữ dài hạn → retain cycle.
  3. Nghĩ rằng let trên class chặn mọi thay đổi → sai; chỉ khóa tham chiếu.
  4. Tự viết CoW sai cách cho struct tự thiết kế → copy không như kỳ vọng.
  5. Kỳ vọng deinit ở struct → không có.

12) Ví dụ CoW tối giản

struct CowBuffer {
    private class Storage { var data: [Int] = [] }
    private var storage = Storage()

    var data: [Int] { storage.data }

    // ensure unique before write
    private mutating func makeUnique() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage()
        }
    }

    mutating func append(_ x: Int) {
        makeUnique()
        storage.data.append(x)
    }
}

13) Liên quan đến Concurrency

  • Value type giúp tránh shared mutable state giữa task. Truyền dữ liệu theo copy.
  • Shared state cần cô lập: dùng class + actor hoặc queue riêng. actor là reference type an toàn luồng ở mức ngôn ngữ, nhưng khác chủ đề bài này.

14) Bộ câu hỏi phỏng vấn mẫu và gợi ý trả lời

  • Khác biệt cốt lõi giữa struct và class?
    • Struct là value type, copy khi gán/truyền; không kế thừa, không deinit. Class là reference type, có kế thừa, ARC, deinit, identity.
  • Tại sao Swift ưu tiên value semantics?
    • Dễ suy luận, ít side‑effect, an toàn luồng hơn, dễ test.
  • let khác nhau thế nào với struct và class?
    • let struct khóa toàn bộ giá trị, không gọi method mutating. let class chỉ khóa tham chiếu, vẫn đổi property var bên trong.
  • Copy‑on‑Write là gì?
    • Trì hoãn copy đến lúc mutate. Array, Dictionary, String áp dụng. Giảm chi phí copy không cần thiết.
  • Khi nào chọn class thay vì struct?
    • Cần chia sẻ trạng thái, kế thừa, deinit, interop Obj‑C, hoặc vòng đời do ARC.
  • Làm sao tránh retain cycle với closure?
    • Dùng capture list [weak self] hoặc [unowned self] tùy vòng đời.
  • === cho struct không?
    • Không. === chỉ cho class. Struct so sánh bằng == nếu conform Equatable.
  • Struct có kế thừa không?
    • Không. Dùng protocol để mở rộng hành vi.
  • Có thể có deinit trong struct?
    • Không. Chỉ class có.
  • Memberwise initializer là gì?
    • Khởi tạo được auto‑generate cho struct khi không có custom init.
  • Hai‑giai‑đoạn khởi tạo (two‑phase init) áp cho loại nào?
    • Class. Đảm bảo tất cả stored property được khởi tạo trước khi dùng.
  • Tại sao mutating tồn tại ở struct?
    • Vì method có thể thay đổi self là giá trị. Ngôn ngữ yêu cầu khai báo rõ.
  • Thread safety giữa struct và class?
    • Struct dễ an toàn hơn do copy semantics. Class cần đồng bộ khi chia sẻ.
  • SwiftUI vì sao dùng struct cho View?
    • View là mô tả thuần. Dễ diff, rẻ khi copy, tối ưu render.

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

retain_cycle_swift

Retain Cycle trong Swift

1) ARC nền tảng 2) Retain cycle: hai mô thức phổ biến 2.1 Object ↔ Object 2.2 Closure ↔ Object 3) weak vs unowned Thuộc tính weak unowned Loại Optional…

Xem thêm

Unit Test cho thư viện CocoaPods bằng Podspec

Trong bài viết này, bạn sẽ học cách tạo một thư viện CocoaPods, cấu hình Unit Test sử dụng podspec, cách tích hợp vào dự án chính, và cách chạy…

Xem thêm

Unit Test trong Swift với Quick và Nimble

Trong phát triển phần mềm, đảm bảo chất lượng mã nguồn thông qua kiểm thử tự động là một bước quan trọng. Bài viết này sẽ hướng dẫn bạn cách…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận