面向协议的日志:给 Swift 协议添加默认参数

作者:Natasha The Robot,原文链接,原文日期:2016-05-01
译者:Channe;校对:walkingway;定稿:CMB

Swift 2.2 不允许在协议声明时提供默认参数。如果你想使用协议抽象出 App 中的日志代码,就会面临一个问题。因为默认参数通常用来将源代码位置传递给日志函数。不过,你可以在协议扩展中使用默认参数,这是一个变通方案。

一个典型的日志消息应该包括日志事件的源代码位置(文件名、行号和可能的函数名)。Swift 为此提供了 #file#line#column#function 调试标识。在编译时,解析器将这些占位符展开为字符串或用来描述当前源代码位置的整数字面量。如果我们在每次调用日志函数时都包含这些参数,那重复的次数太多,所以它们通常都是作为默认参数传递。这里之所以可行是因为编译器足够聪明,能够在评估默认参数列表时将调试标识扩展到函数调用处。标准库中的 assert 函数就是一个例子,它这样声明:

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

第三个和第四个参数默认扩展为调用者源代码的位置。(如果你对 @autoclosure 属性有疑问,它把一个表达式封装为一个闭包,有效地将表达式的执行从调用处延迟到函数体执行时,即闭包表达式在明确使用时才会执行。assert 只在调试构建时使用它来执行 condition 参数的计算(可能代价高昂或者有副作用),同时只在断言失败时才计算 message 参数。)

一个简单、全局的日志函数

你可以使用同样的方法来写一个日志函数,该函数需要一个日志消息和一个日志级别作为参数。它的接口和实现类似于:

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` 打印日志
    // 此时不用考虑 `logLevel`
    print("\(logLevel) – \(file):\(line) – \(function) – \(message())")
}

你可能主张使用另一种方法,而不是像这里将 message 参数声明为 @autoclosure。这个属性并没有提供多少好处,因为 message 参数无论什么情况都会计算。既然如此,我们来修改一下。

具体类型

为了代替全局的日志函数,我们创建一种叫做 PrintLogger 的类型,它用最小日志级别初始化,只会记录最小日志级别的事件。LogLevel 因此需要 Comparable 协议,这是为什么我之前把它声明为 Int 型来存储原始数据的原因:

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())")
        }
    }
}

你将会这样使用 PrintLogger

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

带默认参数的协议

下一步,我将会创建一个 Logger 协议作为 PrintLogger 的抽象。它将允许我今后使用更高级的实现替换简单的 print 语句,比如记录日志到文件或者发送日志给服务器。但是,我在这里碰了壁,因为 Swift 不允许在协议声明时提供默认参数。下面的代码无法通过编译:

protocol Logger {
    func log(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString = #file,
        line: Int = #line,
        function: StaticString = #function)
    // 错误: 协议方法中不允许默认参数
}

因此,我不得不删掉默认参数,使协议编译能够通过。这似乎并不是一个问题。PrintLogger 可以使用带有空扩展的协议,它目前的实现基本上能满足要求。通过使用一个 logger: PrintLogger 类型的变量和之前的用法没有什么区别。

如果你尝试使用一个 logger2: Logger 协议类型的变量,问题马上就来了,因为你调用代码时是猜不到具体的实现的:

let logger2: Logger = PrintLogger(minimumLogLevel: .warning)
logger2.log(.error, "An error occurred")
    // 错误:调用时缺少参数
logger2.log(.error, "An error occurred", file: #file, line: #line, function: #function)
    // 可用但是 😱

logger2 只知道这个日志函数有五个必须的参数,所以你不得不每次都全部写上它们。讨厌!

把默认参数移到协议扩展里

解决方法是声明两个版本的日志函数:一,在协议声明时没有默认参数,我命名这个方法为 writeLogEntry。二,在 Logger 的协议扩展里包含默认参数(这是允许的),我保持这个方法名就为 log,因为该方法会是这个协议的公开接口。

现在,log 的实现只有一行代码:调用 writeLogEntry,传入所有参数,而调用者通过默认参数传入了源代码位置。writeLogEntry 从另一方面来说是协议必须实现的适配器方法,用来执行实际的日志操作。这里是完整的协议代码:

protocol Logger {
    /// 打印一条日志
    /// 类型必须遵循 Logger 协议的必选参数
    /// - 注意:Logger 的调用者永远不应该调用此方法
     /// 总是调用 log(_:,_:) 方法
    func writeLogEntry(
        logLevel: LogLevel,
        @autoclosure _ message: () -> String,
        file: StaticString,
        line: Int,
        function: StaticString)
}

extension Logger {
    /// Logger 协议的公开 API
    /// 只是调用 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 是一个协议要求和协议的用户自定义点,但 log 并不是。这就是我们想要的。log 方法的唯一任务就是立刻转发给 writeLogEntrywriteLogEntry 包含了实际的逻辑。实现 Logger 协议时就没有理由重写log方法了。

下面是采用协议后的完整 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())")
        }
    }
}

现在你可以像期望中那样使用协议了:

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

调用者的 API 可见度

这个方法有一个弊端,不能简便清晰的通过访问控制给使用者指出协议中的 logwriteLogEntry 的作用。理想情况下,调用者使用协议时不会看到 writeLogEntry 方法,然而部署协议的对象可能同时看到 logwriteLogEntry 。如果你不想让调用者创建自己的 Logger 类型,只能使用 publicinternalprivate。当然,通过文档说明情况也是一个选择。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • (http://www.cnblogs.com/zhangchenliang/p/4546352.html) 1、...
    凌雲木阅读 2,332评论 0 2
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,817评论 1 13
  • 又是教师节了,算来,读书廿载,遇见过的老师,应该不下五六十位了,有些短期执教,有些长期授业,有些名字刻骨铭心,有些...
    一袍风阅读 823评论 0 1