Swift和Objective-C混編

2021-01-14 Cocoa開發者社區

翻譯自蘋果官方文檔


和Objective-C交互


互用性是指,在Swift和Objective-C之間可以建立一個互通接口,不管是Swift生成接口給Objective-C對接,還是Objective-C生成接口給Swift對接。既然你決定開始用Swift來開發,那麼有必要理解一下怎麼運用互用性來重定義、提高、改進你寫Cocoa app的方式。


互用性重要性之一是,在Swift中調用Objective-C的API。在你import一個Objective-C框架之後,你就可以用Swift的語法來實例化裡面的類,繼而使用它們。


初始化


要在Swift裡初始化一個Objective-C類,需要用Swift的初始化語法來調Objective-C的初始化方法。


Objective-C初始化方法都以init開頭,或者,如果有一個或多個參數,會以initWith:開頭。在Swift文件裡如果要調用Objective-C初始化方法,那麼init前綴會變成Swift初始化方法。如果此時初始化方法帶有參數,會去掉with,而其他參數會根據情況劃分到各個參數中。


Objective-C初始化方法的聲明:

- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;


轉為Swift的初始化聲明:

init() { }
init(frame: CGRect, style: UITableViewStyle) { }


實例化對象的過程,更能看出Objective-C和Swift語法的不同:


Objective-C:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];


Swift:

let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)


不用調用alloc,Swift替你處理了。還有,調用Swift風格的初始化函數,不會到處出現init。


當給變量或者常量賦值的時候,你可以指明一個類型,或者可以不指明這個類型,讓Swift根據初始化方法自動推導出類型。

let myTextField = UITextField(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 40.0))


這裡的UITableView和UITextField和你在Objective-C裡實例化出來的對象是一樣的,Objective-C裡怎麼用,這裡就怎麼用,根據各自的類型,獲取屬性、調用方法都一樣。


類工廠方法和方便初始化方法


為了保持一致性和簡單,Objective-C的類工廠方法引入Swift後,會改為方便初始化方法。這樣,使用這些方法就像使用初始化方法一樣。


例如,下面這個就是Objective-C中的一個工廠方法:

UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];


在Swift中,要這樣調用:

let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)


可失敗的初始化


Objective-C中初始化函數直接返回初始化的結果,如果初始化失敗,就會直接返回nil。在Swift中,這個模式由語言的一個特性來實現,叫可失敗的初始化(原文:failable initialization)。


很多系統框架裡的初始化方法都有標識初始化能不能失敗。在你自己的Objective-C類裡可以使用可空性(原文:nullability)來標識是不是能失敗,Nullability and Optionals裡有描述。在Objective-C裡不可失敗的話,在Swift裡是init(...),可失敗的話,用init?(...)。否則,都用init!(...)。


例如,UIImage(contentsOfFile:)初始化方法,如果提供的路徑下面沒有圖片文件,就會初始化失敗。如果可失敗初始化函數初始化成功了,你可以用可選綁定去解包返回的結果。(譯者註:public init?(contentsOfFile path: String))

if let image = UIImage(contentsOfFile: "MyImage.png") {
   
} else {
   
}


獲取屬性


在Objective-C中用@property聲明的屬性,Swift會做下面的處理:


已經用nonnull、nullable和 null_resettable修飾的屬性,按照Nullability and Optionals說的,轉成可選和非可選屬性。


readonly屬性會轉化為Swift中的計算型屬性,也即只有一個getter方法({ get })。


weak屬性轉化為Swift中的weak屬性(weak var)。


對於不是weak的所有者屬性(原文:ownership property)(即assign、 copy、strong、 unsafe_unretained)都會轉成相對應的存儲屬性。


class修飾的屬性(譯者註:Objective-C在Xcode9引入了一個新的屬性class,應該就是為了對接Swift吧),轉為在Swift中類型屬性。


Objective-C中的原子性修飾符(atomic和nonatomic)在Swift中沒有對應的修飾符,但是原子性所提供的功能在Swift裡是依然存在的。(譯者註:Atomicity property attributes (atomic and nonatomic) are not reflected in the corresponding Swift property declaration, but the atomicity guarantees of the Objective-C implementation still hold when the imported property is accessed from Swift.這句翻譯的不是太好,請指教!)


存取屬性(getter=和setter=)在Swift被忽略掉。(譯者註:今天才知道getter=和setter=也是屬性)


在Swift中,獲取Objective-C 對象的屬性值,用點語法直接帶上屬性名字,不需要括號。


例如,設置UITextField的textColor和text屬性:

myTextField.textColor = .darkGray
myTextField.text = "Hello world"


Objective-C中用點語法調用無參數有返回值的方法,形式和獲取屬性很像(譯者註:方法除了點,還有括號,其實並不一樣)。雖然調用形式一樣,方法在Swift中還是會轉換為方法,只有Objective-C中用@property聲明的變量才能轉為Swift中的屬性。關於方法的引入和調用可以參考方法的使用。


方法的使用


在Swift中,用點語法來調用Objective-C方法。


Objective-C方法引入到Swift後,方法的第一部分,變成方法名,在括號前面。第一個參數在括號裡,沒有參數名。剩下的參數對應各自的參數名排在括號裡。方法的所有組成部分都是需要的,少了任何部分,調用地址就是不對的。


例如Objective-C中:

[myTableView insertSubview:mySubview atIndex:2]


在Swift中,會是這樣:

myTableView.insertSubview(mySubview, at: 2)


調用一個沒有參數的方法,依然要帶上括號。

myTableView.layoutIfNeeded()


id兼容


Objective-C中的id類型會轉為Swift中的Any類型。在編譯時和運行時,將Swift的值或者對象(譯者註:Swift裡引入了結構體,所以不光是類引用類型,也包括結構體值類型,所以,總是出現「值或者對象」)傳給Objective-C的類型為id參數時,編譯器會去執行一個全局橋接轉換操作(原文:universal bridging conversion operation)。當將一個Objective-C中的id傳給Swift的Any參數時,運行時會自動橋接回Swift的類引用或者值類型。(譯者註:換句話說就是,id和Any可以轉換,並且轉換由系統完成)

var x: Any = "hello" as String
x as? String  
x as? NSString

x = "goodbye" as NSString
x as? String  
x as? NSString


(譯者註:這個例子代碼說明啥?不是特別清楚,求指教!)


向下轉換Any(譯者註:從一個比較寬的類型,轉化為一個比較具體的類型)


如果知道Any裡實際是什麼類型,那麼將其向下轉化到一個更具體的類型比較有用。由於Any類型可以包含任意類型,所以,向下轉型到一個更具體的類型不一定都能成功。


可以試試可選類型轉化操作符(as?),返回一個包裹著轉化結果的可選值:

let userDefaults = UserDefaults.standard
let lastRefreshDate = userDefaults.object(forKey: "LastRefreshDate")
if let date = lastRefreshDate as? Date {
   print("\(date.timeIntervalSinceReferenceDate)")
}


如果你能確定對象的類型,那麼可以用強制向下類型轉換(as!)。

let myDate = lastRefreshDate as! Date
let timeInterval = myDate.timeIntervalSinceReferenceDate


如果強制向下轉換失敗,會觸發一個運行時錯誤:

let myDate = lastRefreshDate as! String


動態方法查找


Swift還有一個AnyObject類型,用來表示某些對象類型(譯者註:和Any不同的是,Any還可以存儲值類型),並且這個類型還有特別的能力,可以將@objc修飾的方法轉為動態查找。通過這個特性,對Objective-C返回的id類型的值可以更好的操作與維護。


(譯者註:AnyObject能力比較強大,因為Swift是強類型,不指定具體類型是不能調用方法的,但是AnyObject不一樣,可以調用任意方法。如果一個對象的方法或者屬性標記為@objc,那麼這些方法就變成可以動態查找的方法。因為Objective-C的方法是採用動態查找實現的,所以,只有這樣,這些方法才能提供給Objective-C使用。@objc後面有詳細敘述)


例如,可以給AnyObject類型的常量或者變量賦值任意的對象,這個變量又可以再賦值一個不同類型的對象。你不需要轉換為具體的類型,直接通過AnyObject類型就可以調用Objective-C具體類的方法和屬性。

var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow


不能識別的方法和可選鏈


因為直到運行時才能知道AnyObject的值到底是什麼類型,所以,就有可能寫出不安全的代碼。無論是Swift還是Objective-C,試圖去調用一個不存在的方法都會觸發一個找不到方法(譯者註:unrecognized selector)的錯誤。


例如,下面的代碼不會有編譯時的警告,但是在運行時會報錯:

myObject.character(at: 5)


Swift可以用可選的方式來避免不安全的行為。調用AnyObject類型的方法時,實際進行了一個隱式解包。可以用可選鏈語法來調用AnyObject類型的方法。


例如下面的代碼,第一行和第二行不會執行,因為NSDate對象沒有count屬性和character(at:)方法。常量myCount會被推導為可選Int類型,值為nil。可以用if let語句來解包方法返回的結果,如第三行所示。

// myObject has AnyObject type and NSDate value
let myCount = myObject.count
// myCount has Int? type and nil value
let myChar = myObject.character?(at: 5)
// myChar has unichar? type and nil value
if let fifthCharacter = myObject.character?(at: 5) {
   print("Found \(fifthCharacter) at index 5")
}
// conditional branch not executed


注意


雖然Swift沒有強制要求,AnyObject類型的值調用方法時,一定要解包,但還是推薦解包,以此來避免不可預知的行為。(譯者註:Although Swift does not require forced unwrapping when calling methods on values of type AnyObject, it is recommended as a way to safeguard against unexpected behavior.不是太理解,求指教!)


可空和可選


Objective-C中,通過原始指針(原文:raw pointer)來引用對象,指針可能是NULL(在Objective-C中是nil)。在Swift中,所有的結構體類型,或者對象類型都不會為空(譯者註:不會直接是nil,而是一個可選值)。如果要表示這個值可以為空,那麼要將這個值包裝為可選類型。關於可選值的更多信息,請看Swift程式語言(Swift 4.0.3)中可選值。


Objective-C中可以用可空性標識來表示參數、屬性、返回值是不是可以為NULL或者nil。單個的類型聲明,我們可以用_Nullable和_Nonnull標識,單個的屬性聲明可以用nullable, nonnull和null_resettable標識,或者用宏NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END來標識整個區域的可空性。如果沒有給一個類型設置可空性修飾,Swift無法區分是可選還是非可選,那麼統一設置為隱式解包可選類型。


(譯者註:對於隱式解包可選類型加個說明,隱式解包的可選類型主要用在一個變量/常量在定義瞬間完成之後值一定會存在的情況,在使用時,不需要解包,直接使用,系統自動解包)


用_Nonnull標識,或者在整個非空宏區域裡的類型,聲明為不可空,在Swift裡都轉為不可選。

聲明為_Nullable的可空類型,在Swift裡轉為可選類型。

沒有任何可空性標識的類型,在Swift裡都轉為隱式解包可選類型。


例如,下面的Objective-C聲明:

@property (nullable) id nullableProperty;
@property (nonnull) id nonNullProperty;
@property id unannotatedProperty;

NS_ASSUME_NONNULL_BEGIN
- (id)returnsNonNullValue;
- (void)takesNonNullParameter:(id)value;
NS_ASSUME_NONNULL_END

- (nullable id)returnsNullableValue;
- (void)takesNullableParameter:(nullable id)value;

- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;


下面是在Swift中的樣子:

var nullableProperty: Any?
var nonNullProperty: Any
var unannotatedProperty: Any!

func returnsNonNullValue() -> Any
func takesNonNullParameter(value: Any)

func returnsNullableValue() -> Any?
func takesNullableParameter(value: Any?)

func returnsUnannotatedValue() -> Any!
func takesUnannotatedParameter(value: Any!)


大部分的系統框架,包括Foundation,都有可空性標識,所以在和這些值打交道時,可以保持類型安全和語言的習慣


橋接可選類型到非空對象


Swift將可選值橋接為Objective-C的非空對象,如果可選值裡有值,那麼把這個值傳遞給Objective-C對象,如果可選值是nil,那麼會傳遞一個NSNull實例給Objective-C對象。例如,可以將一個Swift可選類型直接傳入Objective-C接受非空id參數的API,也可以將包含可選項的數組([T?])橋接為NSArray。


下面的代碼可以看出,怎麼根據實際所包含的值來橋接String?實例到Objective-C中。

@implementation OptionalBridging
+ (void)logSomeValue:(nonnull id)valueFromSwift {
   if ([valueFromSwift isKindOfClass: [NSNull class]]) {
       os_log(OS_LOG_DEFAULT, "Received an NSNull value.");
   } else {
       os_log(OS_LOG_DEFAULT, "%s", [valueFromSwift UTF8String]);
   }
}
@end


valueFromSwift參數類型是id,所以在Swift中要傳入Any類型,如下所示。一般情況下,給參數為Any函數傳入一個可選類型並不普遍,所以,需要將傳入logSomeValue(_:)類方法的可選值進行顯式的轉換為Any類型,避免編譯器警告。

let someValue: String? = "Bridge me, please."
let nilValue: String? = nil

OptionalBridging.logSomeValue(someValue as Any)  
OptionalBridging.logSomeValue(nilValue as Any)  


協議限定類(Protocol-Qualified Classes)


實現了一個或者多個協議的Objective-C類,在Swift中會轉換成協議組合類型。例如以下的Objective-C中定義的view controller屬性

@property UIViewController<UITableViewDataSource, UITableViewDelegate> * myController;


Swift中會是這樣:

var myController: UIViewController & UITableViewDataSource & UITableViewDelegate


Objective-C中協議限定元類(protocol-qualified metaclasses),在Swift中轉換為協議元類型(protocol metatypes)。例如以下Objective-C方法對特定的類執行操作(原文:Objective-C protocol-qualified metaclasses are imported by Swift as protocol metatypes. For example, given the following Objective-C method that performs an operation on the specified class):

- (void)doSomethingForClass:(Class<NSCoding>)codingClass;


Swift中的樣子:

func doSomething(for codingClass: NSCoding.Type)


輕量級泛型


用輕量級泛型參數化(譯者註:generic parameterization泛型參數化,意思是,給泛型指定了類型,也即尖括號裡有具體的類型。所以後面說到的參數化,都是指已經給泛型指定了具體的類型)。聲明的Objective-C類型,內容的類型信息在Swift會保留下來。例如下面的Objective-C屬性聲明:(譯者註:Objective-C裡面的泛型實際是Xcode7引入的一個語法糖,稱為輕量級泛型。Stack Overflow上有這個討論)

@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;


Swift中會是這樣:

var dates: [Date]
var cachedData: NSCache<NSObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]


一個Objective-C中參數化的類,引入Swift後,轉換為泛型類,有同樣數量的類型參數(譯者註:type parameters類型參數,這裡的意思是,泛型尖括號裡的類型)。所有Objective-C中的泛型類型參數,在Swift中,會將類型參數轉換為像(T: Any)類似的類的形式。如果Objective-C裡泛型參數化指定了一個限制類(譯者註:class qualification限制類,意思是,這個類起到了一個限制的作用,下面說道的protocol qualification協議限制,也是一樣的理解,表示,這個協議對這個泛型起到一個限制作用),在Swift中也會轉換這個限制:這個類必須是這個限制類的子類。如果Objective-C離泛型參數化指定了一個限制協議,在Swift中也會轉換這個限制:這個類也必須遵守指定的協議。對於沒有任何限制的Objective-C類型,Swift會推導給出限制,如下,Objective-C類和類別的聲明:

@interface List<T: id<NSCopying>> : NSObject
- (List<T> *)listByAppendingItemsInList:(List<T> *)otherList;
@end

@interface ListContainer : NSObject
- (List<NSValue *> *)listOfValues;
@end

@interface ListContainer (ObjectList)
- (List *)listOfObjects;
@end


Swift中是這個樣子的:

class List<T: NSCopying> : NSObject {
   func listByAppendingItemsInList(otherList: List<T>) -> List<T>
}

class ListContainer : NSObject {
   func listOfValues() -> List<NSValue>
}

extension ListContainer {
   func listOfObjects() -> List<NSCopying>
}


擴展


Swift擴展和Objective-C中的類別相似。擴展增加了已有類,結構體,枚舉的功能,也可以擴展在Objective-C中定義的這些類型(譯者註:應該是系統將Objective-C中的結構體和枚舉類型轉成了Swift中的結構體和枚舉類型,這樣才具備擴展功能)。可以給系統類型添加擴展,也可以給自定義的類添加擴展。Swift裡引入Objective-C的模塊,用Objective-C中的名字 ,來引用這些類,結構體,枚舉類型。


例如,給UIBezierPath類擴展功能,根據邊長和起點生成一個等邊三角形,再由這個等邊三角形創建一個貝塞爾路徑。

extension UIBezierPath {
   convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
       self.init()
       let squareRoot = CGFloat(sqrt(3.0))
       let altitude = (squareRoot * triangleSideLength) / 2
       move(to: origin)
       addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y))
       addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
       close()
   }
}


可以用擴展來添加屬性(包括類屬性和靜態屬性)。但是,只能是計算屬性;擴展不能給類,結構體,枚舉類型添加存儲屬性。


下面的例子是給CGRect結構體擴展一個計算屬性area:

extension CGRect {
   var area: CGFloat {
       return width * height
   }
}
let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
let area = rect.area


可以不通過繼承,通過擴展來讓一個類來實現協議(譯者註:Objective-C中分類也是可以實現這個功能)。如果協議是在Swift中定義的,那麼也可以給Objective-C和Swift中的結構體,枚舉類型添加實現協議的擴展。


擴展不能覆蓋Objective-C中已經存在的方法和屬性。


閉包


Objective-C中的block使用@convention(block)自動轉換為Swift中的閉包。下面是Objective-C block變量:

void (^completionBlock)(NSData *) = ^(NSData *data) {
 
}


Swift中:

let completionBlock: (Data) -> Void = { data in
   // ...
}


Swift閉包和Objective-C block是兼容的,可以將Swift閉包傳遞給接受block的Objective-C方法。Swift閉包和函數是相同的類型,所以也可以給Objective-C的block傳遞一個Swift的函數名字。


閉包和block有相似的值捕獲功能,但是閉包有一點不一樣:變量默認是可以修改的。換句話說,Objective-C中給變量添加__block修飾,在Swift是默認行為。


避免捕獲self而造成循環引用


Objective-C中,在block中捕獲self,需要考慮內存管理。


block會對任何捕獲的對象保持一個強引用,包括self。如果self又對block保持一個強引用,例如block是self的一個copy屬性,那麼會造成一個循環引用。為了避免循環引用,我們讓block捕獲一個弱引用的self。

__weak typeof(self) weakSelf = self;
self.block = ^{
  __strong typeof(self) strongSelf = weakSelf;
  [strongSelf doSomething];
};


和Objective-C裡的block一樣,Swift中的閉包也會對捕獲的對象保持一個強引用,當然也包括self。為了防止造成循環引用,可以在閉包的捕獲列表裡,將self設置為unowned

self.closure = { [unowned self] in
   self.doSomething()
}


更多信息參照Swift程式語言(Swift 4.0.3)中的解決閉包循環引用


對象比較


在Swift中有兩個不同的比較類型來比較兩個對象。一個是等號(==),來比較對象的內容。還有一個是恆等(===),來判斷比較的變量或常量是不是指向同一個對象。


Swift對==和===操作符提供了默認的實現,對於繼承自NSObject的對象,遵守Equatable協議。==的默認實現是調用isEqual:方法,===的默認實現是檢查指針是否相等。從Objective-C引入的類型,不能重寫等號和恆等操作符。


NSObject類提供的isEqual:實現和恆等是一樣的,也是比較指針是否相等。在Swift和Objective-C中,可以通過重寫isEqual:,來比較對象的內容是不是相等,而不是比較指針。關於比較邏輯實現的更多信息,請看Cocoa Core Competencies裡的對象比較。


注意

Swift也提供了等和恆等對應的相反的實現(!=和!==)。這兩個操作符不能重寫。


哈希


Objective-C中NSDictionary沒有對key指定類型限制,Swift會轉化為Dictionary,key的類型是AnyHashable。NSSet也一樣,如果在Objective-C中沒有對其中的元素指定類型限制,Swift會將Set中的元素類型設置為AnyHashable。如果NSDictionary和NSSet已經參數化key或者元素的,那麼Swift裡就會用相應的類型。如下Objective-C的聲明:

@property NSDictionary *unqualifiedDictionary;
@property NSDictionary<NSString *, NSDate *> *qualifiedDictionary;

@property NSSet *unqualifiedSet;
@property NSSet<NSString *> *qualifiedSet;


轉為Swift:

var unqualifiedDictionary: [AnyHashable: Any]
var qualifiedDictionary: [String: Date]

var unqualifiedSet: Set<AnyHashable>
var qualifiedSet: Set<String>


Objective-C中未指明類型或者類型為id的類型,轉到Swift中都是AnyHashable,為什麼不是Any類型,因為在這裡,這些類型都需要遵守Hashable協議。AnyHashable類型表示任何可哈希的類型,可以用as?和as!操作符轉化為更具體的類型。


更多信息,見AnyHashable。


Swift類型兼容


Swift可以繼承Objective-C類,生成一個Swift類,其中成員屬性、方法、索引(譯者註:subscript索引是Swift裡的新功能)、初始化方法,這些能在Objective-C中找到對應的轉化,都會自動轉化為Objective-C相應的功能。但是有些特性是Swift特有的,無法轉到Objective-C,如下所示:


泛型(譯者註:Objective-C沒有泛型,後來引入的稱作輕量級泛型,只是一個編譯器的語法糖,和Swift中真正的泛型不是一回事)


元祖


在Swift中定義的枚舉類型,raw值不為Int類型(譯者註:Objective-C中也有枚舉類型,但是,原始值一定是int類型。在Swift中,枚舉類型可以是其他類型,不光是int類型,所以要和Objective-C兼容的話,必須限定為int類型)。


Swift中定義的結構體


Swift中定義的頂級函數(原文:Top-level functions意思是,不屬於某個類,某個結構體,枚舉等的函數,直接寫在文件裡的函數)


Swift中定義的全局變量


Swift中的Typealiases關鍵字


Swift風格的可變參數(譯者註:Objective-C中也有可變參數,但是和Swift相比,功能弱很多,所以,Swift特有的功能轉不過去)


嵌套類型(譯者註:Swift裡類型可以嵌套,例如類裡面還可以定義類)


柯裡化函數(譯者註:王巍的Swift 100 tips第一節就是介紹柯裡化)


Swift API轉為Objective-C API,和上面的Objective-C API轉為Swift API類似,下面是Swift轉Objective-C:


Swift可選類型轉為__nullable


Swift非可選類型轉為__nonnull


Swift常量的存儲屬性和計算屬性都轉為Objective-C的read-only。


Swift變量存儲屬性轉為Objective-C的read-write。


Swift的類型屬性(type properties)轉為class屬性(譯者註:參考Xcode8添加一個屬性class)。


Swif類型方法轉為Objective-C的類方法(靜態方法)。


Swift初始化方法(譯者註:Swift的初始化函數,在Swift裡是按特殊函數對待,但是Objective-C初始化函數也是實例函數)和實例方法轉為Objective-C的實例方法。


Swift中(throw)拋出錯誤的方法,會轉化為Objective-C中的帶NSError **參數的方法。如果這個Swift方法沒有參數,會追加AndReturnError: 到Objective-C方法名的後面,否則就追加error:參數。如果Swift方法沒有指定返回類型,相應的Objective-C方法會返回一個BOOL類型。如果Swift方法返回一個非可選類型,相應的Objective-C方法會返回一個可選類型(譯者註:原文是「 If the Swift method returns a nonoptional type, the corresponding Objective-C method has an optional return type. 」,Objective-C中沒有所謂的可選類型,這句不甚了解,求指導!)。


以下Swift聲明:

class Jukebox: NSObject {
   var library: Set<String>
 
   var nowPlaying: String?
 
   var isCurrentlyPlaying: Bool {
       return nowPlaying != nil
   }
 
   class var favoritesPlaylist: [String] {
       
   }
 
   init(songs: String...) {
       self.library = Set<String>(songs)
   }
 
   func playSong(named name: String) throws {
       
   }
}


轉為Objective-C:

@interface Jukebox : NSObject
@property (nonatomic, strong, nonnull) NSSet<NSString *> *library;
@property (nonatomic, copy, nullable) NSString *nowPlaying;
@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;
@property (nonatomic, class, readonly, nonnull) NSArray<NSString *> *favoritesPlaylist;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error;
@end


注意

Objective-C不能繼承Swift類。


調整Swift,適配Objective-C


某些情況下,對於暴露給Objective-C的Swift API需要有一個更細粒度的控制(譯者註:原文finer grained control)。用@objc(name)可以給類,屬性,方法,枚舉,及枚舉裡的case重新改一個名字,暴露給Objective-C。


例如,Swift類包含的字符是Objective-C不支持的,那麼我們可以改個名字暴露給Objective-C。如果要給Swift函數改名,需要用Objective-C的selector語法。如果有參數,不要漏了冒號(:)。

@objc(Color)
enum Цвет: Int {
   @objc(Red)
   case Красный
 
   @objc(Black)
   case Черный
}

@objc(Squirrel)
class Белка: NSObject {
   @objc(color)
   var цвет: Цвет = .Красный
   
   @objc(initWithName:)
   init (имя: String) {
       
   }
   @objc(hideNuts:inTree:)
   func прячьОрехи(количество: Int, вДереве дерево: Дерево) {
       
   }
}


當給一個Swift類使用@objc(name)屬性的時候,Objective-C就可以訪問這個類,並且不會帶有任何命名空間信息(譯者註:因為Swift是有命名空間的,如果不去掉命名空間,直接搬到Objective-C中使用,類名就會是xxx.類名的形式,這個在Objective-C裡當然不能使用,所以這裡的意思是,轉到Objective-C,會去掉命名空間,也就是去掉類名前的xxx)。


在將可歸檔的Objective-C類遷移到Swift中時(譯者註:意思是將一個Objective-C類改寫為Swift類),這個屬性也非常有用。因為歸檔的對象會儲存類名,用@objc(name)指定一個和Objective-C中一樣的名字,這樣才能用新的Swift類來解檔之前Objective-C歸檔的類。


(譯者註:用我的話說就是,Objective-C歸檔的時候,沒有所謂的命名空間,直接按照類名來歸檔,但是改成Swift之後,想把之前用Objective-C歸檔的東西解檔,就必須去掉命名空間,去掉命名空間的做法,就是用@objc(name)來指定類名)


注意


相反,Swift也有@nonobjc屬性,這個屬性表示Objective-C中不能訪問。You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C.(譯者註:譯者對於混編經驗不足,這句意思不甚了解,求指教!原文在上面)有些功能,例如可變參數,在Objective-C中不能表示,所以,對於這類方法,需要標記為@nonobjc。


要求動態派發(譯者註:Objective-C裡面的消息調用機制稱作動態派發)


暴露給Objective-C調用的Swift API,可必須是通過動態派發的方式調用。但是,這些通過動態派發的Swift API,當Swift調用這些API的時候,Swift編譯器會選擇一個更加高效的方法來調用,而不會直接用動態派發的方式調用(譯者註:其實Objective-C中的動態派發是一種低效的機制,Swift已經摒棄)。


在@objc後面加上dynamic,表示通過Objective-C運行時動態派發來訪問成員。很少情況下需要這種動態派發。但是在使用KVO和 method_exchangeImplementations等那些需要在運行時動態替換方法的時候,就需要指明動態派發。


添加dynamic的聲明也必須添加@objc,除非@objc被系統隱式添加了。@objc隱式添加的相關信息請看Swift程式語言中屬性聲明


選擇子(譯者註:原文selector)


Objective-C中,selector表示一個方法的類型。Swift中,用Selector結構體表示Objective-C的selector類型,可以用#selector表達式來創建這個結構體。要給一個方法創建一個可供Objective-C調用的selector,需要傳入方法名,例如#selector(MyViewController.tappedButton(_:))。如果要給一個屬性的getter或者setter方法創建一個selector,需要傳遞一個以getter或者setter標籤為前綴的屬性名,例如#selector(getter: MyViewController.myButton)。

import UIKit
class MyViewController: UIViewController {
   let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
 
   override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
       super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
       let action = #selector(MyViewController.tappedButton)
       myButton.addTarget(self, action: action, forControlEvents: .touchUpInside)
   }
 
   @objc func tappedButton(_ sender: UIButton?) {
       print("tapped button")
   }
 
   required init?(coder: NSCoder) {
       super.init(coder: coder)
   }
}


注意


Objective-C方法指針可以加括號,可以使用as操作符來區別不同的重載函數,例如#selector(((UIView.insert(subview:at:)) as (UIView) -> (UIView, Int) -> Void))(譯者註:可以參考Stack Overflow)


Objective-C方法的不安全調用


可以使用perform(_:)或者它的變體去調用一個Objective-C方法。用selector調用方法是不安全的,因為編譯器對結果不能做任何保證,甚至不能保證對象是否能響應這個selector。除非非要依賴Objective-C運行時的動態解決方案,否則我們不建議使用這些API。例如,你需要實現一個類,使用target-action設計模式,像NSResponder那樣,這種情況使用這些API是合適的。大部分情況下,將對象轉為AnyObject,再使用可選鏈來調用,會更安全,更方便。這個在id兼容性裡有說到(譯者註:這裡蘋果推薦先將對象轉為AnyObject,再直接來調用方法,而不是使用selector,出於安全考慮)。


通過selector同步執行方法,例如perform(_:),返回一個隱式解包的Unmanaged指針,指向AnyObject實例(Unmanaged<AnyObject>!),因為返回值的類型和所有者在編譯期不能確定。按照約定,在一個指定的線程,或者做一個延時再執行一個selector,例如perform(_:on:with:waitUntilDone:modes:)和perform(_:with:afterDelay:)不會返回一個值。更多信息見非託管對象。

let string: NSString = "Hello, Cocoa!"
let selector = #selector(NSString.lowercased(with:))
let locale = Locale.current
if let result = string.perform(selector, with: locale) {
   print(result.takeUnretainedValue())
}


對象調用不能識別的selector,會觸發doesNotRecognizeSelector(_:),拋出NSInvalidArgumentException異常。

let array: NSArray = ["delta", "alpha", "zulu"]


let selector = #selector(NSDictionary.allKeysForObject)


array.perform(selector)


Key和Key Path


Objective-C中,key是一個字符串,表示一個對象的屬性。key path是一個用點分割的字符串,表示一個對象屬性的屬性。key和key path經常用於鍵值編碼(KVC),一種通過字符串間接獲取對象屬性的機制。key和key path也用於健值觀察(KVO),一個對象的屬性改變,會通知另外一個對象的機制。


Swift中,可以用key-path表達式創建一個key path來獲取屬性,例如用\Animal.name來獲取Animal的name屬性,例如下面代碼。用key-path表達式創建的key path指向的屬性已經包含了類型信息。對一個實例用key path取得的屬性值,和直接用屬性取得的值一樣。key-path表達式可以是一個屬性,也可以是鏈式的屬性,如\Animal.name.count。


class Animal: NSObject {
   @objc var name: String
 
   init(name: String) {
       self.name = name
   }
}

let llama = Animal(name: "Llama")
let nameAccessor = \Animal.name
let nameCountAccessor = \Animal.name.count

llama[keyPath: nameAccessor]

llama[keyPath: nameCountAccessor]


Swift中,還可以用#keyPath字符串表達式創建一個可以在編譯期檢查的key和key path,用於KVC方法,如value(forKey:)和value(forKeyPath:),用於KVO方法,如addObserver(_:forKeyPath:options:context:)。#keyPath也支持鏈式的方法或屬性。鏈中可以有可選值,如#keyPath(Person.bestFriend.name)(譯者註:bestFriend就是一個可選值)。和用key-path表達式創建的key path不同,用#keyPath字符串創建的key path所指定的屬性或者方法沒有類型信息。


注意

#keyPath字符串語法和#selector表達式類似,見Selectors。

class Person: NSObject {
   @objc var name: String
   @objc var friends: [Person] = []
   @objc var bestFriend: Person? = nil
   
   init(name: String) {
       self.name = name
   }
}

let gabrielle = Person(name: "Gabrielle")
let jim = Person(name: "Jim")
let yuanyuan = Person(name: "Yuanyuan")
gabrielle.friends = [jim, yuanyuan]
gabrielle.bestFriend = yuanyuan

#keyPath(Person.name)

gabrielle.value(forKey: #keyPath(Person.name))

#keyPath(Person.bestFriend.name)

gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))

#keyPath(Person.friends.name)

