http://www.jianshu.com/p/abb55919c453ios
debugPrint在發佈的版本里也 會輸出
debugPrint只是更傾向於輸出對象的調試信息。不論是開發環境仍是測試環境都會輸出的git
在進行調試的時候,咱們有時會把一個變量自身,或其成員屬性的值打印出來以檢查是否符合咱們的預期。或者乾脆簡單一些,直接print
整個變量,不一樣於C++的std::cout
,若是你調用print(value)
,無論value
是什麼類型程序都不會報錯,並且大多數時候你能得到比較全面的、可讀的輸出結果。若是這引發了你對print
函數的好奇,接下來咱們共同研究如下幾個問題:程序員
print("hello, world")
和print(123)
的執行原理Streamable
和OutputStreamType
協議CustomStringConvertible
和CustomDebugStringConvertible
協議print
和debugPrint
函數的區別本文的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>
這個庫文件,以及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
函數處理字符串輸出。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
函數前兩步和輸出字符串如出一轍,不過因爲是自定義的結構體,並且沒有實現任何協議,因此在第三步驟沒法知足任意一個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
類型的初始化函數有一個重載爲:
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
函數以前,這兩個輸出函數的調用棧是徹底平行的關係,下面這張圖做爲二者的比較,也是整篇文章的總結,純手繪,美死早: