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 name
và age
:
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
Vì name
và age
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 name
và age
đượ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 name
và age
được đặt thành các tham số name
và age
được truyền vào initializer.