Những khái niệm đặc trưng trong Swift
Bài đăng này đã không được cập nhật trong 7 năm
Swift là một ngôn ngữ lập trình mới cho iOS, macOS, watchOS và tvOS apps, nó được xây dựng dựa trên những gì tốt nhất của C và Objective-C. Swift chứa những programming pattern an toàn và thêm vào nhiều tính năng mới giúp việc lập trình dễ dàng, linh hoạt hơn và thú vị hơn. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về những khái niệm cơ bản và đặc trưng trong ngôn ngữ lập trình Swift.
Enumerations
Enumerations hay còn gọi là enums, là một loại giá trị đặc biệt trong Swift, nó cho phép ta biểu diễn nhiều trường hợp giá trị khác nhau. Hãy cùng xem ví dụ sau:
enum Fruit {
case apple
case banana
case mango
case strawberry
}
Ở trên ta khai báo bốn loại trái cây, và bạn sẽ thấy rằng việc tạo enum vô cũng dễ dàng và hoàn toàn có thể sử dụng một cách đơn giản như String hay Int:
var favoriteFruit = Fruit.strawberry
Tuy nhiên, các bạn có thể thắc mắc tại sao không dùng mảng String để khai báo như trước đây:
let fruits = ["apple", "banana", "mango", "strawberry"]
let availableFruit = fruits[0]
Đối với enums, bạn chỉ cần gọi Fruit.apple là biết được giá trị của enums đó là gì, trong khi đối với array, việc gọi fruits[0] sẽ không cho bạn cái nhìn trực quan là giá trị nào đang được sử dụng. Ngoài ra enums sẽ giúp bạn giới hạn lượng giá trị có thể tồn tại, như trong ví dụ trên thì chỉ có bốn loại trái cây, ta có thể ngăn được việc truyền vào một loại trái mới không được cho phép. Còn đối với array, availableFruit có type là String nên ta không thể giới hạn việc sử dụng những giá trị string khác không hợp lệ.
Khi khai báo enums, tên của enums phải được viết hoa chữ cái đầu tiên và ở trạng thái số ít. Như vậy, fruit hay Fruits đều là những tên không theo chuẩn. Ngoài ra, các trường hợp của enums nên bắt đầu bằng chữ cái thường: apple, banana...
Enums được kết hợp với switch case để sử dụng:
let favoriteFruit = Fruits.strawberry
switch favoriteFruit {
case .apple:
print("Making apple cake")
case .banana:
print("Making banana ice cream")
case .mango:
print("Making mango juice")
case .strawberry:
print("Making strawberry jam")
}
Điều này cho phép ta viết các lệnh điều kiện tốt hơn câu lệnh if thông thường. Compiler sẽ bắt ta phải kiểm tra hết các trường hợp trong enums với câu lệnh switch case, để đảm bảo ta không bỏ sót một trường hợp nào, giúp đoạn code an toàn hơn.
Đến đây, hẳn bạn nghĩ rằng enums không thật sự đưa thêm chức năng nào mới cho Swift, nó chỉ giúp code an toàn hơn, và ta hoàn toàn có thể sử dụng String hay Bool để khai báo dữ liệu lưu trong enums. Bây giờ, hãy cùng tìm hiểu một trong những tính năng vượt trội của Swift enums, đó là các giá trị liên quan (associated values). Associated values cho phép ta lưu trữ các dữ liệu bổ sung bên trong một enums. Hãy cùng xem ví dụ sau:
enum Food {
case pizza
case spageti
case salmon
}
enum Juice {
case orange
case milk
case apple
}
enum Meal {
case breakfast(mainDish: Food, juice: Juice)
case lunch(mainDish: Food, desert: Fruit)
case dinner(mainDish: Food, desert: Fruit)
}
Associated values cho phép ta lưu trữ thông tin bổ sung một cách đơn giản. Bên trong các trường hợp của enum Meal, ta có thể lưu giá trị của các enums khác.
let meal = Meal.breakfast(mainDish: .pizza, juice: .orange)
Sử dụng switch case cho associated value cũng đơn giản như những trường hợp bình thường:
switch meal {
case .breakfast(let mainDish, let juice):
print("We gonna have \(mainDish) and \(juice) juice for breakfast")
case .lunch(let mainDish, let desert):
print("We gonna have \(mainDish) and \(desert) for lunch")
case .dinner(let mainDish, let desert):
print("We gonna have \(mainDish) and \(desert) for dinner")
}
Closures and Higher Order Functions
Closure là một khái niệm quan trọng trong Swift. Hiểu đơn giản, nó là một hàm không có tên. Swift closures có thể được gán cho biến như các loại dữ liệu khác. Closures còn được truyền vào function như là parameters.
func sampleFunction(_ stringParameter: String, closureParameter: (String) -> Void) {
closureParameter(stringParameter)
}
Phương thức trên có param là một string và một closure, trong function, closure được gọi đến như một function thông thường, nên nó còn được gọi là function không tên. Việc sử dụng cũng vô cùng đơn giản:
sampleFunction("Windy day", closureParameter: {(string) in
print(string)
})
Ngoài ra, ta còn có thể rút gọn cách sử dụng nếu closure là param cuối cùng của function như sau:
sampleFunction("Windy day") {(string) in
print(string)
}
Theo sau closures là một loạt các function được gọi là higher order functions (HOFs). HOFs là function sử dụng function khác như một parameter, khái niệm này thường xuất hiện trong kiểu collection trong Swift: map, filter, forEach, reduce và flatMap.
1. Map
Map được sử dụng để lặp qua một collection và áp dụng cùng operation lên từng phần tử trong collection. Map function trả về một array chứa kết quả của việc áp dụng mapping hoặc transform từng item.
let mapNumbers = [1, 2, 3, 4, 5]
let doubledMapNumbers = mapNumbers.map { $0 * 2 }
print(doubledMapNumbers) //prints [2, 4, 6, 8, 10]
Ví dụ trên demo việc nhân mỗi phần tử của mapNumbers với 2, thay vì viết vòng lặp for, ta có thể rút gọn trong một dòng lệnh. Map function chính là function sử dụng closure như một parameter mà nó đã rút gọn, parameter của closure không được đặt tên nên ta có thể sử dụng $0 để chỉ tới param đầu tiên, tương tự $1, $2... cho các param tiếp theo. Ta có thể viết rõ ra như sau:
let doubledMapNumbers = mapNumbers.map( {(number) in
return number * 2
})
2. Filter
Tương tự như map, filter sử dụng để chọn lọc ra những giá trị thoả mãn điều kiện đặt ra.
let filterNumbers = [1, 2, 3, 4, 5]
let filteredNumbers = filterNumbers.filter { $0 > 3 }
print(filteredNumbers) //prints [4, 5]
3. ForEach
forEach duyệt qua lần lượt từng phần tử và thực hiện câu lệnh tương ứng:
let forEachNumbers = [1, 2, 3, 4, 5]
forEachNumbers.forEach { print($0) } //In ra từng phần tử trên từng dòng riêng biệt
4. Reduce
let reduceNumbers = [1, 2, 3, 4 ,5]
let reducedNumber = reduceNumbers.reduce(0) { $0 + $1 }
print(reducedNumber) //prints 15
Sử dụng reduce để kết hợp tất cả các items trong một collection lại và tạo ra một giá trị mới. Trong ví dụ trên, ta cộng tất cả các số trong mảng reduceNumbers thành một. Param (0) được cung cấp trong reduce function là giá trị khởi tạo, reduce bắt đầu với giá trị khởi tạo và cộng tiếp các giá trị tiếp theo trong collection. Nếu ta truyền giá trị khởi tạo là 10, thì kết quả trong ví dụ trên là 25.
5. FlatMap
Cách dùng đơn giản nhất của flatMap chính là làm "phẳng" một collection của các collection:
let collections = [[5, 2, 7], [4, 8], [9, 1, 3]]
let flat = collections.flatMap { $0 }
// [5, 2, 7, 4, 8, 9, 1, 3]
Nó còn nhận biết được các giá trị nil và loại bỏ khỏi collection:
let flatMapNumbers = [1, nil, 2, nil, 3, nil, 4, nil, 5]
let flatMappedNumbers = flatMapNumbers.flatMap { $0 }
print(flatMappedNumbers) //prints [1, 2, 3, 4, 5]
flatMap duyệt qua từng phần tử trong collection và chỉ chọn những giá trị khác nil. Ngoài ra, nó còn có thể kết hợp với các functions khác một cách hiệu quả:
let collections1 = [[5, 2, 7], [4, 8], [9, 1, 3]]
let onlyEven1 = collections1.flatMap { $0.filter { $0 % 2 == 0 }}
// print [2, 4, 8]
Generics
Generics là một khái niệm thú vị trong Swift, nó cho phép ta tái sử dụng code cho những kiểu dữ liệu khác nhau. Hãy cùng xem ví dụ sau:
func swap(_ firstValue: inout Int, _ secondValue: inout Int) {
let temp = firstValue
firstValue = secondValue
secondValue = temp
}
Function trên thực hiện chức năng hoán đổi giá trị của hai giá trị kiểu Int. Tuy nhiên, nếu cần hoán đổi hai String thì ta lại phải viết một function khác với input là String. Thay vào đó, generics cung cấp một cách đơn giản hơn có thể áp dụng cho nhiều kiểu dữ liệu:
func swapAnything<T>(_ firstValue: inout T, _ secondValue: inout T) {
let temp = firstValue
firstValue = secondValue
secondValue = temp
}
Ta có thể sử dụng với nhiều input khác nhau:
var number1 = 1
var number2 = 2
swapAnything(&number1, &number2)
var string1 = "Monday"
var string2 = "Sunday"
swapAnything(&string1, &string2)
Value vs Reference Types
Khi tạo mới object2 và gán giá trị của object1 cho nó, reference types sẽ refer object2 tới object1, nghĩa là nếu giá trị của object1 thay đổi thì object2 cũng thay đổi theo, vì chúng cùng chia sẻ chung một giá trị. Trong khi đối với value type, object2 sẽ được copy ra một giá trị riêng độc lập với object1. Hãy cùng xem ví dụ sau:
// Reference type
NSString *myString = @"Day";
NSString *myOtherString = myString;
myString = @"Night";
NSLog(myOtherString); //prints "Night"
// Value type
var myString = "Don't change me"
let myOtherString = myString
myString = "Dun care, I changed"
print(myOtherString) //prints "Don't change me"
Trong Swift, struct là value type và class là reference type.
Protocols
Protocol định nghĩa ra các yêu cầu (methods, properties..). Class, struct hay enums thực thi protocol sẽ cung cấp phần thực thi cho các yêu cầu đó. Để phân biệt class và protocol, ta có thể hiểu class định nghĩa object là gì, còn protocol định nghĩa object sẽ làm gì. Hãy cùng xem ví dụ sau:
protocol Animal {
func makeSound()
func move()
}
struct Fish: Animal {
func makeSound() {
print("Huruzu")
}
func move() {
print("Swim under water")
}
}
struct Bird: Animal {
func makeSound() {
print("Liulo liulo")
}
func move() {
print("Fly above cloud")
}
}
Protocol Animal định ra ra các phương thức makeSound() và move(). Khi struct Fish và Bird implement protocol này (gọi là implemented types), nó cũng phải thực thi những methods bắt buộc của Animal. Điều này sẽ giảm thiểu việc implemented types bỏ sót việc thực thi các methods. Nếu không, compiler sẽ báo nó chưa thoả mãn yêu cầu của protocol. Ngoài ra ta có thể định nghĩa các optional methods, implemented type có thể hoặc không thực thi những methods này. Trên đây là những khái niệm cơ bản và đặc trưng trong Swift, hy vọng nó có thể giúp ích cho bạn khi tiếp cận ngôn ngữ thú vị này.
All rights reserved