Class và Struct trong Swift - Những điểm giống và khác nhau

Class và Struct trong Swift - Những điểm giống và khác nhau

Giới thiệu

ClassStruct là những thành phần code chính trong hầu hết mọi ứng dụng iOS. Chúng giúp chúng ta tổ chức và quản lý code thành những khối một cách trực giác và dễ dàng sử dụng. Trong ngôn ngữ Objective-C, Class và Struct thật sự rất khác nhau. Tuy nhiên điều này không đúng đối với Swift. Ví dụ như bạn có thể khai báo những thuộc tính và method tới cả Class và Struct trong Swift. Bên cạnh đó, Struct trong Swift cũng hỗ trợ việc hiện thực protocol. Như vậy, những gì khác nhau giữa class và struct? Khi nào chúng ta sử dụng class và khi nào sử dụng struct? Đây là những câu hỏi cơ bản quen thuộc trong những buổi phỏng vấn iOS. Ngày hôm nay, chúng ta sẽ làm rõ cách classstruct khác nhau như thế nào và cũng tìm hiểu thêm khi nào sử dụng chúng cho thích hợp.

Điểm giống nhau

ClassStruct trong Swift có nhiều điểm tương đồng như:

  1. Cho phép chúng ta khai báo những thuộc tính (property) để lưu trữ giá trị (//1), khai báo những phương thức (methods) để cung cấp chức năng(//2), khai báo subscripts để cung cấp khả năng truy cập giá trị của chúng sử dụng cú pháp subcript.
  2. Cho phép định nghĩa constructer (init) để khởi tạo giá trị ban đầu (//3)
  3. Cho phép mở rộng chức năng xa so với hiện thực mặc định ban đầu (//4)
  4. Cho phép hiện thực protocols để cung cấp những chức năng tiêu chuẩn của một loại hình nào đó (//4). Protocol trong Swift tương tự như interface trong ngôn ngữ lập trình Java. Một protocol bao gồm tên những hàm chưa được hiện thực chi tiết. Khi một class hay struct adopt protocol đó, nó phải hiện thực tất cả các hàm đó.
struct Bird{
    var code: Int //1
    var name: String //1
    
    init(code: Int, name: String) { //3
        self.code = code
        self.name = name
    }
    
    func introduce(){ //2
        print("I am \(name).")
    }
}

protocol Flyable{
    func fly()
}

extension Bird: Flyable { //4
    func fly() {
        print("I can fly.”)
    }
}

Trong blog này, mình sẽ không phân tích chi tiết về những điểm giống nhau giữa class và struct bởi vì hầu như mọi developer đều biết và sử dụng chúng hằng ngày. Mình sẽ tập trung phân tích chi tiết những điểm khác nhau giữa chúng.

Sự khác nhau

Class có những khả năng thêm mà struct không có như sau:

Tính thừa kế (Inheritance)

Classes hỗ trợ thừa kế trong khi Structs thì không. Thừa kế là một đặc tính không thể thiếu được trong lập trình hướng đối tượng. Nó cho phép một lớp thừa hường những thuộc tính và hành vi của lớp cha. Đoạn code dưới đây sẽ chứng minh điều đó.

class Vehicle{
    var manufacturer: String?
    let passengerCapacity: Int
    
    init(passengerCapacity: Int) {
        self.passengerCapacity = passengerCapacity
    }
}

class Car: Vehicle {
    var fuelType: String?
}

let car = Car(passengerCapacity: 4)

Trong ví dụ trên, lớp Vehicle là lớp cha của lớp Car. Điều này có nghĩa rằng lớp Car sẽ thừa hưởng những thuộc tính và hành vi của lớp Person. Dòng cuối cùng sẽ chứng minh điều này. Chúng ta khởi tạo instance của Car bằng việc sử dụng constructer init được định nghĩa trong lớp cha Person. Lỗi diễn ra khi một struct thừa kế từ struct khác.

Kiểu tham chiếu và kiểu giá trị (Reference Types vs. Value Types)

Kiểu giá trị (value type) là một kiểu mà giá trị của nó được copy khi nó được gán tới một biến hay một hằng, hoặc khi nó được truyền như thể là một parameter tới function. Không giống như kiểu giá trị, kiểu tham chiếu (reference types) không được copy khi chúng được gán tới một biến hay hằng, hoặc khi chúng được truyền tới một function. Thay vì copy giá trị, một tham chiếu (reference) tới instance đang tồn tại được sử dụng. Trong Swift, Struct là kiểu giá trị trong khi Class là kiểu tham chiếu. Ví dụ dưới đây sẽ chứng minh khái niệm trên.

struct Location{
    var longitude: Double
    var latitude: Double
    
    init(longitude: Double, latitude: Double) {
        self.longitude = longitude
        self.latitude = latitude
    }
}

var location1 = Location(longitude: 1.23, latitude: 1.23)
var location2 = location1

location1.longitude = 4.56

print(location1.longitude) //4.56
print(location2.longitude) //1.23

Chúng ta định nghĩa một struct Location để đóng gói dữ liệu bao gồm kinh độ (longitude) và vĩ độ (latitude) của một vị trí. Sau đó, chúng ta khai báo một biến là location1 bằng cách sử dụng constructer và truyền vào giá trị cho kinh độ và vĩ độ. Chúng ta khai báo thêm 1 biến location2 bằng cách gán giá trị hiện tại của location1 cho location2. Bởi vì Location là kiểu struct, một bản copy của instance hiện tại (location1) sẽ được làm và bản copy mới này được gán cho location2. Thậm chí mặc dù location1 và location2 bây giờ có cùng kinh độ và vĩ độ, chúng vẫn hoàn toàn là 2 instance khác nhau trên bộ nhớ. Sau đó, chúng ta thay đổi thuộc tính longitude của location1 tới 4.56 và in giá trị thuộc tính longitude của location1 và location2. Thuộc tính longitude của location1 cho thấy rằng nó đã bị thay đổi giá trị tới 4.56, trong khi thuộc tính longitude của location2 vẫn có giá trị cũ là 1.23.

Bây giờ cùng lặp lại ví dụ trên với class Location

class Location{
    var longitude: Double
    var latitude: Double
    
    init(longitude: Double, latitude: Double) {
        self.longitude = longitude
        self.latitude = latitude
    }
}

var location1 = Location(longitude: 1.23, latitude: 1.23)
var location2 = location1

location1.longitude = 4.56

print(location1.longitude) //4.56
print(location2.longitude) //4.56

Ở ví dụ trên, chúng ta khởi tạo instance của lớp Location với các thuộc tính ban đầu cho nó, sau đó gán location1 tới location2 và chỉnh sửa giá trị thuộc tính longitude của location1. Bởi vì những lớp này được truyền bởi tham chiếu, do đó location1 và location2 thật sự đầu cùng tham chiếu tới cùng một instance của Location trên bộ nhớ. Vì vậy khi thuộc tính longitude của location2 thay đổi thì thuộc tính longitude của location1 cũng đổi theo.

Toán tử đồng nhất thức (Identity Operators)

Bởi vì sự khác nhau giữa kiểu giá trị và kiểu tham chiếu, chúng ta nên cẩn thận khi tiến hành phép so sánh của chúng. Có hai khái niệm mà chúng ta cần quan tâm đó là: “identical to” and “equal to”. “identical to” (được trình bày bởi ba dấu bằng, ===) không có nghĩa giống như “equal to” (được trình bày bởi 2 dấu bằng, ==):

  1. “Identical to” có nghĩa rằng hai biến hoặc hằng của kiểu class tham chiếu tới chính xác cùng một instance của class.
class Car{}
let toyota = Car()
let lexus = toyota

toyota === lexus //true

let honda = Car()
honda === toyota //false
  1. “Equal to” có nghĩa rằng hai instance được xem như bằng nhau (equal) hoặc tương đương nhau (equipvalent) trong giá trị (value).
10 == 10 // true
"same string" == "same string"     // true
"one string" == "different string" // false

Deinitializer

Deinitializers cho phép instance của một class phải phóng bất cứ tài nguyên nào mà nó đã gán. Một hàm deinitializer được gọi ngay trước khi instance của một class được giải phóng (trả lại bộ nhớ đã được cấp phát tới ram). Bạn viết hàm deinitializer với từ khoá deinit, tương tự như cách hàm khởi tạo được viết với từ khoá init. Hàm deinit chỉ có sẵn trên kiểu class. Chúng ta cùng xem ví dụ dưới đây:

class D {
    deinit {
        //Deallocated from the heap, tear down things here
        print("Deallocated from the heap")
    }
}

var d:D? = D()
d = nil //Deallocated from the heap

Chúng ta định nghĩa một class D và khởi tạo instance của D. Bởi vì chúng ta sẽ gán biến d cho nil để kích hoạt hàm deinit nên chúng ta thêm optional dấu chấm hỏi sau phần khai báo như trên. Kế tiếp, chúng ta gán biến d tới nil. Tại điểm nãy, tham chiếu của biến d tới instance của D bị bẽ gãy và vì thế biến d được thu hồi để giải phóng bộ nhớ. Trước khi điều này diễn ra, hàn deinit được gọi tự động và in ra màn hình. Để hiểu rõ những trường hợp nào nên dùng hàm này, bạn có thể đọc về chủ để Deinitialization trong tài liệu The Swift Programming Language (Swift 4) ở link: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Deinitialization.html#//apple_ref/doc/uid/TP40014097-CH19-ID142

Chọn giữa Classes và Structs

Những instances của struct luôn luôn được truyền bởi giá trị và instance của class luôn luôn được truyền bởi tham chiếu. Điều này có nghĩa rằng chúng phù hợp cho mỗi loại hình công việc khác nhau. Tuỳ vào cấu trúc dữ liệu và chức năng bạn cần cho project mà quyết định nên sử dụng class hay struct.

Chúng ta xem xét việc tạo struct khi:

  1. Cấu trúc dữ liệu đơn giản, có ít thuộc tính
  2. Những dữ liệu được đóng gói sẽ được copy hơn là tham chiếu khi bạn gán hay truyền instance của struct đó.
  3. Những thuộc tính được lưu trữ bởi struct thì bản thân nó là kiểu giá trị.
  4. Struct không cần thừa kế thuộc tính hay hành vi từ bất kì kiểu khác.

Bây giờ hãy cùng xem một struct mà Apple định nghĩa:

public struct CGPoint {
    public var x: CGFloat
    public var y: CGFloat

    public init()
    public init(x: CGFloat, y: CGFloat)
}

Những lý do cho việc sử dụng struct cho CGPoint là :

  1. Cấu trúc dữ liệu đơn giản
  2. Hai thuộc tính là kiểu giá trị
  3. Nó không cần thừa kế bất kì lớp nào khác.

Cẩn trọng Trong Swift, nhiều kiểu dữ liệu cơ bản như String, Array và Dictionary thì được hiện thực như thể struct. Điều này có nghĩa rằng chúng thì được copy khi chúng được gán tới một hằng hay biến mới, hoặc chúng được truyền như thể là parameter tới hàm. Hành vi này thì khác xa so với những kiểu trong framework Foundation: NSString, NSArray và NSDictionary thì được hiện thực như class, không phải struct. String, Array và Dictionary trong Foundation luôn luôn được gán và truyền với một tham chiếu tới một instance đang tồn tại hơn là một bản copy.

Kết luận

Những vấn đề mình đã trình bày như trên là tất cả những gì về class và struct. Mình hi vọng bài viết này sẽ giúp bạn hiểu rõ những điểm khác nhau giữa class và struct và khi nào sử dụng chúng. Nếu bạn có bất cứ câu hỏi nào, vui lòng để lại comment bên dưới hay email tại địa chỉ: [email protected].