做者:Mike Ash,原文連接,原文日期:2016-01-29
譯者:ray16897188;校對:Channe;定稿:千葉知風html
Swift 的類對大多數剛接觸編程語言的人來講是很容易理解的,它們和其餘語言中的類十分相似。不管你是從 Objective-C、Java 仍是 Ruby 過來的,在 Swift 中對於類的使用並沒有太大區別。而 Swift 中的結構體就是另一回事兒了,它們有點兒像類,可是它們是值類型,尚未繼承,另外我老是聽到這個什麼 copy-on-write(寫入時複製)的說法。那麼 Swift 中的結構體是存在哪裏?它們是怎麼個工做原理?今天咱們來仔細研究一下如何在內存中保存和操做結構體。編程
我建了一個有兩個文件的程序來探究一下結構體在內存中是怎樣存儲的。對這個測試程序採用 optimizations enabled 選項編譯,並取消 whole-module optimization 選項。此測試是讓一個文件調用一個文件,這就會防止編譯器將全部東西都內聯,從而讓咱們能更清楚的看明白東西都存在哪兒,以及數據在函數間如何傳遞。swift
從建立一個三個元素的結構體開始:安全
struct ExampleInts { var x: Int var y: Int var z: Int }
又寫了三個函數,都是各自接收一個結構體的實例,而後分別返回該實例的一個字段(field):框架
func getX(parameter: ExampleInts) -> Int { return parameter.x } func getY(parameter: ExampleInts) -> Int { return parameter.y } func getZ(parameter: ExampleInts) -> Int { return parameter.z }
在另外一個文件中建立了一個結構體實例,而後調用全部的 get 函數:編程語言
func testGets() { let s = ExampleInts(x: 1, y: 2, z: 3) getX(s) getY(s) getZ(s) }
針對 getX,編譯器生成了以下代碼:函數
pushq %rbp movq %rsp, %rbp movq %rdi, %rax popq %rbp retq
查看一下彙編的備忘單,知道參數是按順序被傳進寄存器 rdi、rsi、rdx、rcx、r8 和 r9 中,而後返回值被存放在 rax 中。這裏前兩個指令只是函數序言(function prologue),然後兩個是函數尾聲(function epilogue)。真正作的工做就是 movq %rdi, %rax
:提取第一個參數並將其返回。再看一下 getY:測試
pushq %rbp movq %rsp, %rbp movq %rsi, %rax popq %rbp retq
基本同樣,只不過它返回的是第二個參數。那 getZ 呢?優化
pushq %rbp movq %rsp, %rbp movq %rdx, %rax popq %rbp retq
仍是,基本都同樣,但返回的是第三個參數。從這咱們能夠看出來每一個單獨的結構體元素都是被看作獨立的參數,被單獨的傳遞進函數中。在接收端挑出某個元素,僅僅就是選擇它所在的相應的寄存器。翻譯
在調用點驗證一下。下面是 testGets 的編譯器生成碼:
pushq %rbp movq %rsp, %rbp movl $1, %edi movl $2, %esi movl $3, %edx callq __TF4main4getXFVS_11ExampleIntsSi movl $1, %edi movl $2, %esi movl $3, %edx callq __TF4main4getYFVS_11ExampleIntsSi movl $1, %edi movl $2, %esi movl $3, %edx popq %rbp jmp __TF4main4getZFVS_11ExampleIntsSi
能夠看出這個結構體實例的是直接在組建於參數寄存器上的。(edi、esi 和 edx 寄存器分別是 rdi、rsi 和 rdx 對應的低 32 bit 帶寬版本。)這樣甚至不用在調用途中操心值的額外存儲,只需每次調用時重建這個結構體的實例就行了。由於編譯器明確的知道寄存器中的內容,這就能夠大大的改變 Swift 的代碼編寫方式。注意到對 getZ 的調用和對 getX、getY 的調用有些許不一樣:因爲它是該函數中的最後一部分,編譯器以尾調用(tail call)的形式將其生成,清空本地調用棧幀(local call frame),而後讓 getZ 直接返回到 testGets 函數被調用的地方。
再讓咱們看一下當編譯器不知道結構體的內容時會生成怎樣的代碼。下面是這個 test 函數的變體,從其餘的地方得到結構體的實例:
func testGets2() { let s = getExampleInts() getX(s) getY(s) getZ(s) }
getExampleInts 建立了一個結構體實例而後將其返回,但這個函數是在另外一個文件中,因此優化 testGets2 的時候編譯器是不知道發生了什麼狀況的。函數以下:
func getExampleInts() -> ExampleInts { return ExampleInts(x: 1, y: 2, z: 3) }
當編譯器不知道結構體的內容時 testGets2 會生成怎樣的代碼呢?
pushq %rbp movq %rsp, %rbp pushq %r15 pushq %r14 pushq %rbx pushq %rax callq __TF4main14getExampleIntsFT_VS_11ExampleInts movq %rax, %rbx movq %rdx, %r14 movq %rcx, %r15 movq %rbx, %rdi movq %r14, %rsi movq %r15, %rdx callq __TF4main4getXFVS_11ExampleIntsSi movq %rbx, %rdi movq %r14, %rsi movq %r15, %rdx callq __TF4main4getYFVS_11ExampleIntsSi movq %rbx, %rdi movq %r14, %rsi movq %r15, %rdx addq $8, %rsp popq %rbx popq %r14 popq %r15 popq %rbp jmp __TF4main4getZFVS_11ExampleIntsSi
因爲編譯器不能在每一個階段都直接將相應的值重現,它就得把這些值存起來。結構體的三個元素被放到 rbx、r14 和 r15 寄存器中,並在每次調用時從這些寄存器裏將值加載到參數寄存器中。調用者會保存這三個寄存器,就是說在調用過程當中它們存的值會被持有。而後和以前同樣,編譯器也對 getZ 生成了尾調用,以及一些更昂貴的預先清理。
函數的開始部分調用了 getExampleInts 並從 rax、rdx 和 rcx 中加載了其中的值。顯然結構體的值是從這些寄存器裏返回的,看看 getExampleInts 函數來確認下:
pushq %rbp movl $1, %edi movl $2, %esi movl $3, %edx popq %rbp jmp __TFV4main11ExampleIntsCfMS0_FT1xSi1ySi1zSi_S0_
這代碼把值 一、2 和 3 放進參數寄存器中,而後調用結構體的構造器。下面是構造器的生成碼:
pushq %rbp movq %rsp, %rbp movq %rdx, %rcx movq %rdi, %rax movq %rsi, %rdx popq %rbp retq
夠清楚了,它向 rax、rdx 和 rcx 中返回三個值。備忘單並未說起往多個寄存器中返回多個值。那官方的PDF呢?裏面說到了能夠往 rax 和 rdx 中返回兩個值,卻沒說能夠給 rcx 返回第三個值。而上面的代碼仍是很明確的。新語言有趣的地方就在這兒,它不必定非按老規矩來。要是和C語言聯調就得按傳統規範,可是 Swift 和 Swift 之間的調用就能夠玩新路子了。
那 inout 參數呢?若是它是像咱們在 C 中所熟悉的那樣,結構體就會被安置在內存中,而後傳過去一個指針。下面是兩個 test 函數(固然是在兩個不一樣文件裏的):
func testInout() { var s = getExampleInts() totalInout(&s) } func totalInout(inout parameter: ExampleInts) -> Int { return parameter.x + parameter.y + parameter.z }
下面是 testInout 的生成碼:
pushq %rbp movq %rsp, %rbp subq $32, %rsp callq __TF4main14getExampleIntsFT_VS_11ExampleInts movq %rax, -24(%rbp) movq %rdx, -16(%rbp) movq %rcx, -8(%rbp) leaq -24(%rbp), %rdi callq __TF4main10totalInoutFRVS_11ExampleIntsSi addq $32, %rsp popq %rbp retq
函數序言中先建立了一個 32 字節的堆棧幀,再調用 getExampleInts,然後的調用把結果的值分別存在偏移量爲 -2四、-16 和 -8 的棧槽(stack slots)中。隨即計算出指向偏移爲 -24 的指針,將其加載到 rdi 參數寄存器中後調用 totalInout。下面是這個函數的生成碼:
pushq %rbp movq %rsp, %rbp movq (%rdi), %rax addq 8(%rdi), %rax jo LBB4_3 addq 16(%rdi), %rax jo LBB4_3 popq %rbp retq LBB4_3: ud2
以上是從傳遞進來的參數加載偏移量所對應的值,合併以後將結果返回到 rax 中。jo 指令作溢出檢查。若是有任一 addq 指令引發溢出,jo 指令會跳轉到 ud2 指令,將程序終結。
能夠看出這正是咱們所想的那樣:把一個結構體傳遞給一個 inout 參數時,該結構體被置進連續的內存中,隨後獲得一個指向該內存的地址。
若是咱們處理的是一些更大的結構體,大到寄存器再也不適合了的話會怎樣呢?下面是一個有十個元素的結構體:
struct TenInts { var elements = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) }
而這面是一個 get 函數,建立一該結構體的實例並將其返回。爲防止內聯它被放在另外一個文件中:
func getHuge() -> TenInts { return TenInts() }
一個獲取該結構體中單個元素的函數:
func getHugeElement(parameter: TenInts) -> Int { return parameter.elements.5 }
最後是一個 test 函數:
func testHuge() { let s = getHuge() getHugeElement(s) }
看下生成碼,從 testHuge 開始:
pushq %rbp movq %rsp, %rbp subq $160, %rsp leaq -80(%rbp), %rdi callq __TF4main7getHugeFT_VS_7TenInts movups -80(%rbp), %xmm0 movups -64(%rbp), %xmm1 movups -48(%rbp), %xmm2 movups -32(%rbp), %xmm3 movups -16(%rbp), %xmm4 movups %xmm0, -160(%rbp) movups %xmm1, -144(%rbp) movups %xmm2, -128(%rbp) movups %xmm3, -112(%rbp) movups %xmm4, -96(%rbp) leaq -160(%rbp), %rdi callq __TF4main14getHugeElementFVS_7TenIntsSi addq $160, %rsp popq %rbp retq
這段代碼(除去函數序言和尾聲)能夠分紅三部分。
第一部分計算出對這個堆棧幀有 -80 偏移量的地址,而後調用 getHuge,把計算出的地址傳參給它。getHuge 函數在源代碼裏沒有任何參數,可是用一個隱式參數返回較大的結構體並不罕見。調用處爲返回值分配儲存空間,然後給把一個指向該分配好的空間的指針傳給隱藏參數。棧中的這塊已分配空間告訴咱們基本就是這樣的。
第二部分將棧偏移-80的地方結構體複製到 -160 的地方。它將這個結構體加載到五個 xmm 寄存器中,每次加載十六字節的片斷,而後把寄存器的內容放回到從 -160 開始的地方。我不大清楚爲何編譯器要弄一個拷貝而不是直接用原始值。我懷疑優化器可能仍是不夠聰明,意識不到它根本就不須要用到拷貝。
第三部分計算出棧偏移 -160 的地址,而後調用 getHugeElement,傳參給它計算出的地址。以前的三個元素的試驗中傳遞的是寄存器中的值,而對於這個更大的結構體,傳遞的是指針。
其餘函數的生成碼確認了這點:結構體是以指針形式傳進傳出的,並存活在棧中。從 getHugeElement 開始:
pushq %rbp movq %rsp, %rbp movq 40(%rdi), %rax popq %rbp retq
加載了離傳入參數 40 個偏移量的內容。每一個元素爲 8 字節,偏移量是 40 就是第 5 個元素,該函數返回這個值。
getHuge 函數:
pushq %rbp movq %rsp, %rbp pushq %rbx subq $88, %rsp movq %rdi, %rbx leaq -88(%rbp), %rdi callq __TFV4main7TenIntsCfMS0_FT_S0_ movups -88(%rbp), %xmm0 movups -72(%rbp), %xmm1 movups -56(%rbp), %xmm2 movups -40(%rbp), %xmm3 movups -24(%rbp), %xmm4 movups %xmm0, (%rbx) movups %xmm1, 16(%rbx) movups %xmm2, 32(%rbx) movups %xmm3, 48(%rbx) movups %xmm4, 64(%rbx) movq %rbx, %rax addq $88, %rsp popq %rbx popq %rbp retq
和上面的 testHuge 很像:分配棧空間,調用一個函數,這回是 TenInts 構造器函數,而後把返回值複製到它最終的地方:隱式參數傳進來的指針所指向的地址。
都說到這兒了,看一下 TenInts 構造器吧:
pushq %rbp movq %rsp, %rbp movq $1, (%rdi) movq $2, 8(%rdi) movq $3, 16(%rdi) movq $4, 24(%rdi) movq $5, 32(%rdi) movq $6, 40(%rdi) movq $7, 48(%rdi) movq $8, 56(%rdi) movq $9, 64(%rdi) movq $10, 72(%rdi) movq %rdi, %rax popq %rbp retq
相似於另外一個函數,它也是用了一個指向新結構體的隱式指針做爲參數,而後把從 1 到 10 這些值存儲好,再返回。
建立這些test案例時我遇到過一個有意思的地方。這是一個 test 函數,調用了三次 getHugeElement:
func testThreeHuge() { let s = getHuge() getHugeElement(s) getHugeElement(s) getHugeElement(s) }
其生成碼以下:
pushq %rbp movq %rsp, %rbp pushq %r15 pushq %r14 pushq %r13 pushq %r12 pushq %rbx subq $392, %rsp leaq -120(%rbp), %rdi callq __TF4main7getHugeFT_VS_7TenInts movq -120(%rbp), %rbx movq %rbx, -376(%rbp) movq -112(%rbp), %r8 movq %r8, -384(%rbp) movq -104(%rbp), %r9 movq %r9, -392(%rbp) movq -96(%rbp), %r10 movq %r10, -400(%rbp) movq -88(%rbp), %r11 movq %r11, -368(%rbp) movq -80(%rbp), %rax movq -72(%rbp), %rcx movq %rcx, -408(%rbp) movq -64(%rbp), %rdx movq %rdx, -416(%rbp) movq -56(%rbp), %rsi movq %rsi, -424(%rbp) movq -48(%rbp), %rdi movq %rdi, -432(%rbp) movq %rbx, -200(%rbp) movq %rbx, %r14 movq %r8, -192(%rbp) movq %r8, %r15 movq %r9, -184(%rbp) movq %r9, %r12 movq %r10, -176(%rbp) movq %r10, %r13 movq %r11, -168(%rbp) movq %rax, -160(%rbp) movq %rax, %rbx movq %rcx, -152(%rbp) movq %rdx, -144(%rbp) movq %rsi, -136(%rbp) movq %rdi, -128(%rbp) leaq -200(%rbp), %rdi callq __TF4main14getHugeElementFVS_7TenIntsSi movq %r14, -280(%rbp) movq %r15, -272(%rbp) movq %r12, -264(%rbp) movq %r13, -256(%rbp) movq -368(%rbp), %rax movq %rax, -248(%rbp) movq %rbx, -240(%rbp) movq -408(%rbp), %r14 movq %r14, -232(%rbp) movq -416(%rbp), %r15 movq %r15, -224(%rbp) movq -424(%rbp), %r12 movq %r12, -216(%rbp) movq -432(%rbp), %r13 movq %r13, -208(%rbp) leaq -280(%rbp), %rdi callq __TF4main14getHugeElementFVS_7TenIntsSi movq -376(%rbp), %rax movq %rax, -360(%rbp) movq -384(%rbp), %rax movq %rax, -352(%rbp) movq -392(%rbp), %rax movq %rax, -344(%rbp) movq -400(%rbp), %rax movq %rax, -336(%rbp) movq -368(%rbp), %rax movq %rax, -328(%rbp) movq %rbx, -320(%rbp) movq %r14, -312(%rbp) movq %r15, -304(%rbp) movq %r12, -296(%rbp) movq %r13, -288(%rbp) leaq -360(%rbp), %rdi callq __TF4main14getHugeElementFVS_7TenIntsSi addq $392, %rsp popq %rbx popq %r12 popq %r13 popq %r14 popq %r15 popq %rbp retq
此函數的結構和前面的那個版本相似,調用 getHuge,複製結果,而後調用三次 getHugeElement。每次的調用都再次的複製該結構體,猜想是爲了防止 getHugeElement 發生變更。發現真正有意思的是這些都是使用整型寄存器、每次只複製一個元素,而不是像 testHuge 函數那樣每次往 xmm 寄存器中複製兩個元素。我不肯定是什麼致使編譯器在這裏選擇了整型寄存器,看起來用 xmm 寄存器一次複製兩個元素是更有效率的,生成碼也更簡潔。
我還試驗了很是大的結構體:
struct HundredInts { var elements = (TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts(), TenInts()) } struct ThousandInts { var elements = (HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts(), HundredInts()) } func getThousandInts() -> ThousandInts { return ThousandInts() }
getThousandInts 的生成碼至關的瘋狂:
pushq %rbp pushq %rbx subq $8008, %rsp movq %rdi, %rbx leaq -8008(%rbp), %rdi callq __TFV4main12ThousandIntsCfMS0_FT_S0_ movq -8008(%rbp), %rax movq %rax, (%rbx) movq -8000(%rbp), %rax movq %rax, 8(%rbx) movq -7992(%rbp), %rax movq %rax, 16(%rbx) movq -7984(%rbp), %rax movq %rax, 24(%rbx) movq -7976(%rbp), %rax movq %rax, 32(%rbx) movq -7968(%rbp), %rax movq %rax, 40(%rbx) movq -7960(%rbp), %rax movq %rax, 48(%rbx) movq -7952(%rbp), %rax movq %rax, 56(%rbx) movq -7944(%rbp), %rax movq %rax, 64(%rbx) movq -7936(%rbp), %rax movq %rax, 72(%rbx) ... movq -104(%rbp), %rax movq %rax, 7904(%rbx) movq -96(%rbp), %rax movq %rax, 7912(%rbx) movq -88(%rbp), %rax movups -80(%rbp), %xmm0 movups -64(%rbp), %xmm1 movups -48(%rbp), %xmm2 movups -32(%rbp), %xmm3 movq %rax, 7920(%rbx) movq -16(%rbp), %rax movups %xmm0, 7928(%rbx) movups %xmm1, 7944(%rbx) movups %xmm2, 7960(%rbx) movups %xmm3, 7976(%rbx) movq %rax, 7992(%rbx) movq %rbx, %rax addq $8008, %rsp popq %rbx popq %rbp retq
編譯器爲複製這個結構體生成了兩千多條指令。這種狀況下貌似調用 memcpy 函數很是合適,而我以爲爲這種大的出奇的結構體作優化應該不是編譯器團隊如今的首要目標。
咱們來看看當結構體的字段(struct fields)比整形複雜的多的狀況下會發生什麼。下面有個簡單的類,而後包含了一個結構體:
class ExampleClass {} struct ContainsClass { var x: Int var y: ExampleClass var z: Int }
這裏是一堆試驗的函數(分在兩個不一樣文件中防止內聯):
func testContainsClass() { let s = ContainsClass(x: 1, y: getExampleClass(), z: 3) getClassX(s) getClassY(s) getClassZ(s) } func getExampleClass() -> ExampleClass { return ExampleClass() } func getClassX(parameter: ContainsClass) -> Int { return parameter.x } func getClassY(parameter: ContainsClass) -> ExampleClass { return parameter.y } func getClassZ(parameter: ContainsClass) -> Int { return parameter.z }
從 getters 的生成碼看起,首先是 getClassX:
pushq %rbp movq %rsp, %rbp pushq %rbx pushq %rax movq %rdi, %rbx movq %rsi, %rdi callq _swift_release movq %rbx, %rax addq $8, %rsp popq %rbx popq %rbp retq
三個結構體元素會被傳遞進前三個參數寄存器中,rdi、rsi 和 rdx。該函數想經過把 rdi 中的值移動到 rax 中再返回,但得先記錄一下才行。看上去彷佛是傳入 rsi 的對象引用被持有了,在函數返回以前是必須被釋放掉的。這段生成碼把 rdi 搬進了一個安全的臨時寄存器 rbx,而後將對象引用移動到 rdi,再調用 swift_release 將其釋放。隨後把 rbx 中的值移動到 rax 中,再從函數中返回。
getClassZ 也很相似,除了它是從 rdx,而不是 rdi 中獲取值的:
pushq %rbp movq %rsp, %rbp pushq %rbx pushq %rax movq %rdx, %rbx movq %rsi, %rdi callq _swift_release movq %rbx, %rax addq $8, %rsp popq %rbx popq %rbp retq
getClassY 的生成碼會是特殊的一個,由於它返回的是對象的引用而非一個整型:
pushq %rbp movq %rsp, %rbp movq %rsi, %rax popq %rbp retq
十分簡短!它從 rsi 中取值,該值爲對象引用,而後放進 rax 再將其返回。這裏就不須要記錄,僅僅是數據的拖拽。顯然值傳進來的時候被持有,被返回的時候也被持有,因此這段代碼是無需任何內存管理的。
到目前爲止咱們看到的對這個結構體的處理和前面的對那個有三個整型元素的結構體的處理沒有太大區別,除了對象引用字段傳遞進來是被持有的,必需要由被調用者作釋放處理。記着這一點,咱們來看下 testContainsClass 的生成碼:
pushq %rbp movq %rsp, %rbp pushq %r14 pushq %rbx callq __TF4main15getExampleClassFT_CS_12ExampleClass movq %rax, %rbx movq %rbx, %rdi callq _swift_retain movq %rax, %r14 movl $1, %edi movl $3, %edx movq %rbx, %rsi callq __TF4main9getClassXFVS_13ContainsClassSi movq %r14, %rdi callq _swift_retain movl $1, %edi movl $3, %edx movq %rbx, %rsi callq __TF4main9getClassYFVS_13ContainsClassCS_12ExampleClass movq %rax, %rdi callq _swift_release movl $1, %edi movl $3, %edx movq %rbx, %rsi popq %rbx popq %r14 popq %rbp jmp __TF4main9getClassZFVS_13ContainsClassSi
這個函數作的第一件事就是調用 getExampleClass 來得到結構體中存儲的 ExampleClass 實例,它獲得返回的引用以後將其移動到 rbx 中以安全保留。
接下來調用了 getClassX,爲此它得在參數寄存器中創建一個該結構體的拷貝。兩個整型的字段是很容易的,而對象的字段就須要按照函數所期的那樣被持有。這段代碼對 rbx 中所存的值調用了 swift_retain,而後將其放入 rsi,再把 1 和 3 分別放入 rdi 和 rdx 中,構建出完整的結構體。最後,它調用了 getClassX。
調用 getClassY 也基本同樣,然而 getClassY 返回的是一個須要被釋放的對象,在調用以後這段代碼將返回值移動至 rdi 中,調用 swift_release 來實線所要求的內存管理。
函數調用 getClassZ 做爲其尾調用,因此這裏的生成代碼有些許不一樣。從 getExampleClass 得來的對象引用已被持有,因此它不須要爲這個最後的調用而再被單獨持有。代碼將其放進 rsi 裏,再把 1 和 3 分別放進 rdi 和 rdx 裏,而後作清空棧,跳轉到 getClassZ 作最後的調用。
基本上說,與全是整型的那個結構體相比幾乎沒有變化。惟一的實質的不一樣就是複製一個帶有對象的結構體時須要持有這個對象,銷燬這個結構體時也須要釋放掉這個對象。
Swift 中的結構體存儲根本上講仍是比較簡單,咱們看到的這些也是從C語言中很是簡單的結構體中延續過來的。一個結構體實例在很大程度上能夠看作是一些獨立值的鬆散集合,須要時這些值能夠做爲總體被操做。本地的結構體變量可能會被存儲到棧中,其中的每一個元素可能會被存到寄存器裏,這取決於結構體的大小、寄存器對餘下代碼的利用以及編譯器臨時冒出的什麼點子。小的結構體在寄存器中傳遞和返回,大的結構體經過引用傳遞和返回。結構體在傳遞和返回時會被複制,儘管你能夠用結構體來實現寫入時複製(copy-on-write)的數據類型,可是基本的語言框架仍是會被複制,並且在選擇複製的數據時多多少少有些盲目。
今天就到這兒吧。歡迎再回來閱讀更多的精彩編程技術。Friday Q&A 是由讀者想法驅動的,因此若是你等不及下一期,還有一些但願看到的討論話題,就發郵件過來吧!
以爲這篇文章怎麼樣?我在買一本書,裏面全是這樣的文章。在 iBooks 上和 Kindle 上有售,加上一個能夠直接下載的PDF和ePub格式。還有傳統的紙質版本。點擊這裏查看更多信息
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。