gabrielle.value(forKeyPath: #keyPath(Person.friends.name))


作者:桔子聽

連結:https://www.jianshu.com/p/742c6af3afae


相關推薦:


相關焦點

  • 通過LLVM 在 Android 上運行 Swift 代碼
    我們現在需要手動編譯和連接一個簡單的 Swift "Hello world" :// hello.swiftprint("Hello, world!");構建對象文件:$ $SDK/usr/bin/swiftc -emit-object hello.swifthello.o 裡面到底有什麼:$ nm hello_swift.o                 U __TFSSCfMSSFT21_builtinStringLiteralBp8byteSizeBw7isASCIIBi1
  • 【競賽】Evolutionary Transfer Multiobjective Optimization
    Many-objective Optimization Problems: ETMOF9 to ETMOF16Multitask Large-scale Multiobjective Optimization Problems: ETMOF17 to ETMOF24Many-task Multiobjective Optimization Problems: ETMOF25
  • Package Swift
    /Demo.xcodeproj$ swift build Compile Swift Module 'Demo' (1 sources)Linking ./.build/debug/Demo$ ./.build/debug/Demo  Hello, world!
  • 眉山芳綸高水基混編盤根廠家
    眉山芳綸高水基混編盤根廠家芳綸纖維盤根適用於有顆粒的流體及其介質、蒸汽、有機溶劑、酸、鹼等,應用於流體輸送設備裝置上所用的機械、泵、閥門、管道、容器等的密封。芳綸盤根由芳綸纖維浸漬聚四氟乳液和潤滑劑處理編織而成。有較好的耐化學性,高回彈,低冷流。
  • 15種快速技巧,提升Swift編碼能力
    通過十六進位代碼生成UIColor創建一個名為 UIColor+Extensions.swift的文件,包含以下代碼:import UIKitextension UIColor {convenience init(hex:Int, alpha: CGFloat = 1.0) {self.init(red: CGFloat
  • 北京:新歷史教材中外混編 世界大戰退出必修課
    每章開篇均有一個操作性強,或結合學生生活體驗,或聯繫當今世界和我國的重大問題和常見問題,或反映地理學科發展方向的研究課題。例如:「調查家族人口的增長和遷移」、「比較可持續發展的不同對策」、「認識地理信息技術的應用」等,有的課題學生必須獨立完成,有的課題必須分組完成。
  • 英漢混編「三字經」,有違少兒英語學習規律,是譁眾取寵的害人經
    這位老師就是就數百個單詞整理成英漢混編的「三字經」,合轍押韻,念起來朗朗上口,可以幫助孩子更高效地記住這些單詞。而我們也知道,英語和漢語發音要領不同,從口形到唇齒咽喉等的配合有著較大的差異。比如漢語聲母「d、t、n、l」,在某種程度上對應著英文的輔音字母/d/、/ t/、/n/、/l/,其發音咋一聽差不多,但細微處卻有著很大的不同。
  • C-Myc抑制DNA雙鏈斷裂修復
    Because our results showed that c-Myc suppresses both Ku and DNA-PKcs activities , it is also possible that c-Myc may negatively affect V(D)J recombination.
  • Swift 關鍵字
    與 struct 類似,不同之處在於:類型轉換,允許在運行時檢查和指定一個類的實際類型。對於類成員,允許在定義的模塊之外訪問和重寫。operator:特殊符號,用於檢查、修改、組合值。此外還可以用於區分函數參數和成員屬性名稱相同的情況。
  • 智能攝像機玩出新花樣,和目 C13c震撼發布
    不過當下智能攝像機的品牌雜亂,質量參差不齊,炒概念和噱頭的產品充斥著市場,安全隱患也更令人堪憂;真正能滿足需求的產品確實不多。在這樣一個良莠不齊的市場情況下,中移物聯網再次帶來了令廣大消費者振奮的消息——推出和目智能攝像機的新品,高性價比的和目C13c。
  • 谷歌為何要養蘋果的親兒子Swift?原來意在可微分編程
    他們還發布了一份非常詳實的文檔(https://github.com/tensorflow/swift/blob/master/docs/WhySwiftForTensorFlow.md),其中詳細地介紹了他們做出這一決定的歷程,並解釋了他們為這一任務考慮過的其它語言並最終選中 Swift 的原因。
  • C語言中的運算符和表達式
    1、加法和減法運算符加法運算符為「+」,使運算符兩側的值相加,兩側的值可以是變量、常量和表達式等。減法運算符為「-」,使運算符左側的值減去右側的值。符號運算符、自增和自減運算符為一元運算符。表一:邏輯運算符邏輯表達式運算結果:a&&b 只有a和b都是真時,表達式結果為真,有一個為假,表達式結果為假。a||b a或b有一個為真,表達式結果為真,a和b都為假,表達式結果為假。
  • 什麼是c位?c位是什麼意思?c位的含義
    c位這個詞想必大家一定耳熟能詳,哪怕不知道具體的含義,也多多少少聽過身邊的人說上那麼幾句。c位所以說,c位到底是什麼意思呢?但是不管怎麼樣,c所代表的就是最中心最重要的的意思。在最早期,「C位」一般指舞臺中央或藝人在宣傳海報的中間位置,一般來說,娛樂圈登臺時,只有資歷最老、本事最強的人才能站在c位。C位不僅是身份和本領的表現,更是讓觀眾第一眼就能看到與留意的最佳位置。後來隨著意識的拓展,這個詞也逐步被引申為——處在各種場合中最重要、最受關注的位置。
  • C語言之const和volatile"究極"學習
    說明:我這個版本的編譯器支持標準c語言,所以沒導致程序崩潰,能夠正常運行4、const的本質c語言中的const使得變量具有隻讀屬性現代c編譯器中的const將具有全局生命周期的變量存儲於只讀存儲區,不是放在全局數據區註:const不能定義真正意義上的常量;同時這裡注意static關鍵字修飾的變量,它的生命周期和全局變量一樣。
  • Linux 之父拒絕 996,Swift、Python 之父痴迷深夜編程,程式設計師之神...
    Ivan Bessarabov <ivan@bessarabov.ru> 1563188141 +0300committer Ivan Bessarabov <ivan@bessarabov.ru> 1563188141 +0300Initial commit從上圖你可以看到提交信息 (Initial commit) 、存儲文件結構的帳號的信息、提交作者的名字和郵件以及最有趣的時戳
  • 原來PARP抑制劑對腫瘤的效果和c-Met表達量相反!
    PARP和c-MET是什麼鬼?PARP(poly-ADP-ribose polymerase/聚ADP核糖聚合酶):是一類存在於多數真核細胞中的蛋白翻譯後修飾酶,能選擇性識別並結合DNA缺口的DNA結合蛋白酶,具有保持染色體結構完整、參與DNA複製和轉錄的功能,在維持基因組穩定和細胞死亡過程發揮重要作用。