Bài 3.5 - Constant and Variable Scope - Develop in Swift - Fundamentals

Khi bạn viết những chương trình lớn và phức tạp hơn, bạn sẽ cần phải chú ý đến vị trí khai báo hằng số và biến số. Vị trí tối ưu trong code của bạn là ở đâu? Nếu bạn khai báo mọi biến số ở đầu chương trình, bạn có thể thấy code của mình khó đọc hơn và khó debug hơn rất nhiều.

Trong bài học này, bạn sẽ học cách viết code có cấu trúc tốt, dễ đọc. Bạn sẽ làm điều này bằng cách xác định phạm vi (scope) cho hằng số và biến số một cách hợp lý.

What You'll Learn

  • Phân biệt giữa phạm vi toàn cục (global scope) và phạm vi cục bộ (local scope).
  • Cách tạo biến số và hàm trong phạm vi toàn cục và phạm vi cục bộ.
  • Cách sử dụng lại tên biến số bằng kỹ thuật che khuất biến số (variable shadowing).

Vocabulary

  • Scope (Phạm vi): Phạm vi của một biến số hoặc hằng số xác định nơi nó có thể được truy cập và sử dụng trong code.
  • Global scope (Phạm vi toàn cục): Biến số hoặc hằng số được khai báo trong global scope có thể được truy cập từ bất kỳ đâu trong chương trình.
  • Local scope (Phạm vi cục bộ): Biến số hoặc hằng số được khai báo trong local scope chỉ có thể được truy cập trong khối code mà nó được khai báo (ví dụ: trong một hàm, vòng lặp, hoặc câu lệnh if).
  • Variable shadowing (Che khuất biến số): Xảy ra khi một biến số được khai báo trong local scope có cùng tên với một biến số trong global scope hoặc local scope bên ngoài. Biến số trong local scope sẽ "che khuất" biến số có cùng tên ở phạm vi bên ngoài, nghĩa là code trong local scope sẽ sử dụng biến số trong local scope.

Giải thích

Mỗi hằng số và biến số đều tồn tại trong một phạm vi nhất định, nơi nó có thể được nhìn thấy và truy cập. Có hai cấp độ phạm vi: toàn cục và cục bộ. Bất kỳ biến số nào được khai báo trong phạm vi toàn cục đều được gọi là biến toàn cục, và biến số được khai báo trong phạm vi cục bộ là biến cục bộ.

Phạm vi toàn cục đề cập đến code có sẵn từ bất kỳ đâu trong chương trình của bạn. Ví dụ, khi bạn bắt đầu khai báo biến số bên trong Xcode playground, bạn đang khai báo chúng trong phạm vi toàn cục. Sau khi bạn khai báo một biến số trên một dòng, nó sẽ có sẵn cho mỗi dòng sau đó. Bất cứ khi nào bạn hoàn thành việc nhập vào playground, code sẽ được thực thi từng dòng một, bắt đầu từ phạm vi toàn cục này.

Bất cứ khi nào bạn thêm một cặp dấu ngoặc nhọn ({ })—cho dù là cho struct, class, hàm, câu lệnh if, vòng lặp for, hoặc nhiều hơn nữa—khu vực bên trong dấu ngoặc nhọn xác định một phạm vi cục bộ mới. Bất kỳ hằng số hoặc biến số nào được khai báo bên trong dấu ngoặc nhọn đều được xác định trong phạm vi cục bộ đó và không thể được truy cập bởi bất kỳ phạm vi nào khác.

Ví dụ 1:

var age = 55

func printMyAge() {
    print("My age: \(age)")
}

print(age)
printMyAge()

Kết quả:

55
My age: 55

Trong ví dụ này, biến số age được khai báo ở đầu code và không nằm trong cấu trúc điều khiển luồng hoặc hàm. Điều này có nghĩa là nó nằm trong phạm vi toàn cục và có thể được truy cập trong toàn bộ chương trình. Hàm printMyAge có thể tham chiếu đến age, mặc dù nó không được truyền vào như một tham số. Tương tự, hàm printMyAge không được khai báo bên trong struct hoặc class, vì vậy nó nằm trong phạm vi toàn cục và do đó có thể được truy cập bởi dòng cuối cùng trong code.

Ví dụ 2:

func printBottleCount() {
    let bottleCount = 99
    print(bottleCount)
}

printBottleCount()
print(bottleCount) // Lỗi!

Biến số bottleCount được khai báo bên trong hàm printBottleCount, có phạm vi cục bộ riêng giữa các dấu ngoặc nhọn. Vì vậy, bottleCount nằm trong phạm vi cục bộ và chỉ có thể được truy cập bởi nội dung của hàm, bên trong dấu ngoặc nhọn. Dòng cuối cùng trong code sẽ báo lỗi, vì nó không thể tìm thấy biến số có tên bottleCount.

Ví dụ 3:

func printTenNames() {
    var name = "Grey"
    for index in 1...10 {
        print("\(index): \(name)")
    }
    print(index) // Lỗi!
    print(name)
}

printTenNames()

Trong code ở trên, name là một biến số cục bộ và có sẵn cho bất kỳ thứ gì được khai báo trong cùng phạm vi. Nó cũng có sẵn trong một phạm vi cục bộ nhỏ hơn: vòng lặp for trên dòng tiếp theo. Biến số index, mặc dù được khai báo bên trong hàm, nhưng được khai báo bên trong vòng lặp, có thể được coi là một phần nhỏ hơn được xác định trong phạm vi của hàm. Do đó, index chỉ có thể được truy cập bên trong vòng lặp. Vì print(index) xảy ra ngay bên ngoài vòng lặp, nên nó tạo ra lỗi.

Ví dụ 4 (Variable Shadowing):

func printComplexScope() {
    let points = 100
    print(points)

    for index in 1...3 {
        let points = 200
        print("Loop \(index): \(points+index)")
    }

    print(points)
}

printComplexScope()

Kết quả:

100
Loop 1: 201
Loop 2: 202
Loop 3: 203
100

Đầu tiên, points được khai báo và gán giá trị 100. Giá trị này được in ra trên dòng tiếp theo. Bên trong vòng lặp for, một biến số points khác được khai báo, biến số này có giá trị là 200. points thứ hai che khuất hoàn toàn biến số có phạm vi hàm, nghĩa là nó khiến points đầu tiên không thể truy cập được. Bất kỳ tham chiếu nào đến points sẽ truy cập biến số gần nhất với cùng phạm vi. Vì vậy, khi câu lệnh print bên trong vòng lặp được gọi, nó sẽ in giá trị 200 ba lần. Sau khi vòng lặp kết thúc, câu lệnh print sẽ in biến số points duy nhất mà nó có thể truy cập: biến số có giá trị là 100.

Để tránh nhầm lẫn không cần thiết trong ví dụ cụ thể này, bạn có thể đổi tên biến số points bên trong. Và có lẽ bạn sẽ đúng. Tuy nhiên, có một số trường hợp che khuất biến số có thể hữu ích. Hãy tưởng tượng bạn có một String? optional name và bạn muốn sử dụng cú pháp if let để thực hiện một số 작업 với giá trị của nó. Thay vì đặt một tên biến số mới, như unwrappedName, bạn có thể sử dụng lại name trong phạm vi của dấu ngoặc nhọn if let:

var name: String? = "Brady"

if let name = name {
    // name là một `String` cục bộ che khuất `String?` toàn cục có cùng tên
    print("My name is \(name)")
}

Bạn cũng có thể sử dụng che khuất biến số để đơn giản hóa việc đặt tên cho các optional đã được unwrapped từ câu lệnh guard.

func exclaim(name: String?) {
    if let name = name {
        // Bên trong dấu ngoặc nhọn, `name` là giá trị `String` đã được unwrapped
        print("Exclaim function was passed: \(name)")
    }
}

func exclaim(name: String?) {
    guard let name = name else { return }
        // name: `String?` không còn truy cập được nữa, chỉ có name: `String`
    print("Exclaim function was passed: \(name)")
}

Che khuất optional bằng giá trị đã được unwrapped là phổ biến trong code Swift. Hãy chắc chắn rằng bạn có thể đọc và hiểu mô hình phổ biến này.

Ví dụ 5 (Shadowing trong Initializer):

Bạn có thể tận dụng kiến thức của mình về che khuất biến số để tạo ra các initializer rõ ràng, dễ đọc. Giả sử bạn muốn tạo một instance của Person bằng cách truyền vào tên và tuổi làm hai tham số. Bạn cũng giả định rằng mọi instance Person đều có cả thuộc tính nameage:

struct Person {
    var name: String
    var age: Int
}

let francesco = Person(name: "Francesco", age: 35)
print(francesco.name)
print(francesco.age)

init(name: String, age: Int) {
    self.name = name
    self.age = age
}

Kết quả:

Francesco
35

nameage là tên của các tham số trong phạm vi hàm, nên chúng che khuất các thuộc tính nameage được khai báo trong phạm vi Person. Bạn có thể đặt từ khóa self trước tên thuộc tính để tham chiếu cụ thể đến thuộc tính đó — và để tránh nhầm lẫn mà che khuất biến số có thể gây ra cho trình biên dịch và người đọc. Cú pháp này làm cho rõ ràng rằng các thuộc tính nameage được đặt thành các tham số nameage được truyền vào initializer.