你其實真的不懂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調用棧
相關文章
相關標籤/搜索