(Bài viết được chia sẻ bởi Chris Eidhof chuyên gia lập trình ứng dụng iOS và MAC đang sống tại Berlin, Đức)
Trước khi viết ra bất kỳ điều gì, tôi phải thừa nhận rằng, tôi đang rất phấn khích. Tôi yêu ngôn ngữ lập trình Swift. Tôi nghĩ đó là thứ tốt nhất đã được sinh ra cho hệ sinh thái Cocoa từ trước đến nay. Tôi muốn bạn cũng cảm nhận được sự tuyệt vời đó bằng cách chia sẻ kinh nghiệm của tôi với Swift, Objective-C và cả Haskell nữa. Bài viết này chưa phải là những ví dụ ứng dụng tốt nhất (bởi tại thời điểm viết, Swift còn quá mới để đưa ra được kết luận như vậy), nhưng hơn hết đó là những ứng dụng mà Swift thực sự tỏa sáng.
Giới thiệu về bản thân một chút: Trước khi trở thành lập trình viên chuyên nghiệp trên iOS và OS X, tôi đã có thời gian dài làm việc với Haskell(là ngôn ngữ lập trình khác). Tôi vẫn nghĩ đó là một trong nhưng ngôn ngữ lập trình tuyệt vời nhất mà tôi đã trải nghiệm. Tuy nhiên, tôi đã chuyển sang Objective-C bởi vì tôi đã tin rằng iOS là môi trường làm việc thú vị nhất tôi đã trải qua. Phải thừa nhận rằng, lúc mới bắt đầu, nó đã làm tôi có chút bối rối, thậm chí nản lòng nhưng tôi vẫn quyết tâm học và đã yêu nó.
Khi Apple công bố Swift tại WWDC, tôi đã thực sự bị phấn khích. Tôi chưa bao giờ như thế khi có bất cứ công nghệ mới nào được công bố. Sau khi nghiên cứu tài liệu, tôi nhận ra rằng Swift cho phép chúng ta sử dụng lại toàn bộ nội dung của lập trình chức năng nhưng vẫn tích hợp liền mạch các Cocoa APIs. Tôi nghĩ việc kết hợp của hai đặc điểm này là rất đặc biệt và duy nhất từ trước đến nay. Nhìn lại ngôn ngữ Haskell, nó rất khó để gọi Objective-C APIs, còn Objective-C thì lại rất khó để làm lập trình chức năng.
Ví dụ thể hiện sức mạnh của Swift:
Một trong những tính năng nổi bật của Swift là sử dụng các tùy chọn. Các tùy chọn cho phép chúng ta xử lý các giá trị có thể tồn tại hoặc không. Nhưng trong Objective-C thì ngược lại, chúng ta phải chính xác việc có hay không có giá trị được phép tồn tại. Với các tùy chọn, chúng ta chuyển trách nhiệm cho hệ thống. Nếu bạn có giá trị tùy chọn, nó có thể sẽ không có giá trị. Hoặc nếu bạn có giá trị mà không phải là kiểu tùy chọn thì bạn đã biết rằng nó cũng có thể là không có giá trị
Ví dụ với đoạn code sau:
- (NSAttributedString *)attributedString:(NSString *)input
{
return [[NSAttributedString alloc] initWithString:input];
}
Nhìn thì rất trong sáng, nhưng nếu đầu vào là không có giá trị thì nó sẽ sinh ra lỗi, nó có thể chỉ được thấy khi chạy chương trình. Nó phụ thuộc vào cách nó được sử dụng như thế nào? Bạn có thể tìm ra rất nhanh chóng hoặc cũng có thể chỉ gặp phải khi di chuyển các ứng dụng, dẫn đến hiện tượng lỗi cho khách hàng của bạn.
So sánh tương tự đoạn code trên trong Swift như sau:
Nó có thể nhìn giống như được dịch chính xác từ Objective-C nhưng Swift không cho giá trị bằng 0 được truyền vào. Nếu có trường hợp đó, API sẽ như sau:
extension NSAttributedString {
init(string str: String?)
}
Nhìn kỹ đoạn code, chúng ta thấy thêm một dấu “?”. Nó có nghĩa là bạn có thể truyền vào cả hai trường hợp có giá trị hoặc không. Kiểu dữ liệu thì rất rõ ràng, chúng ta có thế thấy điều đó và xác định kiểu dữ liệu được chấp nhận. Sau khi làm việc với kiểu tùy chọn trong một khoảng thời gian, bạn sẽ nhận ra rằng có thể đọc được kiểu dữ liệu thay vì cần các tài liệu hướng dẫn. Và nếu bạn tạo ra lỗi, bạn sẽ nhận được cảnh báo lỗi biên dịch(compile-error) thay vì lỗi khi thực hiện(runtime-error).
Lời khuyên của tôi:
Nếu có thể, hãy tránh xa kiểu tùy chọn vì nó có thể tạo thêm trở ngại cho khách hàng sử dụng API của bạn. Nếu bạn có một chức năng mà có thể không thành công vì một lý do rõ ràng, bạn có thể trả lại kiểu tùy chọn. Ví dụ, giả sử bạn chuyển đổi chuỗi “#00ff00 “ thành màu sắc . Nếu đầu vào không tuân theo định dạng mà bạn muốn trả về không:
func parseColorFromHexString(input: String) -> UIColor? {
// ...
}
Trong trường hợp này, cần xác định một thông báo lỗi và cũng có thể sử dụng kiểu kết quả trả về, cái mà ko có trong thư viện chuẩn. Việc này rất có ích vì lí do gây thất bại là rất quan trọng
Enums
Enums là nhân tố mới trong Swift và nó hoàn toàn khác so với những gì được sử dụng trong Objective-C. In Objective-C chúng ta có nhiều thứ được gọi là Enums, nhưng nó không được coi là nhiều hơn giá trị của một số nguyên.
Hãy xem kiểu Boolean, nó chắc chắn chỉ có hai giá trị là true hoặc false. Nó không thể thêm được giá trị nào khác.
Nhưng kiểu này lại đúng trong kiểu tùy chọn. Có hai trường hợp xảy ra là có giá trị và không có giá trị. Cả hai trường hợp tùy chọn và Boolean có thể được định nghĩa như Enums trong Swift với chỉ một khác biệt là trong kiểu tùy chọn Enums có trường hợp có giá trị liên quan như sau:
enumBoolean {
case False
case True
}
enumOptional A? {
case Nil
case Some(A)
}
Cũng tương tự, nếu bạn thay đổi tên của các trường hợp thì điều duy nhất khác biệt là giá trị liên quan. Nếu bạn cũng thêm giá trị cho trường hợp không có giá trị của kiểu tùy chọn thì nó sẽ như sau:
enumEither(A,B) {
case Left(A)
case Right(B)
}
Trường hợp như trên được sử dụng rất nhiều trong lập trình chức năng khi bạn muốn có đại diện lựa chọn giữa hai giá trị. Ví dụ, nếu bạn có chức năng trả về hoặc là số nguyên hoặc là lỗi thì bạn có thể sử dụng Either<Int,NSError> và có thể tương tự với Either<Bool,String>
Để biết khi nào sử dụng kiểu Enums hoặc kiểu khác như Class, Structs có thể cũng khó khăn nhưng có một cách rất dễ là khi bạn có một tập hợp khép kín của giá trị. Ví dụ khi ta thiết kế một ứng dụng Swift bao quanh Github API, chúng ta không thể chỉ rõ điểm cuối(End point) đại diện bằng Enums được vì các Endpoint đó không thể đưa ra được bất kỳ tham số nào. Để lấy về được hồ sơ người dùng, chúng ta phải cung cấp Username và để hiện thị được nơi lưu trữ User, chúng ta cung cấp Username và hiển thị đó được sắp xếp tăng dần hay không
enumGithub {
case Zen
case UserProfile(String)
case Repositories(username: String, sortAscending: Bool)
}
Định nghĩa điểm cuối API là trường hợp để định nghĩa Enums. Danh sách của nó là có giới hạn, và chúng ta có thể chỉ định nghĩa các trường hợp cho mỗi Endpoint. Nếu chúng ta truyền giá trị vào các Endpoint, chúng ta sẽ có thông báo cho nó. Vì vậy, nếu tại một vài điểm, chúng ta sẽ muốn thêm trường hợp thì ta cần cập nhật các function hoặc method phù hợp với các kiểu Enums này.
Người khác khi sử dụng Enums của chúng ta, sẽ không thể mở rộng thêm các trường hợp của nó trừ khi họ có thể truy cập vào mã gốc, việc này là rất hữu ích cho việc bảo mật và giới hạn trong lập trình. Hãy cân nhắc, nếu bạn thêm các trường hợp cho kiểu Boolean và Optional thì sau đó các function hoặc method sử dụng nó cũng cần phải được viết lại.
Ví dụ về việc chuyển đổi tiền tệ như sau, nó đã định nghĩa tiền tệ như là Enums
numCurrency {
case Eur
case Usd
}
Bây giờ ta có thể tạo ra function lấy ra các kí tự đại diện cho tiền tệ:
func symbol(input: Currency) -> String {
switch input {
case .Eur: return"€"
case .Usd: return"$"
}
}
Hơn nữa, chúng ta có thể sử dụng để tạo ra định dạng kí tự đẹp mắt hơn:
func format(amount: Double, currency: Currency) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .CurrencyStyle
formatter.currencySymbol = symbol(currency)
return formatter.stringFromNumber(amount)
}
Có một hạn chế là người sử dụng muốn thêm nhiều trường hợp thì sao? Trong Objective-C, cách phổ biến để thêm nhiều trường hợp là dùng các lớp con(Subclassing), theo lý thuyết thì lớp con có thể thừa kế và bổ xung chức năng mới. Trong Swift, bạn vẫn có thể sử dụng Subclassing nhưng là chỉ trên các lớp mà không được sử dụng cho Enums. Tuy nhiên, chúng ta có thể có kỹ thuật khác( nó có thể sử dụng cho cả các giao thức của Objective-C và Swift)
protocolCurrencySymbol {
func symbol() -> String
}
Bây giờ ta có thể tạo kiểu Currency là lớp con của Protocol này
extensionCurrency : CurrencySymbol {
func symbol() -> String {
switchself {
case .Eur: return"€"
case .Usd: return"$"
}
}
}
Và có thể viết lại chức năng Format để có thể sử dụng cho bất kỳ kiểu nào
func format(amount: Double, currency: CurrencySymbol) -> String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .CurrencyStyle
formatter.currencySymbol = currency.symbol()
return formatter.stringFromNumber(amount)
}
Và cũng như CurrencySymbol, giả sử nếu tạo thêm kiểu mới là Bitcoins:
structBitcoin : CurrencySymbol {
func symbol() -> String {
return"B"
}
}
Bạn thấy đấy, đây là khả năng rất tuyệt vời để viết các chức năng có thể được mở rộng. Bạn vẫn có thể sử dụng mềm dẻo Enums, nhưng bằng cách kết hợp thêm Protocols, bạn có thể tùy biến hơn nữa. Phụ thuộc vào yều cầu cụ thể, từ nay bạn dễ dàng có thể đóng hoặc mở API như ý.
Kiểu dữ liệu chặt chẽ
Tôi nghĩ rằng sức mạnh lớn nhất của Swift chính là đây, kiểu dữ liệu chặt chẽ(Type Safety). Như đã biết về Optional, chúng ta có thể kiểm tra những gì đã có sẵn nào đó từ khi thực hiện(runtime) đến thời gian chạy(compile time) bằng cách sử dụng các kiểu theo cách mềm dẻo. Một ví dụ khác về cách hoạt động của mảng trong Swift, một mảng thì có các đặc điểm chung hoặc lưu trữ các phần tử có cùng kiểu. Nó không thể thêm giá trị số nguyên và mảng chuỗi. Việc loại trừ này sẽ loại bỏ toàn bộ các lớp có thể có lỗi. Nếu bạn muốn có một mảng gồm cả chuỗi và số thì ta có thể sử dụng kiểu Either như phía trên bài viết này.
Giả sử một lần nữa ta muốn mở rộng việc chuyển đổi tiền tệ như ở trên ra một đơn vị chung chung, nếu chúng ta sử dụng kiểu Double cho amount, nó có thể gây ra chút khó hiểu. Ví dụ 100.0 nghĩa là 100 dolars, 100 kilogram hoặc bất kỳ cái gì liên quan có chữ 100…Vậy chúng ta cần làm gì để kiểu của hệ thống giúp chúng ta tạo ra các kiểu khác nhau? Giả sử ta có thể định nghĩa kiểu miêu tả Money
structMoney {
let amount : Double
let currency: Currency
}
Hoặc miêu tả số lượng, ta có
structMass {
let kilograms: Double
}
Bây giờ chúng ta không thể nhầm lẫn khi sử dụng Money hay Mass. Phụ thuộc vào chức năng của ứng dụng, nó có thể rất hữu dụng để định nghĩa các kiểu dữ liệu đơn giản giống như thế và hơn nữa việc đọc mã nguồn cũng dễ hiểu hơn. Giả sử chúng ta có chức năng Pounds
func pounds(input: Double) -> Double
Nó khó hiểu được kiểu của nó là gì và có thể chuyển đổi từ Euro sang Pounds, Kilo sang Pounds … được không? Chúng ta có thể tạo ra kiểu rõ ràng hơn như sau:
func pounds(input: Mass) -> Double
Nó không chỉ làm cho ta sử dụng dễ dàng mà còn hiểu nó là cái gì và có thể tránh được nhầm lẫn
Duy trì tính ổn định
Một khả năng khác nữa của Swift là tính ổn định(Immutability). Trong Cocoa, có rất nhiều APIs thể hiện được sự ổn định. Ví dụ, ở góc độ là nhà phát trển Cocoa, chúng tôi sử dụng rất nhiều cặp các lớp như: NSString vs. NSMutableString, NSArray vs.NSMutableArray
Khi bạn nhận về NSString, giả sử nó không thay đổi. Để chắc chắn bạn vẫn Copy nó và sau đó bạn biết chắc chắn rằng có một bản copy duy nhất không thay đổi.
Trong Swift, tính ổn định được xây dựng trực tiếp trong ngôn ngữ. Ví dụ, nếu bạn muốn tạo ra một một chuỗi hay thay đổi thì bạn có thể làm như sau:
var myString = "Hello"
Và ngược lại, nếu chuỗi đó không thay đổi thì
let myString = "Hello"
Khi có dữ liệu không thay đổi, nó sẽ giúp ích rất nhiều khi làm việc với APIs. Ví dụ, nếu bạn có chức năng đưa ra một mảng, nó rất hữu ích để biết rằng mảng sẽ không thay đổi khi duyệt qua nó. Trong Swift, đây là trường hợp mặc định và tạo đoạn code đa luồng mà dữ liệu không bị thay đổi thật sự rất dễ dàng
Khi làm việc với dữ liệu cố định, nó rất rõ ràng xác định được kiểu dữ liệu và điều gì sẽ xảy ra. Ví dụ như xác định kiểu của Map trên Optional. Chúng ta biết rằng có tùy chọn giá trị “T” và có chức năng chuyển đổi “T” sang “U”. Kết quả “U” là giá trị tùy chọn. Sẽ không có cách nào làm cho giá trị gốc bị thay đổi.
func (x: T?, f: T -> U) -> U?
Trường hợp giống như Map được áp dụng trên mảng. Nó được định nghĩa như là phần mở rộng của mảng, vì vậy đầu vào của mảng là self. Chúng ta có thể nhìn thấy rằng nó đưa ra chức năng chuyển đổi “T” sang “U” và tạo ra một mảng giá trị “U”. Bởi vì nó là chức năng không thay đổi, chúng ta biết rằng giá trị gốc của mảng không thể thay đổi và kết quả cũng vậy. Có những rằng buộc đã có sẵn trong kiểu hệ thống và bắt buộc trình biên dịch(compile) phải tuân theo để đưa ra kết quả như quy định và nhớ chính xác những gì thay đổi
extensionArray {
func (transform: T -> U) -> [U]
}
Kết luận
Có rất nhiều tính năng mới rất thú vị từ Swift. Tôi đặc biệt thích cái cách mà trình biên dịch có thể kiểm tra mọi thứ chúng ta đã sử dụng để lập trình. Chúng ta có thể chọn để sử dụng khả năng nào mà ta thấy phù hợp. Chúng ta vẫn có thể tạo ra mã nguồn sử dụng những gì đã có sẵn nhưng cũng có thể tùy chọn một vài tính năng mới cho những phần cụ thể để tối ưu hơn nếu muốn.
Theo tôi nghĩ, Swift có thể thay đổi hoàn toàn cách mà chúng ta đang lập trình theo chiều hướng tốt hơn. Nó có thể cần mất vài năm để mọi người chuyển từ Objective-C sang, có người chuyển đổi rất nhanh và ngược lại. Nhưng tôi tự tin rằng trong thời gian sắp tới, hầu hết mọi người sẽ thấy được lợi ích mà Swift mang lại.
Bình luận