面向協(xié)議的日志:給 Swift 協(xié)議添加默認(rèn)參數(shù)

作者:Natasha The Robot,原文鏈接,原文日期:2016-05-01
譯者:Channe;校對(duì):walkingway;定稿:CMB

Swift 2.2 不允許在協(xié)議聲明時(shí)提供默認(rèn)參數(shù)。如果你想使用協(xié)議抽象出 App 中的日志代碼,就會(huì)面臨一個(gè)問題。因?yàn)槟J(rèn)參數(shù)通常用來將源代碼位置傳遞給日志函數(shù)。不過,你可以在協(xié)議擴(kuò)展中使用默認(rèn)參數(shù),這是一個(gè)變通方案。

一個(gè)典型的日志消息應(yīng)該包括日志事件的源代碼位置(文件名、行號(hào)和可能的函數(shù)名)。Swift 為此提供了 #file,#line#column#function 調(diào)試標(biāo)識(shí)。在編譯時(shí),解析器將這些占位符展開為字符串或用來描述當(dāng)前源代碼位置的整數(shù)字面量。如果我們?cè)诿看握{(diào)用日志函數(shù)時(shí)都包含這些參數(shù),那重復(fù)的次數(shù)太多,所以它們通常都是作為默認(rèn)參數(shù)傳遞。這里之所以可行是因?yàn)榫幾g器足夠聰明,能夠在評(píng)估默認(rèn)參數(shù)列表時(shí)將調(diào)試標(biāo)識(shí)擴(kuò)展到函數(shù)調(diào)用處。標(biāo)準(zhǔn)庫中的 assert 函數(shù)就是一個(gè)例子,它這樣聲明:

