+3

Swift 5.9 có gì mới?

1. if và switch

Thêm khả năng để chúng ta sử dụng if và switch dưới dạng biểu thức trong một số tình huống Ví dụ:

let score = 800
let simpleResult = if score > 500 { "Pass" } else if score > 300 { "Merit" } else { "Fail" }
print(simpleResult)
let complexResult = switch score {
    case 0...300: "Fail"
    case 301...500: "Pass"
    case 501...800: "Merit"
    default: "Distinction"
}

print(complexResult)

Trong ví dụ về if chúng ta thấy khá giống với toán tử ba ngôi. Tuy nhiên, sử dụng if sẽ giúp chúng ta tường minh hơn và có thể kiểm tra nhiều điều kiện.

let customerRating = 4
let bonusMultiplier1 = customerRating > 3 ? 1.5 : 1
let bonusMultiplier2 = if customerRating > 3 { 1.5 } else { 1.0 }

Nhìn ví dụ trên, chúng ta thấy rằng với toán tử 3 ngồi thì value chỉ cần viết là 1 thay vì sử dụng if chúng ta phải viết là 1.0. Khi sử dụng toán tử ba ngôi Swift kiểm tra loại của cả hai giá trị cùng một lúc và do đó tự động coi 1 là 1.0, trong khi với biểu thức if, hai tùy chọn được kiểm tra loại độc lập: nếu chúng ta sử dụng 1.5 cho một trường hợp và 1 cho trường hợp kia thì chúng tôi sẽ gửi lại Double và Int, điều này không được phép.

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/if-switch-expressions

2. Noncopyable structs and enums

struct User: ~Copyable {
    var name: String
}

Ví dụ trên chúng ta đã tạo struct User noncopyable. Lưu ý, Noncopyable types không thể tuân thủ bất kỳ giao thức nào khác ngoài Sendable

func createUser() {
    let newUser = User(name: "Anonymous")

    var userCopy = newUser
    print(userCopy.name)
}

createUser()

Ở ví dụ trên, chúng ta đã khai báo cấu trúc User là không thể sao chép được – làm sao nó có thể lấy một bản sao của newUser? Câu trả lời là không thể: việc gán newUser cho userCopy khiến giá trị newUser ban đầu được sử dụng, điều đó có nghĩa là nó không thể được sử dụng nữa vì quyền sở hữu hiện thuộc về userCopy. Nếu bạn thử thay đổi print(userCopy.name) thành print(newUser.name), bạn sẽ thấy Swift đưa ra lỗi trình biên dịch.

Chúng ta có thể viết một hàm tạo Userborrowing User để có quyền truy cập chỉ đọc vào dữ liệu của User đó:

func createAndGreetUser() {
    let newUser = User(name: "Anonymous")
    greet(newUser)
    print("Goodbye, \(newUser.name)")
}

func greet(_ user: borrowing User) {
    print("Hello, \(user.name)!")
}

createAndGreetUser()

Ngược lại, nếu function greet() sử dụng consuming User sau đó print("Goodbye, \(newUser.name)") sẽ không được phép. Khi đó sẽ báo lỗi 'newUser' used after consume

Hiện tại, các noncopyable types không thể phù hợp với các lĩnh vực sau:

  1. protocols
  2. generic parameters
  3. associated type requirements in protocols
  4. the Self type in a protocol declaration, or in extensions

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/noncopyable-structs-and-enums https://medium.com/@reshmaUnni/new-in-swift-5-9-noncopyable-type-c3860354ee4c

3. Value and Type parameter packs

Value and type parameter packs cho phép chúng ta giảm số lượng quá tải và viết các hàm tổng quát chấp nhận số lượng đối số tùy ý với các kiểu riêng biệt. Chúng ta cùng xem ví dụ dưới đây:

func eachFirst<T>(
    _ item: T
) -> T?

func eachFirst<T1, T2>(
    _ item1: T1,
    _ item2: T2
) -> (T1?, T2?)

func eachFirst<T1, T2, T3>(
    _ item1: T1,
    _ item2: T2,
    _ item3: T3
) -> (T1?, T2?, T3?)

Bạn có thể nhận ra những tình trạng quá tải này từ các toán tử Kết hợp như zip, CombineLatest và hợp nhất. Những tình trạng quá tải này cũng là lý do dẫn đến giới hạn 10 view trong SwiftUI, vì tham số nội dung của chế độ xem sử dụng phương thức khối xây dựng cơ bản @ViewBuilder, được xác định như sau:

static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View {
    return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}

Ví dụ trên là phương pháp nạp chồng cuối cùng của tất cả các nạp chồng khối xây dựng: image.png Bắt đầu từ Swift 5.9, phương thức tương tự được viết lại bằng cách sử dụng type and value parameter packs. Ví dụ:

func eachFirst<each T: Collection>(_ item: repeat each T) -> (repeat (each T).Element?) {
    return (repeat (each item).first)
}

let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let firstValues = eachFirst(numbers, names)
print(firstValues) // Optional(0), Optional("Antoine")

