Text​Output​Stream

print là một trong những function được sử dụng nhiều nhất Swift. Thật vậy, nó là chức năng đầu tiên mà một lập trình viên học được khi viết ra “Hello, world!”. Nhưng có thể chúng ta cũng chưa hoàn toàn quen thuộc với các hình thức khác của nó.

Chẳng hạn, bạn có biết print các văn bản của các mục đã cho vào với đầu ra tiêu chuẩn. print(_:separator:terminator:)? Hoặc nó có một biến thể có tên là print(_:separator:terminator:to:)?

Chúng ta cùng dành chút thời gian để tìm hiểu toàn bộ sự thật về một chức năng mà trước đây bạn có thể nghĩ là không cần giới thiệu thêm.

Chúng ta cùng xem xét kỹ hơn về khai báo hàm đó từ trước:

func print<Target>(_ items: Any...,
                   separator: String = default,
                   terminator: String = default,
                   to output: inout Target)
    where Target : TextOutputStream

Quá tải của print là có một danh sách các đối số có độ dài thay đổi, theo sau là các tham số separatorterminator cả hai đều có giá trị mặc định.

  • separator là chuỗi được sử dụng để nối đại diện của từng phần tử trong các mục thành một chuỗi. Theo mặc định, đây là một khoảng trắng (" ").
  • terminator là chuỗi được nối vào cuối của đại diện được in. Theo mặc định, đây là một dòng mới ("\ n").

Tham số cuối cùng, đầu ra lấy một thể hiện có thể thay đổi của loại Target chung phù hợp với giao thức TextOutputStream.

Một thể hiện của một type theo TextOutputStream có thể được chuyển đến chức năng print(_:to:) để thu và chuyển hướng chuỗi từ đầu ra tiêu chuẩn.

Triển khai kiểu TextOutputStream tùy chỉnh

Việc tuân thủ giao thức TextOutputStream chỉ đơn giản là vấn đề đáp ứng yêu cầu phương thức write (_:)

protocol TextOutputStream {
    mutating func write(_ string: String)
}

Trong quá trình triển khai, chúng ta lặp lại qua từng giá trị Unicode.Scalar trong chuỗi đã truyền; phương thức thu enumerated() cung cấp phần bù hiện tại trên mỗi vòng lặp. Ở đầu phương thức, một câu lệnh guard sẽ return nếu chuỗi trống hoặc một dòng mới (điều này làm giảm lượng nhiễu trong giao diện điều khiển).


struct UnicodeLogger: TextOutputStream {
    mutating func write(_ string: String) {
        guard !string.isEmpty && string != "\n" else {
            return
        }

        for (index, unicodeScalar) in
            string.unicodeScalars.lazy.enumerated()
        {
            let name = unicodeScalar.name ?? ""
            let codePoint = String(format: "U+%04X", unicodeScalar.value)
            print("\(index): \(unicodeScalar) \(codePoint)\t\(name)")
        }
    }
}

Để sử dụng loại UnicodeLogger mới của chúng tôi, hãy khởi tạo nó và gán nó cho một biến ( var) để nó có thể được truyền dưới dạng đối số đầu vào. Bất cứ khi nào chúng tôi muốn nhận được tia X của chuỗi thay vì chỉ print biểu diễn bề mặt của chuỗi, chúng tôi có thể giải quyết một tham số bổ sung cho câu lệnh in của mình.


Làm như vậy cho phép chúng tôi tiết lộ một bí mật về nhân vật biểu tượng cảm xúc 👨‍👩‍👧‍👧: nó thực sự là một chuỗi gồm bốn biểu tượng cảm xúc riêng lẻ được nối bởi các nhân vật ZWJ - tổng cộng bảy điểm mã!

print("👨‍👩‍👧‍👧")
// Prints: "👨‍👩‍👧‍👧"

