Swift和C/Objective-C混編超詳解

    文章可能寫得有點晚了,Swift語言已經誕生很久的時間了,現在它已經擠掉了OC很大的市場了,但是,總是存在很多老項目,或者是第三方庫還沒有完全翻譯成Swift,因此,混編還是需要的。雖然現在詳解可能有點晚,不過還是希望能寫一篇關於混編的詳細講解,方便那些遇到困惑的童鞋學習和查閱。

    無論是在OC工程裏插入Swift,還是在Swift工程裏插入OC,其實都沒什麼區別,因爲Swift編譯器本來就是用OC寫的,所謂的Swift工程,其背後其實有大量OC代碼參與了編譯,因此,效果是等價的,本文以Swift項目插入OC代碼爲例進行講解。順便強調一下,如果是嵌入純C代碼(.c後綴)的,它和嵌入OC代碼(.m後綴)完全相同,只是不能夠使用OC語法和函數庫而已,因此本文的內容完全適用。至於與C++混編的問題,必須要OC++文件作爲橋樑(.mm後綴),所以,Swift與C++混編問題可以拆解爲Swift與OC混編,以及OC與C++混編問題,關於OC與C++混編問題,請移步至本人早些日子的播客文章,有一篇詳細介紹C、C++和OC混編的問題,因此,在本文不再贅述,本文只針對於Swift與OC混編。

    重要!!:本文很多地方用到了“轉化”“轉換”“變成”“等價於”這樣的詞語,只是說,在接口調用時,可以視爲在這個語言中是這樣寫的,但是,並不是說兩種寫法等價,或者說真的有做轉換,代碼本身是沒有進行轉換的,它們都是獨自編譯成機器碼,最後整體鏈接的,並不存在兩種語言的轉化過程,這個請讀者一定要明白,不要被誤導!具體來說,文中說OC中的函數[OC內容]會被轉化爲Swift中的[Swift內容],其實指的是,如果想在Swift中調用OC的函數的話,我們可以理解爲,調用了Swift中的[Swift內容]這中形式的函數,但其實在代碼中並不會出現[Swift內容]這樣的代碼。

    重要的事情要再強調一遍!我說的語言1中的A轉換成語言2中的B,意思是在語言2中要想調用語言1中的A,可以當做是調用了語言2中的B。而事實上並沒有B這個東西的存在!切記切記!

    例如,我們的工程中主流程存在於main.swift中,然後,我們創建了test.m和test.h,第一次在Swift工程中嵌入.c或者.m文件時Xcode可能會自動生成一個test-Bridging-Header.h的文件,如果沒有自動生成,我們需要手動生成,注意,將test替換爲任意的名字,後面的部分是不能夠改變的,並且,並不需要爲每個OC文件都配一個Bridge頭文件,工程中只需出現一個即可(當然了,你想根據代碼的內容創建多個也是完全可以的)。這個Bridge頭文件的作用是什麼呢?OC代碼中,全局變量、全局函數都是需要聲明才能使用的,因此我們通常使用頭文件來保存函數聲明、變量聲明,當然還有一些宏定義的處理。需要注意的是,宏定義等這些預編譯的東西是不能夠在Swift中使用的。在創建了頭文件以後,還需要確認已經把頭文件加入到了工程編譯路徑中(如下圖)

      然後,只要聲明在Bridge頭文件中的(當然也包含Bridge頭文件包含的其他普通頭文件中的聲明),就可以在Swift代碼中使用,同理,如果在Swift代碼中有實現,那麼在Bridge頭文件中加以適當的聲明,那麼,就可以在OC文件中使用(使用時OC文件需要包含Bridge頭文件)。舉一個簡單的例子,在test.m中我們寫了一個函數:

int test1(char a) {
    return a + 5;
}

    我們要想在Swift中使用這個函數,那麼,我們就需要在Bridge頭文件中寫上:

int test1(char a);

    那麼,在main.swift中,可以直接這樣調用:

let a = test1(a: 5)

    並且,執行後a的值是10。

    同理,如果我們在Swift代碼中有這樣一個函數:

func test2(a: Int) -> Double {
    return Double(a) + 0.5
}

    如果想在OC代碼中使用,我們就在Bridge頭文件中這樣聲明:

double test2(int a);

    然後,我們就可以在OC中這樣使用:

double b = test2(5);

    b的值就是5.5,當然,使用之前需要包含頭文件:

#import "test-Bridging-Header.h"

    這個很好理解,但是,比較討厭的就是,多數情況下,我們並不是使用這樣簡單的基本變量類型來傳參,我們使用的是比較複雜的類型,因此,我整理了一個表格,表示Swift類型和OC類型的相互對應:

(1)基本變量類型

名稱 Swift類型 OC類型
字符型 Int8 char
無符號字符型 UInt8 unsigned char
短整型 Int16 short
無符號短整型 UInt16 unsigned short
長整型 Int32 long
無符號長整型 UInt32 unsigned long
超長整型 Int64 long long
無符號超長整型 UInt64 unsigned long long
浮點型 Float float
雙精度浮點型 Double double

    需要注意的是,Swift中的Int,UInt類型以及OC中的int, unsigned類型究竟會轉化爲16位的還是32位的,還是64位的整型,需要根據開發環境的情況,由編譯器決定。因此,如果需要控制數據位數防止溢出的話,那麼並不建議使用這些模棱兩可的數據類型,這得根據實際開發的需求決定。
(2)結構體類型

    OC中的結構體類型會被轉化爲Swift中的結構體類型,例如OC中的:

struct st_test {
    long a;
    char b, c;
};

    對應了Swift中的:

 

struct str_test {
    var a: Int32
    var b: Int8, c: Int8
}

(3)共合體類型

    Swift中不存在共合體類型,因此,OC中的union類型會轉化爲Swift中的struct,但是注意的是,雖然Swift中把他認爲是結構體,但是實際上它還是共合體,因此所有成員是公用內存空間的。Swift中的struct只能在OC中聲明爲struct而不能聲明爲union。

(4)枚舉類型

    注意,Swift的enum類型不能轉化爲OC中的enum,只能將其轉化爲類,才能轉化爲OC的類,而OC中的enum會被轉化爲Swift中的全局變量以及類型重命名,例如OC中的:

enum en_test {
    a, b,
    c = 5, d
};

    會被轉化爲Swift中的:

typealias en_test = Int32
let a: Int32 = 0
let b: Int32 = 1
let c: Int32 = 5
let d: Int32 = 6

    這個是非常不一樣的轉化,一定一定一定要注意!

(5)數組類型

    Swift中的數組(Array)會被轉化爲OC中的NSMutableArray,而OC中的數組會被轉化爲Swift中的元組,例如OC中的:

float arr[5];

    會被轉化爲Swift中的:

var arr: (Float, Float, Float, Float, Float)

    多維數組則會轉化爲嵌套元組,例如OC中的:

long arr_3[2][3][4];

    會被轉化爲Swift中的:

var arr_3: (((Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32)), ((Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32)))

    嵌套順序是OC中越靠後的數字在Swift中越靠內層。

    關於Swift數組的轉換見後面的“類與對象”部分。

(6)非對象指針

        OC中的指針,會被轉化爲Swift中的UnsafePointer類型,並且,會根據關鍵字修飾的不同有不同的轉化,見下表:

名稱 Swift類型 OC類型 示例(Swift) 示例(OC)
普通指針

UnsafeMutablePointer<T>

T *

UnsafeMutablePointer<Int8>

char *
不可變指針 UnsafePointer<T> T *__nonnull  UnsafePointer<Float> float *__nonnull 
(以下可變與不可變都是按照上面的方式,不再單獨列出)
數組指針 UnsafeMutablePointer<((T, T, ...))> T (*)[n]

UnsafeMutablePointer<((Int16, Int16, Int16))>

short (*)[3]
函數指針 (T1, T2, ...) -> T T (*)(T1, T2, ...) (a: Int32, b: Float) -> Double double (*)(long a, float b)

    特別特別要注意的是,雖然數組會被轉化爲元組,但是,數組指針在轉化後會加上一層元組,也就是說n維數組指針在轉化後會被變成UnsafePointer的n+1維的元組的實例。

    還有一點需要關注的是,如果數組在函數參數中,是會把最外層退化爲指針的,那麼轉化成Swift的時候也要按數組指針來對待,例如OC中:

void func1(char a[2][3]);

    會被轉化爲Swift中的:

func func1(a: UnsafeMutablePointer<((Int8, Int8, Int8))>)

    因爲這裏的char a[2][3]其實是被轉化爲了char (*a)[3]。

(7)普通類/對象

    這裏的普通對象指的是非類庫中的對象,以及一部分類庫中的對象(例外將在後面“特殊類/對象”的部分中)。

    普通的類將會被直接轉化,例如OC中的如下代碼:

// 類型示例
@interface Test_Cl : Test_PC {
    long a;
}
- (char)func1;
- (void)func2:(char)arg1 andOA:(unsigned short)arg2;
+ (long)func3;
@end
// 省略實現

// 調用示例
Test_C1 *t1 = [[Test_C1 alloc] init];
char o1 = [t1 func1];
[t1 func2:'a' andOA:5];
[Test_C1 func3];

    會被轉化爲Swift中的:

class Test_C1: Test_PC {
    private var a: Int32

    func func1() -> Int8 {// 省略實現}
    func func2(_ arg1: Int8, andOA arg2: UInt16) {// 省略實現}
    static func func3() {// 省略實現}
}

// 調用示例
var t1 = Test_C1()
var o1: Int8 = t1.fun1
t1.func2(Int8('a'), andOA 5)
Test_C1.func3()

    內容比較多,希望讀者仔細觀察,OC中的方法名中後續部分會被轉化爲Swift方法中的外部變量名,並且,由於OC中的方法名會包含第一個參數的提示,因此轉化爲Swift後,第一個參數一定是無外部參數名的,提示部分會顯示到方法名中(請仔細觀察上面func2的示例)。OC中直接寫在類頭大括號裏的變量全部都會被轉化爲Swift中的private,注意@public、@protected和@private關鍵字會被無視,統一都會轉化爲Swift中的private,而Swift中的private變量會被轉化爲OC中的@private修飾的變量。

    很多關鍵字與類組合時,在轉化時都會有變化,本節只是簡單介紹一下,詳細的情況請看後面部分。

(8)const

    大體來說,OC中沒有用const修飾的,會轉化爲Swift的var修飾,用const修飾的會被轉化爲Swift中的let,但是如果修飾在嵌套類型的非最外層則會被無視,例如:

Swift中 OC中
let a: Int const int a;
var a: UnsafeMutablePointer<Int8> char *a;
var a: UnsafeMutablePointer<Int8> const char *a;
let a: UnsafeMutablePointer<Int8> char *const a;
var a: UnsafeMutablePointer<UnsafeMutablePointer<Int8>> char *const *a;

    但是在搭配很多類庫對象的時候,是會有不同的,詳見“特殊類/對象”部分。

(9)Block與閉包

    OC中的Block和函數指針一樣,都會被轉化爲Swift中的閉包,但是,Swift的閉包只會被轉化爲OC中的Block而不是函數指針,例如OC中的:

int (^const f1)(char, char) = ^(char a, char b) {
    return a + b;
}

    會被轉化爲Swift中的:

let f1: (Int8, Int8) -> Int = {(a, b) in
    return a + b
}

    需要注意的是,OC的Block和函數指針會被轉化爲Swift的非逃逸閉包,相當於用@unscaping修飾。

(10)特殊類/對象

    特殊的主要發生在基本類庫中,這個和Swift版本有關係,第一版的Swift是沒有這些所謂特殊的類的,而後面因爲類庫命名有很大更改,並且,Swift語法也在不斷改變,爲了讓開發者使用方法,所以纔出現了一些特殊的轉換(注:寫本文針對於Swift 4版本,不一定適用於其他版本,因此請讀者關注自己使用的Swift版本,如果你在閱讀本文時已經有了更新的版本,那麼可能會有一些差別,但是這並不影響你閱讀本節,因爲這會給你一個思路,而具體細節可以查閱蘋果的官方文檔,有關於每個版本更新的語法改變說明)。

    這部分比較複雜,所以,這裏還是用表格的方式呈現:

說明 Swift OC 示例(Swift) 示例(OC)
同一類型元組 (T, T, T...) T[] let a = (1, 2, 3, 4) const int a[4] = {1, 2, 3, 4};
數組 [T] NSArray let a = [1, 2, 3] NSArray *a = @[1, 2, 3];
可變數組 [T] NSMutableArray var a [1, 2, 3] NSMutableArray *a = @[1, 2, 3];
字符串 String NSString let a = "123" NSString *a = @"123";
可變字符串 String NSMutableString var a = "123" NSMutableString *a = @"123";
字典 [K, V] NSDictionary let a = [1: "one", 2: "" two] NSDictionary *s = [NSDictionary dictionaryWithObjectsAndKeys: @1, @"one", @2, @"two"];
可變字典 [K, V] NSMutableDictionary
任意 Any void * let a: Any void *const a;
任意對象 AnyObject void *__nonnull  var a: AnyObject void *__nonnull a;

    特別需要注意的是,C語言字符串(char數組)會被轉化爲UnsafePointer<Int8>,而不是String。

    另外,一些NS開頭的類在Swift中也是可用的,他們會被直接轉換,例如Swift中的NSString仍然會被轉化爲OC中的NSString,但是,OC中這些NS開頭的則會轉化爲Swift中的基本類型,例如NSMutableString就會被轉化爲Swift中以var修飾的String。

    對於嵌套類型,則全部會轉化爲對象,例如Swift的[Int:[Int]]類型就會被轉化爲NSArray類型,而其中的成員都已經被轉化爲了NSObject或其子類,Int會被轉化爲NSNumber,而[Int]則會被轉化爲NSArray,操作NSArray時利用多態性,其成員都是NSObject *類型。

(11) 指針與inout參數

    這個說起來應該不算混編內容,應當是純Swift內容,但是由於單純使用Swift時真是太少使用指針了,反而是在混編時經常會遇到,因此在這裏講解。

    在Swift中,如果函數參數是UnsafePointer類型(或其他的指針類型)時,可以直接傳進inout參數,例如:

// 函數定義
func func1(a: UnsafeMutablePointer<Int>) {// 省略實現}

// 函數調用
var a = 0
func1(&a) // 合法

    也就是說,參數是指針類型時,可以把它與用inout修飾的函數參數等同對待。因此,在混編時,如果一個OC函數參數需要傳指針的話,在Swift裏調用時可以直接傳&變量,而不用去用複雜的語句包裝成指針類型再傳參。

(12)可變類型的函數參數

    Swift函數的形參是不能夠改變值的,曾經的Swift可以在參數前用var修飾使其可變,但是在Swift3中把這個語法取消了,因此,Swift函數轉化成OC時,所有的參數都會默認帶上const。例如Swift中:

func f1(a: Int, b: Int) {// 省略實現}

    會被轉化爲OC中的:

void f1(const int a, const int b) {// 省略實現}

    而當OC轉Swift的時候,這裏的const是會被丟棄的(因爲在Swift中本來就不可變)。

(13)可選類型

    通常情況下,OC轉Swift的時候不會轉爲Optional類型,但是,在之前提到的那些特殊類的轉化中,是存在可選類型的,因爲這部分的let和var其實對應了不同的類,所以用於處理__nonnull關鍵字的,就需要可選類型來擔當了,例如:

Swift OC 示例(Swift) 示例(OC)
String? NSString * let a: String? = "123" NSString *const a = @"123";
String NSString *__nonnull  let a = "123" NSString *__nonnull a = @"123"
String NSMutableString *__nonnull var a = "123" NSMutableString *__nonnull a = @"123"

    其他的那些特殊類都是一樣的道理,let和var控制的是是否有Mutable,而是否可選控制的是有無__nonnull。

(14)成員變量

    Swift中沒有被private修飾的成員變量,會轉化爲OC中的@property,反之亦然。例如Swift中的:

class Test_Cl {
    var a: Int
}

    被轉化爲OC中的:

@interface Test_Cl
@property int a;
@end 

    也就是說,在OC中是會自動生成set和get方法的,但是如果手動寫OC的get和set,是不會轉化爲Swift的成員的,仍然會轉化爲一個private成員加兩個普通的方法。

(15)構造函數

    類庫中大部分類都對Swift重構了,所以構造函數這裏可以放心調用,但是,如果沒有進行重構的話,只有標準OC構造函數和Swift無參構造函數可以互相轉化,而OC的非標準構造函數將會轉化爲Swift的類函數,Swift的有參構造函數則無法被轉化。OC中的:

@interface Test_Cl : Test_PC
- (instancetype)init; // 只有這個函數頭是標準的構造函數
- (instancetype)initWithName:(NSString *)name; // 這個就是普通函數了
@end

    會被轉化爲Swift中的:

class Test_Cl: Test_PC {
    init() {// 省略實現}
    static func initWithName(_ name: String) {//這就是個普通函數了,省略實現}
}

(15)不能進行轉化的

    除了上文提到的一些會丟棄的部分,還有一些語法由於沒有提供語法接口,是不能夠跨語言調用的,例如:運算符重載,計算方法,逃逸閉包,含有外部引用的閉包,重載函數, 有參構造函數等等。所以在混編時要注意避免這些問題,否則這部分代碼將不能跨語言調用。

    讀者應該能夠發現,大多數場景下,都是在Swift工程中調用OC的接口,很少會有反過來的,基本上OC的都系在Swift中都能夠得到比較好的兼容,而反過來的話則會有很多不能夠調用的情況。

 

    以上就是本人對Swift於OC混編的總結,希望能夠幫到在此糾結的朋友,幫大家度過轉化期。但是對於需要繼續長久使用的OC工程或是第三方庫,還是應當儘早完成對Swift語言的重構,這樣就可以避免在混編過程中出現的問題,同時還可以充分利用Swift語言的特性優勢,編寫出更優秀的代碼。

    如果讀者還有什麼問題,歡迎留言討論,本文的一切權利歸本人所有,如果讀者希望轉發,請在開頭標註出轉發字樣以及出處,多謝配合!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章