Trong ví dụ này, chúng ta đã xác định một phương thức toàn cục mới có tên eachFirst, cho phép chúng ta chuyển vào bất kỳ số mảng tùy ý nào và nhận cùng số phần tử tùy chọn làm giá trị trả về. Như ví dụ trên nếu chúng ta chuyển thành dùng array thì sẽ xảy ra lỗi như hình: image.png Như vậy việc dùng Value and Type parameter packs là tối ưu và clean nhất. Tham khảo:

https://www.avanderlee.com/swift/value-and-type-parameter-packs/#:~:text=Value and type parameter packs allow us to reduce the,start rewriting using parameter packs.

https://www.hackingwithswift.com/swift/5.9/variadic-generics

4. Add sleep(for:) to Clock

Ví dụ: lớp này có thể được tạo bằng bất kỳ loại Clock nào và sẽ sleep trước khi kích hoạt thao tác lưu:

class DataController: ObservableObject {
    var clock: any Clock<Duration>

    init(clock: any Clock<Duration>) {
        self.clock = clock
    }

    func delayedSave() async throws {
        try await clock.sleep(for: .seconds(1))
        print("Saving…")
    }
}

Với Task cũng thế, giờ chúng ta dùng

try await Task.sleep(for: .seconds(1), tolerance: .seconds(0.5))

Thay vì:

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5))

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/sleep-for-clock

5. Consume operator

Toán tử consume để kết thúc thời gian tồn tại của một ràng buộc biến. Cùng xem ví dụ dưới:

struct User {
    var name: String
}

func createUser() {
    let newUser = User(name: "Anonymous")
    let userCopy = consume newUser
    print(userCopy.name)
}

createUser()

Dòng quan trọng là dòng let userCopy, thực hiện hai việc cùng một lúc:

  1. Nó sao chép giá trị từ newUser vào userCopy.
  2. Nó kết thúc thời gian tồn tại của newUser, do đó, bất kỳ nỗ lực nào tiếp theo để truy cập vào nó sẽ gây ra lỗi. Điều này cho phép chúng ta nói với trình biên dịch một cách rõ ràng “không cho phép tôi sử dụng lại giá trị này” và nó sẽ thay mặt chúng ta thực thi quy tắc. Trong thực tế, có thể nơi phổ biến nhất mà toán tử tiêu thụ sẽ được sử dụng là khi truyền các giá trị vào một hàm như sau:
func createAndProcessUser() {
    let newUser = User(name: "Anonymous")
    process(user: consume newUser)
}

func process(user: User) {
    print("Processing \(name)…")
}

createAndProcessUser()

Có hai điều bổ sung mà tôi nghĩ đặc biệt đáng biết về tính năng này. Đầu tiên, Swift theo dõi nhánh nào trong mã của bạn đã sử dụng giá trị và thực thi các quy tắc có điều kiện. Vì vậy, trong mã này chỉ có một trong hai khả năng sử dụng phiên bản consumes User:

func greetRandomly() {
    let user = User(name: "Taylor Swift")

    if Bool.random() {
        let userCopy = consume user
        print("Hello, \(userCopy.name)")
    } else {
        print("Greetings, \(user.name)")
    }
}

greetRandomly()

Thứ hai, về mặt kỹ thuật, tiêu thụ hoạt động dựa trên các ràng buộc chứ không phải giá trị. Trong thực tế, điều này có nghĩa là nếu chúng ta sử dụng một biến, chúng ta có thể khởi tạo lại biến đó và sử dụng nó:

func createThenRecreate() {
    var user = User(name: "Roy Kent")
    _ = consume user

    user = User(name: "Jamie Tartt")
    print(user.name)
}

createThenRecreate()

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/consume-operator

6. Phương thức Async[Throwing]Stream.makeStream

Thêm phương thức makeStream() mới vào cả AsyncStream và AsyncThrowingStream để gửi lại cả luồng cùng với sự tiếp tục của nó. Thay vì:

var _continuation: AsyncStream<String>.Continuation!
let stream = AsyncStream<String> { continuation = $0 }
let continuation = _continuation!

Chúng ta chỉ cần:

let (stream, continuation) = AsyncStream.makeStream(of: String.self)

Điều này sẽ được đặc biệt khuyên dùng ở những nơi bạn cần truy cập vào phần tiếp theo bên ngoài bối cảnh hiện tại, chẳng hạn như theo một phương pháp khác. Ví dụ, trước đây chúng ta có thể đã viết một trình tạo số đơn giản như thế này, nó cần lưu trữ phần tiếp theo làm thuộc tính riêng của nó để có thể gọi nó từ phương thức queueWork():

struct OldNumberGenerator {
    private var continuation: AsyncStream<Int>.Continuation!
    var stream: AsyncStream<Int>!

    init() {
        stream = AsyncStream(Int.self) { continuation in
            self.continuation = continuation
        }
    }

    func queueWork() {
        Task {
            for i in 1...10 {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(i)
            }

            continuation.finish()
        }
    }
}

Với phương thức makeStream(of:) mới, mã này trở nên đơn giản hơn nhiều:

struct NewNumberGenerator {
    let (stream, continuation) = AsyncStream.makeStream(of: Int.self)

    func queueWork() {
        Task {
            for i in 1...10 {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(i)
            }

            continuation.finish()
        }
    }
}

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/convenience-asyncthrowingstream-makestream

7. Discarding task groups

Thêm các nhóm tác vụ có thể loại bỏ mới để khắc phục một lỗ hổng quan trọng trong API hiện tại: các tác vụ được tạo bên trong một nhóm tác vụ sẽ tự động bị loại bỏ và hủy ngay khi chúng hoàn thành, nghĩa là các nhóm tác vụ sẽ chạy trong khoảng thời gian dài (hoặc có thể là mãi mãi, như trong trường hợp máy chủ web) sẽ không bị rò rỉ bộ nhớ theo thời gian. Khi sử dụng API withTaskGroup() ban đầu, sự cố có thể xảy ra do cách Swift chỉ loại bỏ một tác vụ con và dữ liệu kết quả của nó khi chúng ta gọi next() hoặc lặp qua các tác vụ con của nhóm nhiệm vụ. Việc gọi next() sẽ khiến mã của bạn bị treo nếu tất cả các tác vụ con hiện đang thực thi, vì vậy chúng tôi gặp phải vấn đề: bạn muốn một máy chủ luôn lắng nghe các kết nối để bạn có thể thêm các tác vụ để xử lý chúng, nhưng bạn cũng cần phải dừng mọi việc như vậy thường xuyên để dọn dẹp những công việc cũ đã hoàn thành. Không có giải pháp rõ ràng nào cho vấn đề này cho đến khi Swift 5.9 bổ sung thêm các hàm withDiscardingTaskGroup() và withThrowingDiscardingTaskGroup() để tạo các nhóm tác vụ loại bỏ mới. Đây là các nhóm nhiệm vụ tự động loại bỏ và hủy từng nhiệm vụ ngay khi nó hoàn thành mà chúng ta không cần phải gọi next() để thực hiện thủ công.

Để cung cấp cho bạn ý tưởng về nguyên nhân gây ra sự cố, chúng tôi có thể triển khai trình theo dõi thư mục đơn giản lặp lại mãi mãi và báo cáo lại tên của bất kỳ tệp hoặc thư mục nào đã được thêm hoặc xóa:

struct FileWatcher {
    // The URL we're watching for file changes.
    let url: URL

    // The set of URLs we've already returned.
    private var handled = Set<URL>()

    init(url: URL) {
        self.url = url
    }

    mutating func next() async throws -> URL? {
        while true {
            // Read the latest contents of our directory, or exit if a problem occurred.
            guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
                return nil
            }

            // Figure out which URLs we haven't already handled.
            let unhandled = handled.symmetricDifference(contents)

            if let newURL = unhandled.first {
                // If we already handled this URL then it must be deleted.
                if handled.contains(newURL) {
                    handled.remove(newURL)
                } else {
                    // Otherwise this URL is new, so mark it as handled.
                    handled.insert(newURL)
                    return newURL
                }
            } else {
                // No file difference; sleep for a few seconds then try again.
                try await Task.sleep(for: .microseconds(1000))
            }
        }
    }
}

Sau đó, chúng ta có thể sử dụng nó từ bên trong một ứng dụng đơn giản:

struct FileProcessor {
    static func main() async throws {
        var watcher = FileWatcher(url: URL(filePath: "/Users/twostraws"))

        try await withThrowingTaskGroup(of: Void.self) { group in
            while let newURL = try await watcher.next() {
                group.addTask {
                    process(newURL)
                }
            }
        }
    }

    static func process(_ url: URL) {
        print("Processing \(url.path())")
    }
}

Điều đó sẽ chạy mãi mãi hoặc ít nhất là cho đến khi người dùng chấm dứt chương trình hoặc thư mục chúng tôi đang xem không còn có thể truy cập được. Tuy nhiên, vì nó sử dụng withThrowingTaskGroup() nên nó có một vấn đề: một tác vụ con mới được tạo mỗi khi addTask() được gọi, nhưng vì nó không gọi group.next() ở bất kỳ đâu nên các tác vụ con đó không bao giờ bị hủy. Từng chút một – có thể chỉ vài trăm byte mỗi lần – mã này sẽ ngày càng chiếm nhiều bộ nhớ hơn cho đến khi hệ điều hành hết RAM và buộc phải chấm dứt chương trình. Vấn đề này sẽ biến mất hoàn toàn khi loại bỏ các nhóm nhiệm vụ: chỉ cần thay thế withThrowingTaskGroup(of: Void.self) bằng withThrowingDiscardingTaskGroup nghĩa là mỗi nhiệm vụ con sẽ tự động bị hủy ngay khi công việc của nó kết thúc. Trong thực tế, vấn đề này chủ yếu xảy ra với mã máy chủ, trong đó máy chủ phải có khả năng chấp nhận các kết nối mới trong khi xử lý các kết nối hiện có một cách trơn tru.

Tham khảo:

https://www.hackingwithswift.com/swift/5.9/discarding-task-groups


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí