你其实真的不懂print("Hello,world")

http://www.jianshu.com/p/abb55919c453ios

debugPrint在发布的版本里也 会输出
debugPrint只是更倾向于输出对象的调试信息。不论是开发环境仍是测试环境都会输出的git

 

在进行调试的时候,咱们有时会把一个变量自身,或其成员属性的值打印出来以检查是否符合咱们的预期。或者干脆简单一些,直接print整个变量,不一样于C++的std::cout,若是你调用print(value),无论value是什么类型程序都不会报错,并且大多数时候你能得到比较全面的、可读的输出结果。若是这引发了你对print函数的好奇,接下来咱们共同研究如下几个问题:程序员

  1. print("hello, world")print(123)的执行原理
  2. StreamableOutputStreamType协议
  3. CustomStringConvertibleCustomDebugStringConvertible协议
  4. 为何字符串的初始化函数中能够传入任何类型的参数
  5. printdebugPrint函数的区别

本文的demo地址在个人github,读者能够下载下来自行把玩,若是以为有收获还望给个star鼓励一下。github

字符串输出

有笑话说每一个程序员的第一行代码都是这样的:编程

print("Hello, world!")

先别急着笑,您还真不必定知道这行代码是怎么运行的。swift

首先,print函数支持重载,Swift定义了两个版本的实现。其中简化版的print将输出流指定为标准输出流,咱们忽略playground相关的代码,来看一下上面那一行代码中的print函数是怎么定义的,在不改编代码逻辑的前提下,为了方便阅读,我作了一些排版方面的修改:数组

// 简化版print函数,经过terminator = "\n"可知print函数默认会换行 public func print(items: Any..., separator: String = " ", terminator: String = "\n") { var output = _Stdout() _print(items, separator: separator, terminator: terminator, toStream: &output) } // 完整版print函数,参数中多了一个outPut参数 public func print<Target: OutputStreamType>(items: Any...,separator: String = " ", terminator: String = "\n", inout toStream output: Target) { _print(items, separator: separator, terminator: terminator, toStream: &output) }

二者的区别在于完整版print函数须要咱们提供output参数,而咱们以前调用的显然是第一个print函数,在函数中建立了output变量。这两个版本的print函数都会调用内部的_print函数。less

经过这一层封装,真正的核心操做在_print函数中,而对外则提供了一个重载的,高度可定制的print函数,接下来咱们看一看这个内部的_print函数是如何实现的,为了阅读方便我删去了读写锁相关的代码,它的核心步骤以下:ide

internal func _print<Target: OutputStreamType>( items: [Any], separator: String = " ", terminator: String = "\n", inout toStream output: Target ) { var prefix = "" for item in items { output.write(prefix) // 每两个元素之间用separator分隔开 _print_unlocked(item, &output) // 这句话实际上是核心 prefix = separator } output.write(terminator) // 终止符,一般是"\n" }

这个函数有四个参数,显然第一个和第四个参数是关键。也就是说咱们只关心要输出什么内容,以及输出到哪里,至于输出格式则是次要的。因此_print函数主要是处理了输出格式问题,以及把第一个参数(它是一个数组)中的每一个元素,都写入到output中。经过目前的分析,咱们已经明白文章开头的print("Hello, world!")其实等价于:函数

var output = _Stdout() // 这个是output的默认值 output.write("") // prefix是一个空字符串 _print_unlocked("Hello, world!", &output)

你必定已经很好奇这个反复出现的output是什么了,其实在整个print函数的执行过程当中OutputStreamType类型的output变量都是关键。另外一个略显奇怪的点在于,一样是输出空字符串和"Hello, world!",居然调用了两个不一样的方法。接下来咱们首先分析OutputStreamType协议以及其中的write方法,再来研究为何还须要_print_unlocked函数:

public protocol OutputStreamType { mutating func write(string: String) } internal struct _Stdout : OutputStreamType { mutating func write(string: String) { for c in string.utf8 { _swift_stdlib_putchar(Int32(c)) } } }

简单来讲,OutputStreamType表示了一个输出流,也就说你要把字符串输出到哪里。若是你有过C++编程经验,那你必定知道#include <iostream>这个库文件,以及coutcin这两个标准输出、输入流。

OutputStreamType协议中定义了write方法,它表示这个流是如何把字符串写入的。好比标准输出流_Stdout的处理方法就是在字符串的UFT-8编码视图下,把每一个字符转换成Int32类型,而后调用_swift_stdlib_putchar函数。这个函数在LibcShims.cpp文件中定义,能够理解为一个适配器,它内部会直接调用C语言的putchar函数。

Ok,已经分析到C语言的putchar函数了,再往下就没必要说了(我也不懂putchar是怎么实现的)。如今咱们把思路拉回到另外一个把字符串打印到屏幕上的函数——_print_unlocked上,它的定义以下:

internal func _print_unlocked<T, TargetStream : OutputStreamType>(value: T, inout _ target: TargetStream) { if case let streamableObject as Streamable = value { streamableObject.writeTo(&target) return } if case let printableObject as CustomStringConvertible = value { printableObject.description.writeTo(&target) return } if case let debugPrintableObject as CustomDebugStringConvertible = value { debugPrintableObject.debugDescription.writeTo(&target) return } _adHocPrint(value, &target, isDebugPrint: false) }

在调用最后的_adHocPrint方法以前,进行了三次判断,分别判断被输出的value(在咱们的例子中是字符串"Hello, world!")是否实现了指定的协议,若是是,则调用该协议下的writeTo方法并提早返回,而最后的_adHocPrint方法则用于确保,任何类型都有默认的输出。稍后我会经过一个具体的例子来解释。

这里咱们主要看一下Streamable协议,关于另外两个协议的介绍您能够参考《第七章——字符串(字符串调试)》Streamable协议定义以下:

/// A source of text streaming operations. `Streamable` instances can /// be written to any *output stream*. public protocol Streamable { func writeTo<Target : OutputStreamType>(inout target: Target) }

根据官方文档的定义,Streamable类型的变量能够被写入任何一个输出流中。String类型实现了Streamable协议,定义以下:

extension String : Streamable { /// Write a textual representation of `self` into `target`. public func writeTo<Target : OutputStreamType>(inout target: Target) { target.write(self) } }

看到这里,print("Hello, wrold!")的完整流程就算所有讲完了。还留下一个小疑问,一样是输出字符串,为何不直接调用write函数,而是大费周章的调用_print_unlocked函数?这个问题在讲解完_adHocPrint函数的原理后您就能理解了。

须要强调一点,千万不要把writeTo函数和write函数弄混淆了。write函数是输出流,也就是OutputStreamType类型的方法,用于输出内容到屏幕上,好比_Stdoutwrite函数实际上会调用C语言的putchar函数。

writeTo函数是可输出类型(也就是实现了Streamable协议)的方法,它用于将该类型的内容输出到某个流中。

输出字符串的过程当中,这两个函数的关系能够这样简单理解:

内容.writeTo(输出流) = 输出流.write(内容),通常在前者内部执行后者

字符串不只是可输出类型(Streamable),同时自身也是输出流(OutputStreamType),它是Swift标准库中的惟一一个输出流,定义以下:

extension String : OutputStreamType { public mutating func write(other: String) { self += other } }

在输出字符串的过程当中,咱们用到的是字符串可输出的特性,至于它做为输出流的特性,会在稍后的例子中进行讲解。

实战

接下来咱们经过几个例子来加深对print函数执行过程的理解。

1、字符串输出

仍是用文章开头的例子,咱们分析一下其背后的步骤:

print("Hello, world!")
  1. 调用不带output参数的print函数,函数内部生成_Stdout类型的输出流,调用_print函数
  2. _print函数中国处理完separatorterminator等格式参数后,调用_print_unlocked函数处理字符串输出。
  3. _print_unlocked函数的第一个if判断中,由于字符串类型实现了Streamable协议,因此调用字符串的writeTo函数,写入到输出流中。
  4. 根据字符串的writeTo函数的定义,它在内部调用了输出流的write方法
  5. _Stdout在其write方法中,调用C语言的putchar函数输出字符串的每一个字符

2、标准库中其余类型输出

若是要输出一个整数,彷佛和输出字符串同样简单,但其实并非这样,咱们来分析一下具体的步骤:

print(123)
  1. 调用不带output参数的print函数,函数内部生成_Stdout类型的输出流,调用_print函数
  2. _print函数中国处理完separatorterminator等格式参数后,调用_print_unlocked函数处理字符串输出。
  3. 截止目前和输出字符串一致,不过Int类型(以及其余除了和字符有关的几乎全部类型)没有实现Streamable协议,它实现的是CustomStringConvertible协议,定义了本身的计算属性description
  4. description是一个字符串类型,调用字符串的writeTo方法此前已经讲过,就再也不赘述了。

3、自定义结构体输出

咱们简单的定义一个结构体,而后尝试使用print方法输出这个结构体:

struct Person { var name: String private var age: Int init(name: String, age: Int) { self.name = name self.age = age } } let kt = Person(name: "kt", age: 21) print(kt) // 输出结果:PersonStruct(name: "kt", age: 21)

输出结果的可读性很是好,咱们来分析一下其中的步骤:

  1. 调用不带output参数的print函数,函数内部生成_Stdout类型的输出流,调用_print函数
  2. _print函数中国处理完separatorterminator等格式参数后,调用_print_unlocked函数处理字符串输出。
  3. _print_unlocked中调用_adHocPrint函数
  4. switch语句匹配,参数类型是结构体,执行对应case语句中的代码

前两步和输出字符串如出一辙,不过因为是自定义的结构体,并且没有实现任何协议,因此在第三步骤没法知足任意一个if判断。因而调用_adHocPrint函数,这个函数能够确保任何类型都能在print方法中较好的工做。在_adHocPrint函数中也有switch判断,若是被输出的变量是一个结构体,则会执行对应的操做,代码以下:

internal func _adHocPrint<T, TargetStream : OutputStreamType>( value: T, inout _ target: TargetStream, isDebugPrint: Bool ) { func printTypeName(type: Any.Type) { // Print type names without qualification, unless we're debugPrint'ing. target.write(_typeName(type, qualified: isDebugPrint)) } let mirror = _reflect(value) switch mirror { case is _TupleMirror: // 这里定义了输出元组类型的方法 case is _StructMirror: printTypeName(mirror.valueType) target.write("(") var first = true for i in 0..<mirror.count { if first { first = false } else { target.write(", ") } let (label, elementMirror) = mirror[i] print(label, terminator: "", toStream: &target) target.write(": ") debugPrint(elementMirror.value, terminator: "", toStream: &target) } target.write(")") case let enumMirror as _EnumMirror: // 这里定义了输出枚举类型的方法 case is _MetatypeMirror: // 这里定义了输出元类型的方法 default: // 若是都不是就进行默认输出 } }

您能够仔细阅读case is _StructMirror这一段,它的逻辑和结构体的输出结果是一致的。若是此前定义的不是结构体而是类,那么获得的结果只是Streamable.PersonStruct,根据default段中的代码也很容易理解。

正是因为_adHocPrint方法,不只仅是字符串和Swift内置的类型,任何自定义类型均可以被输出。如今您应该已经明白,为何输出prefix用的是write方法,而输出字符串"Hello, world!"要用_print_unlocked函数了吧?这是由于在那个时候,编译器还没法断定输出内容的类型。

4、万能的String

不知道您有没有注意到一个细节,String类型的初始化函数是一个没有类型约束的范型函数,也就是说任意类型均可以用来建立一个字符串,这是由于String类型的初始化函数有一个重载为:

extension String { public init<T>(_ instance: T) { self.init() _print_unlocked(instance, &self) } }

这里的字符串不是一个可输出类型,而是做为输出流来使用。_print_unlockedinstance输出到字符串流中。

调试输出

_print_unlocked函数中,咱们看到它在输出默认值以前,一共会进行三次判断。依次检验被输出的变量是否实现了StreamableCustomStringConvertibleCustomDebugStringConvertible,只要实现了协议,就会进行相应的处理并提早退出函数。

这三个协议的优先级依次下降,也就是若是一个类型既实现了Streamable协议又实现了CustomStringConvertible协议,那么将会优先调用Streamable协议中定义的writeTo方法。从这个优先级顺序来看,print函数更倾向于字符串的正常输出而非调试输出。

Swift中还有一个debugPrint函数,它更倾向于输出字符串的调试信息。调用这个函数时,三个协议的优先级彻底相反:

extension PersonDebug: CustomStringConvertible, CustomDebugStringConvertible { var description: String { return "In CustomStringConvertible Protocol" } var debugDescription: String { return "In CustomDebugStringConvertible Protocol" } } let kt = PersonDebug(name: "kt", age: 21) print(kt) // "In CustomStringConvertible Protocol" debugPrint(kt) //"In CustomDebugStringConvertible Protocol"

刚刚咱们说到,建立字符串时能够传入任意的参数value,最后的字符串的值和调用print(value)的结果彻底相同,这是由于二者都会调用_print_unlocked方法。对应到debugPrint函数则有:

extension String { public init<T>(reflecting subject: T) { self.init() debugPrint(subject, terminator: "", toStream: &self) } }

简单来讲,在_adHocPrint函数以前,这两个输出函数的调用栈是彻底平行的关系,下面这张图做为二者的比较,也是整篇文章的总结,纯手绘,美死早:


print与debugPring调用栈