func assert(
    @autoclosure condition: () -> Bool,
    @autoclosure _ message: () -> String = default,
    file: StaticString = #file,
    line: UInt = #line)

第三個(gè)和第四個(gè)參數(shù)默認(rèn)擴(kuò)展為調(diào)用者源代碼的位置。(如果你對(duì) @autoclosure 屬性有疑問,它把一個(gè)表達(dá)式封裝為一個(gè)閉包,有效地將表達(dá)式的執(zhí)行從調(diào)用處延遲到函數(shù)體執(zhí)行時(shí),即閉包表達(dá)式在明確使用時(shí)才會(huì)執(zhí)行。assert 只在調(diào)試構(gòu)建時(shí)使用它來執(zhí)行 condition 參數(shù)的計(jì)算(可能代價(jià)高昂或者有副作用),同時(shí)只在斷言失敗時(shí)才計(jì)算 message 參數(shù)。)

一個(gè)簡(jiǎn)單、全局的日志函數(shù)

你可以使用同樣的方法來寫一個(gè)日志函數(shù),該函數(shù)需要一個(gè)日志消息和一個(gè)日志級(jí)別作為參數(shù)。它的接口和實(shí)現(xiàn)類似于:

enum LogLevel: Int {
    case verbose = 1
    case debug = 2
    case info = 3
    case warning = 4
    case error = 5
}

func log(
    logLevel: LogLevel,
    @autoclosure _ message: () -> String,
    file: StaticString = #file,
    line: Int = #line,
    function: StaticString = #function)
{
    // 使用 `print` 打印日志
    // 此時(shí)不用考慮 `logLevel`
    print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
}

你可能主張使用另一種方法,而不是像這里將 message 參數(shù)聲明為 @autoclosure。這個(gè)屬性并沒有提供多少好處,因?yàn)?message 參數(shù)無論什么情況都會(huì)計(jì)算。既然如此,我們來修改一下。

具體類型

為了代替全局的日志函數(shù),我們創(chuàng)建一種叫做 PrintLogger 的類型,它用最小日志級(jí)別初始化,只會(huì)記錄最小日志級(jí)別的事件。LogLevel 因此需要 Comparable 協(xié)議,這是為什么我之前把它聲明為 Int 型來存儲(chǔ)原始數(shù)據(jù)的原因:

extension LogLevel: Comparable {}

func <(lhs: LogLevel, rhs: LogLevel) -> Bool {
    return lhs.rawValue < rhs.rawValue
}

struct PrintLogger {
    let minimumLogLevel: LogLevel

    func log(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString = #file,
        line: Int = #line,
        function: StaticString = #function)
    {
        if logLevel >= minimumLogLevel {
            print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
        }
    }
}

你將會(huì)這樣使用 PrintLogger

let logger = PrintLogger(
    minimumLogLevel: .warning)
logger.log(.error, "This is an error log")
    // 獲取日志
logger.log(.debug, "This is a debug log")
    // 啥也沒做

帶默認(rèn)參數(shù)的協(xié)議

下一步,我將會(huì)創(chuàng)建一個(gè) Logger 協(xié)議作為 PrintLogger 的抽象。它將允許我今后使用更高級(jí)的實(shí)現(xiàn)替換簡(jiǎn)單的 print 語句,比如記錄日志到文件或者發(fā)送日志給服務(wù)器。但是,我在這里碰了壁,因?yàn)?Swift 不允許在協(xié)議聲明時(shí)提供默認(rèn)參數(shù)。下面的代碼無法通過編譯:

protocol Logger {
    func log(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString = #file,
        line: Int = #line,
        function: StaticString = #function)
    // 錯(cuò)誤: 協(xié)議方法中不允許默認(rèn)參數(shù)
}

因此,我不得不刪掉默認(rèn)參數(shù),使協(xié)議編譯能夠通過。這似乎并不是一個(gè)問題。PrintLogger 可以使用帶有空擴(kuò)展的協(xié)議,它目前的實(shí)現(xiàn)基本上能滿足要求。通過使用一個(gè) logger: PrintLogger 類型的變量和之前的用法沒有什么區(qū)別。

如果你嘗試使用一個(gè) logger2: Logger 協(xié)議類型的變量,問題馬上就來了,因?yàn)槟阏{(diào)用代碼時(shí)是猜不到具體的實(shí)現(xiàn)的:

let logger2: Logger = PrintLogger(minimumLogLevel: .warning)
logger2.log(.error, "An error occurred")
    // 錯(cuò)誤:調(diào)用時(shí)缺少參數(shù)
logger2.log(.error, "An error occurred", file: #file, line: #line, function: #function)
    // 可用但是 ??

logger2 只知道這個(gè)日志函數(shù)有五個(gè)必須的參數(shù),所以你不得不每次都全部寫上它們。討厭!

把默認(rèn)參數(shù)移到協(xié)議擴(kuò)展里

解決方法是聲明兩個(gè)版本的日志函數(shù):一,在協(xié)議聲明時(shí)沒有默認(rèn)參數(shù),我命名這個(gè)方法為 writeLogEntry。二,在 Logger 的協(xié)議擴(kuò)展里包含默認(rèn)參數(shù)(這是允許的),我保持這個(gè)方法名就為 log,因?yàn)樵摲椒〞?huì)是這個(gè)協(xié)議的公開接口。

現(xiàn)在,log 的實(shí)現(xiàn)只有一行代碼:調(diào)用 writeLogEntry,傳入所有參數(shù),而調(diào)用者通過默認(rèn)參數(shù)傳入了源代碼位置。writeLogEntry 從另一方面來說是協(xié)議必須實(shí)現(xiàn)的適配器方法,用來執(zhí)行實(shí)際的日志操作。這里是完整的協(xié)議代碼:

protocol Logger {
    /// 打印一條日志
    /// 類型必須遵循 Logger 協(xié)議的必選參數(shù)
    /// - 注意:Logger 的調(diào)用者永遠(yuǎn)不應(yīng)該調(diào)用此方法
     /// 總是調(diào)用 log(_:,_:) 方法
    func writeLogEntry(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString,
        line: Int,
        function: StaticString)
}

extension Logger {
    /// Logger 協(xié)議的公開 API
    /// 只是調(diào)用 writeLogEntry(_:,_:,file:,line:,function:) 方法
    func log(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString = #file,
        line: Int = #line,
        function: StaticString = #function)
    {
        writeLogEntry(logLevel, message,
            file: file, line: line,
            function: function)
    }
}

按照 session 408 的說法,writeLogEntry 是一個(gè)協(xié)議要求和協(xié)議的用戶自定義點(diǎn),但 log 并不是。這就是我們想要的。log 方法的唯一任務(wù)就是立刻轉(zhuǎn)發(fā)給 writeLogEntry,writeLogEntry 包含了實(shí)際的邏輯。實(shí)現(xiàn) Logger 協(xié)議時(shí)就沒有理由重寫log方法了。

下面是采用協(xié)議后的完整 PrintLogger 類型:

struct PrintLogger {
    let minimumLogLevel: LogLevel
}

extension PrintLogger: Logger {
    func writeLogEntry(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString,
        line: Int,
        function: StaticString)
    {
        if logLevel >= minimumLogLevel {
            print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
        }
    }
}

現(xiàn)在你可以像期望中那樣使用協(xié)議了:

let logger3: Logger = PrintLogger(
    minimumLogLevel: .verbose)
logger3.log(.error, "An error occurred") // 撒花??

調(diào)用者的 API 可見度

這個(gè)方法有一個(gè)弊端,不能簡(jiǎn)便清晰的通過訪問控制給使用者指出協(xié)議中的 logwriteLogEntry 的作用。理想情況下,調(diào)用者使用協(xié)議時(shí)不會(huì)看到 writeLogEntry 方法,然而部署協(xié)議的對(duì)象可能同時(shí)看到 logwriteLogEntry 。如果你不想讓調(diào)用者創(chuàng)建自己的 Logger 類型,只能使用 publicinternalprivate。當(dāng)然,通過文檔說明情況也是一個(gè)選擇。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問 http://swift.gg。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,777評(píng)論 25 709
  • (http://www.cnblogs.com/zhangchenliang/p/4546352.html) 1、...
    凌雲(yún)木閱讀 2,620評(píng)論 0 2
  • 在應(yīng)用程序中添加日志記錄總的來說基于三個(gè)目的:監(jiān)視代碼中變量的變化情況,周期性的記錄到文件中供其他應(yīng)用進(jìn)行統(tǒng)計(jì)分析...
    時(shí)待吾閱讀 5,203評(píng)論 1 13
  • 又是教師節(jié)了,算來,讀書廿載,遇見過的老師,應(yīng)該不下五六十位了,有些短期執(zhí)教,有些長(zhǎng)期授業(yè),有些名字刻骨銘心,有些...
    一袍風(fēng)閱讀 1,018評(píng)論 0 1

友情鏈接更多精彩內(nèi)容