考虑下面的例子:

import Foundation
import os.log

class OSLogWrapper {

    func logDefault(_ message: StaticString, _ args: CVarArg...) {
        os_log(message, type: .default, args)
    }

    func testWrapper() {
        logDefault("WTF: %f", 1.2345)
    }
}

如果我创建一个OSLogWrapper的新实例并调用testWrapper()

let logger = OSLogWrapper()
logger.testWrapper()

我在Xcode控制台中获得以下输出:

2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000

我已经判断了我能想到的所有东西,我不知道这里到底出了什么问题.查看文档并没有产生任何帮助.

推荐答案

编译器通过将每个参数强制转换为声明的可变类型,将其打包为该类型的Array,并将该数组传递给可变函数来实现可变参数.在testWrapper的情况下,声明的变量类型是CVarArg,所以当testWrapper调用logDefault时,这是在幕后发生的事情:testWrapper1.2345转换为CVarArg,创建Array<CVarArg>,并将其作为args传递给logDefault.

然后logDefault调用os_log,将其作为参数传递给Array<CVarArg>.This is the bug in your code.这个错误很微妙.问题是os_log不接受Array<CVarArg>的论点;os_log本身就是CVarArg的变量.因此,Swift 将args(ANArray<CVarArg>)铸造到CVarArg,将CVarArg铸造到anotherArray<CVarArg>.结构如下所示:

Array<CVarArg> created in `logDefault`
  |
  +--> CVarArg (element at index 0)
         |
         +--> Array<CVarArg> (created in `testWrapper`)
                |
                +--> CVarArg (element at index 0)
                       |
                       +--> 1.2345 (a Double)

然后logDefault通过这个新的Array<CVarArg>os_log.所以你要求os_log格式化它的第一个元素,有点像Array<CVarArg>,使用%f,这是胡说八道,你碰巧得到0.000000作为输出.(我之所以说"有点",是因为这里有一些微妙之处,我稍后会解释.)

所以,logDefault将其传入的Array<CVarArg>作为可能的多个可变参数之一传递给os_log,但实际上,你想要logDefault做的是将传入的Array<CVarArg>作为整个可变参数集传递给os_log,而不需要重新包装它.在其他语言中,这有时被称为"参数散乱".

遗憾的是,Swift 还没有任何用于参数散乱的语法.在《快速进化》(Swift Evolution,in this thread, for example)一书中,人们不止一次讨论过这个问题,但目前还没有一个解决方案.

解决这个问题的通常方法是寻找一个伴随函数,该函数将已绑定的可变参数作为单个参数.通常情况下,伴随函数名中会添加一个v.例如:

  • va_list(a,C)的等效值
  • NSLog(可变)和NSLogv(取va_list)
  • -[NSString initWithFormat:](可变)和-[NSString WithFormat:arguments:](取va_list)

所以你可以去找一个os_logv.遗憾的是,你找不到.os_log没有记录在案的附带文件可以接受预先绑定的参数.

此时,您有两个选择:

  • 放弃用你自己的变量包装器包装os_log,因为根本没有好方法,或者

  • 接受卡姆兰的建议(在他对你的问题的评论中),用%@而不是%f.但是请注意,在消息字符串中只能有一个%@(没有其他格式说明符),因为您只将一个参数传递给os_log.输出如下所示:

    2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
        "1.2345"
    )
    

你也可以在https://bugreport.apple.com点提交一个增强请求雷达,请求os_logv功能,但你不应该期望它很快就能实现.

就这样.做这两件事中的一件,也许打个雷达,然后继续你的生活.认真地别在这儿看书了.在这条线之后没有什么好东西.


好吧,你一直在读.让我们在os_log的引擎盖下窥视一下.事实证明,Swift os_log功能的实现是public Swift source code:

@_exported import os
@_exported import os.log
import _SwiftOSOverlayShims

@available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
public func os_log(
  _ type: OSLogType,
  dso: UnsafeRawPointer = #dsohandle,
  log: OSLog = .default,
  _ message: StaticString,
  _ args: CVarArg...)
{
  guard log.isEnabled(type: type) else { return }
  let ra = _swift_os_log_return_address()

  message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
    // Since dladdr is in libc, it is safe to unsafeBitCast
    // the cstring argument type.
    buf.baseAddress!.withMemoryRebound(
      to: CChar.self, capacity: buf.count
    ) { str in
      withVaList(args) { valist in
        _swift_os_log(dso, ra, log, type, str, valist)
      }
    }
  }
}

结果是os_log的一个版本,叫做_swift_os_log,它采用了预绑定的参数.Swift包装器使用withVaList(有文档记录)将Array<CVarArg>转换为va_list,并将其传递给_swift_os_log_swift_os_log本身也是public Swift source code的一部分.我不想在这里引用它的代码,因为它很长,我们实际上不需要看它.

不管怎样,即使没有记录,我们实际上可以拨打_swift_os_log.我们基本上可以复制os_log的源代码,并将其转换为您的logDefault函数:

func logDefaultHack(_ message: StaticString, dso: UnsafeRawPointer = #dsohandle, _ args: CVarArg...) {
    let ra = _swift_os_log_return_address()
    message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
        buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { str in
            withVaList(args) { valist in
                _swift_os_log(dso, ra, .default, .default, str, valist)
            }
        }
    }
}

而且很有效.测试代码:

func testWrapper() {
    logDefault("WTF: %f", 1.2345)
    logDefault("WTF: %@", 1.2345)
    logDefaultHack("Hack: %f", 1.2345)
}

输出:

2018-06-20 02:22:56.131875-0500 test[39313:6086331] WTF: 0.000000
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
    "1.2345"
)
2018-06-20 02:22:56.132807-0500 test[39313:6086331] Hack: 1.234500

我能推荐这个解决方案吗?No.Hell No.os_log的内部是一个实现细节,可能会在Swift的future 版本中发生变化.所以不要像这样依赖他们.但不管怎么说,从被子里看还是很有趣的.


最后一件事.为什么编译器不抱怨将Array<CVarArg>转换为CVarArg?为什么卡姆兰的建议(使用%@)有效?

事实证明,这些问题都有相同的答案:这是因为Array可以"桥接"到Objective-C对象.明确地:

这种静默转换可能通常是一个错误(就像在您的例子中一样),因此编译器应该对此发出警告,并允许您使用显式强制转换(例如args as CVarArg)来静默警告.如果你愿意的话,你可以在https://bugs.swift.org点提交错误报告.

Swift相关问答推荐

Swift:连接两个数组而不重复共享后缀/前缀

Vapor - 流利的,将对象保存到 PostgreSQL

组合 flatMap 不会返回预期的上下文结果类型

iPhone 不可用.请重新连接设备. / iOS 14.2 - Xcode 12.1

Xcode 在上传时卡在“将包上传到 App Store”阶段

如何运行导致框架运行时错误“代码签名无效”的 iOS 应用程序

更新我的 pod 导致 GoogleDataTransport Umbrella 标头出现错误

切换 swiftUI toggle() 时如何触发操作?

如何使 TextField 右对齐(尾随)

下拉刷新SwiftUI中的数据

SwiftUI 中的约束有什么用?

Swift(UI) 中的“some”关键字是什么?

Xcode 一直显示:-1: SWIFT_VERSION '5.0' is unsupported, 支持的版本是:3.0, 4.0, 4.2.任何解决方案?

Swift:“Hashable.hashValue”作为协议要求已被弃用;

将 Apple 表情符号(字符串)转换为 UIImage

AVAudioSession setCategory Swift 4.2 iOS 12 - 静音播放声音

在 Swift 中计算两个日期之间的差异

Firebase InstanceID.instanceID().token() 方法已弃用

设置 statusbarStyle(在 iOS 9.0 中已弃用)

在 WKWebView 中加载本地 Web 文件和资源