var logger = UnicodeLogger()
print("👨‍👩‍👧‍👧", to: &logger)
// Prints:
// 0: 👨 U+1F468    MAN
// 1:    U+200D     ZERO WIDTH JOINER
// 2: 👩 U+1F469    WOMAN
// 3:    U+200D     ZERO WIDTH JOINER
// 4: 👧 U+1F467    GIRL
// 5:    U+200D     ZERO WIDTH JOINER
// 6: 👧 U+1F467    GIRL

Ý tưởng cho việc sử dụng TextOutputStreams tùy chỉnh

Bây giờ chúng ta đã biết về một phần bí ẩn chưa rõ của thư viện chuẩn Swift, chúng ta có thể làm gì với nó?

Hóa ra, có rất nhiều trường hợp sử dụng tiềm năng cho TextOutputStream. Để hiểu rõ hơn về những gì họ đang có, hãy xem xét các ví dụ sau:

Logging to Standard Error

Theo mặc định, các câu lệnh print của Swift được hướng đến đầu ra tiêu chuẩn (stdout). Thay vào đó, nếu bạn muốn trực tiếp đến lỗi tiêu chuẩn (stderr), bạn có thể tạo một loại luồng đầu ra văn bản mới và sử dụng nó theo cách sau:

import func Darwin.fputs
import var Darwin.stderr

struct StderrOutputStream: TextOutputStream {
    mutating func write(_ string: String) {
        fputs(string, stderr)
    }
}

var standardError = StderrOutputStream()
print("Error!", to: &standardError)

Writing Output to a File

Ví dụ trước đây về ghi vào stderr có thể được khái quát hóa để ghi vào bất kỳ luồng hoặc tệp nào bằng cách thay vào đó tạo một luồng đầu ra cho FileHandle (đối với lỗi tiêu chuẩn có thể truy cập thông qua thuộc tính loại).

import Foundation

struct FileHandlerOutputStream: TextOutputStream {
    private let fileHandle: FileHandle
    let encoding: String.Encoding

    init(_ fileHandle: FileHandle, encoding: String.Encoding = .utf8) {
        self.fileHandle = fileHandle
        self.encoding = encoding
    }

    mutating func write(_ string: String) {
        if let data = string.data(using: encoding) {
            fileHandle.write(data)
        }
    }
}

Theo cách tiếp cận này, bạn có thể tùy chỉnh in để ghi vào tệp thay vì luồng.

let url = URL(fileURLWithPath: "/path/to/file.txt")
let fileHandle = try FileHandle(forWritingTo: url)
var output = FileHandlerOutputStream(fileHandle)

print("\(Date())", to: &output)

Escaping Streamed Output

Để làm ví dụ cuối cùng, hãy hình dung ra một tình huống trong đó bạn thấy mình thường xuyên sao chép đầu ra bảng điều khiển dán vào một biểu mẫu trên một số trang web. Thật không may, trang web có hành vi vô ích khi cố gắng phân tích cú pháp <and> như thể chúng là HTML.


Thay vì thực hiện thêm một bước để thoát văn bản mỗi khi bạn đăng lên trang web, bạn có thể tự động tạo TextOutputStream để tự động xử lý việc đó (trong trường hợp này, chúng tôi sử dụng chức năng XML-escaping mà chúng tôi thấy được trong Core Foundation).
import Foundation

struct XMLEscapingLogger: TextOutputStream {
    mutating func write(_ string: String) {
        guard !string.isEmpty && string != "\n",
            let xmlEscaped = CFXMLCreateStringByEscapingEntities(nil, string as NSString, nil)
        else {
            return
        }

        print(xmlEscaped)
    }
}

Print là một cách quen thuộc và thuận tiện cho các nhà phát triển để hiểu hành vi code của họ. Nó bổ sung cho các kỹ thuật toàn diện hơn như khung đăng nhập và trình debug.

Bài viết được dịch từ bài chia sẻ cùng tên của tác giả Mattt