http://www.jianshu.com/p/abb55919c453
debugPrint在發布的版本里也 會輸出
debugPrint只是更傾向於輸出對象的調試信息。不管是開發環境還是測試環境都會輸出的
在進行調試的時候,我們有時會把一個變量自身,或其成員屬性的值打印出來以檢查是否符合我們的預期。或者干脆簡單一些,直接print
整個變量,不同於C++的std::cout
,如果你調用print(value)
,不管value
是什么類型程序都不會報錯,而且大多數時候你能獲得比較全面的、可讀的輸出結果。如果這引起了你對print
函數的好奇,接下來我們共同研究以下幾個問題:
print("hello, world")
和print(123)
的執行原理Streamable
和OutputStreamType
協議CustomStringConvertible
和CustomDebugStringConvertible
協議- 為什么字符串的初始化函數中可以傳入任何類型的參數
print
和debugPrint
函數的區別
本文的demo地址在我的github,讀者可以下載下來自行把玩,如果覺得有收獲還望給個star鼓勵一下。
字符串輸出
有笑話說每個程序員的第一行代碼都是這樣的:
print("Hello, world!")
先別急着笑,您還真不一定知道這行代碼是怎么運行的。
首先,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
函數。
通過這一層封裝,真正的核心操作在_print
函數中,而對外則提供了一個重載的,高度可定制的print
函數,接下來我們看一看這個內部的_print
函數是如何實現的,為了閱讀方便我刪去了讀寫鎖相關的代碼,它的核心步驟如下:
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>
這個庫文件,以及cout
和cin
這兩個標准輸出、輸入流。
在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
類型的方法,用於輸出內容到屏幕上,比如_Stdout
的write
函數實際上會調用C語言的putchar
函數。
writeTo
函數是可輸出類型(也就是實現了Streamable
協議)的方法,它用於將該類型的內容輸出到某個流中。
輸出字符串的過程中,這兩個函數的關系可以這樣簡單理解:
內容.writeTo(輸出流) = 輸出流.write(內容),一般在前者內部執行后者
字符串不僅是可輸出類型(Streamable),同時自身也是輸出流(OutputStreamType),它是Swift標准庫中的唯一一個輸出流,定義如下:
extension String : OutputStreamType { public mutating func write(other: String) { self += other } }
在輸出字符串的過程中,我們用到的是字符串可輸出的特性,至於它作為輸出流的特性,會在稍后的例子中進行講解。
實戰
接下來我們通過幾個例子來加深對print
函數執行過程的理解。
一、字符串輸出
還是用文章開頭的例子,我們分析一下其背后的步驟:
print("Hello, world!")
- 調用不帶
output
參數的print
函數,函數內部生成_Stdout
類型的輸出流,調用_print
函數 - 在
_print
函數中國處理完separator
和terminator
等格式參數后,調用_print_unlocked
函數處理字符串輸出。 - 在
_print_unlocked
函數的第一個if判斷中,因為字符串類型實現了Streamable
協議,所以調用字符串的writeTo
函數,寫入到輸出流中。 - 根據字符串的
writeTo
函數的定義,它在內部調用了輸出流的write
方法 _Stdout
在其write
方法中,調用C語言的putchar
函數輸出字符串的每個字符
二、標准庫中其他類型輸出
如果要輸出一個整數,似乎和輸出字符串一樣簡單,但其實並不是這樣,我們來分析一下具體的步驟:
print(123)
- 調用不帶
output
參數的print
函數,函數內部生成_Stdout
類型的輸出流,調用_print
函數 - 在
_print
函數中國處理完separator
和terminator
等格式參數后,調用_print_unlocked
函數處理字符串輸出。 - 截止目前和輸出字符串一致,不過Int類型(以及其他除了和字符有關的幾乎所有類型)沒有實現
Streamable
協議,它實現的是CustomStringConvertible
協議,定義了自己的計算屬性description
description
是一個字符串類型,調用字符串的writeTo
方法此前已經講過,就不再贅述了。
三、自定義結構體輸出
我們簡單的定義一個結構體,然后嘗試使用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)
輸出結果的可讀性非常好,我們來分析一下其中的步驟:
- 調用不帶
output
參數的print
函數,函數內部生成_Stdout
類型的輸出流,調用_print
函數 - 在
_print
函數中國處理完separator
和terminator
等格式參數后,調用_print_unlocked
函數處理字符串輸出。 - 在
_print_unlocked
中調用_adHocPrint
函數 - 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
函數了吧?這是因為在那個時候,編譯器還無法判定輸出內容的類型。
四、萬能的String
不知道您有沒有注意到一個細節,String
類型的初始化函數是一個沒有類型約束的范型函數,也就是說任意類型都可以用來創建一個字符串,這是因為String
類型的初始化函數有一個重載為:
extension String { public init<T>(_ instance: T) { self.init() _print_unlocked(instance, &self) } }
這里的字符串不是一個可輸出類型,而是作為輸出流來使用。_print_unlocked
將instance
輸出到字符串流中。
調試輸出
在_print_unlocked
函數中,我們看到它在輸出默認值之前,一共會進行三次判斷。依次檢驗被輸出的變量是否實現了Streamable
、CustomStringConvertible
和CustomDebugStringConvertible
,只要實現了協議,就會進行相應的處理並提前退出函數。
這三個協議的優先級依次降低,也就是如果一個類型既實現了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
函數之前,這兩個輸出函數的調用棧是完全平行的關系,下面這張圖作為兩者的比較,也是整篇文章的總結,純手繪,美死早:
