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


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

debugPrint在發布的版本里也 會輸出
debugPrint只是更傾向於輸出對象的調試信息。不管是開發環境還是測試環境都會輸出的

 

在進行調試的時候,我們有時會把一個變量自身,或其成員屬性的值打印出來以檢查是否符合我們的預期。或者干脆簡單一些,直接print整個變量,不同於C++的std::cout,如果你調用print(value),不管value是什么類型程序都不會報錯,而且大多數時候你能獲得比較全面的、可讀的輸出結果。如果這引起了你對print函數的好奇,接下來我們共同研究以下幾個問題:

  1. print("hello, world")print(123)的執行原理
  2. StreamableOutputStreamType協議
  3. CustomStringConvertibleCustomDebugStringConvertible協議
  4. 為什么字符串的初始化函數中可以傳入任何類型的參數
  5. printdebugPrint函數的區別

本文的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>這個庫文件,以及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函數執行過程的理解。

一、字符串輸出

還是用文章開頭的例子,我們分析一下其背后的步驟:

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函數輸出字符串的每個字符

二、標准庫中其他類型輸出

如果要輸出一個整數,似乎和輸出字符串一樣簡單,但其實並不是這樣,我們來分析一下具體的步驟:

print(123)
  1. 調用不帶output參數的print函數,函數內部生成_Stdout類型的輸出流,調用_print函數
  2. _print函數中國處理完separatorterminator等格式參數后,調用_print_unlocked函數處理字符串輸出。
  3. 截止目前和輸出字符串一致,不過Int類型(以及其他除了和字符有關的幾乎所有類型)沒有實現Streamable協議,它實現的是CustomStringConvertible協議,定義了自己的計算屬性description
  4. 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)

輸出結果的可讀性非常好,我們來分析一下其中的步驟:

  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函數了吧?這是因為在那個時候,編譯器還無法判定輸出內容的類型。

四、萬能的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調用棧
 
     


    免責聲明!

    本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



     
    粵ICP備18138465號   © 2018-2025 CODEPRJ.COM