Liên thông
Objective-C và C nhìn từ Swift, và cây cầu ấy tốn gì
Swift không xuất hiện trên một nền tảng trống không. Mọi framework của Apple cũ hơn 2014 đều là Objective-C hoặc C ở bên dưới, và rất nhiều thứ trông như một API Swift thật ra là một bản dịch của chúng.
Phần lớn thời gian bản dịch ấy vô hình và miễn phí. Những trường hợp nó không vô hình cũng chẳng miễn phí thì đáng biết, vì đó là chỗ sinh ra những vấn đề hiệu năng bất ngờ và những cú sập bất ngờ.
Thứ gì đi qua được ranh giới Objective-C
Chỉ một phần của Swift diễn đạt được bằng Objective-C. Một kiểu nhìn thấy được từ Objective-C khi nó
là một class kế thừa NSObject và được đánh dấu @objc, và chỉ những thành viên có chữ ký diễn đạt
được mới đi qua cùng nó:
@objc final class Analytics: NSObject {
@objc func track(_ event: String) { … } // ổn
func record(_ result: Result<Int, Error>) { … } // chỉ Swift: không có tương đương Objective-C
}
Struct, enum có giá trị đi kèm, generic, tuple, protocol có associated type, và existential kèm mệnh
đề where đều không có cách biểu diễn trong Objective-C. Chúng không gây lỗi trừ khi bạn đánh dấu
@objc — đơn giản là chúng không nhìn thấy được từ phía bên kia.
@objcMembers đặt trên một class phơi ra mọi thứ có thể phơi, việc này tiện và thường là quá rộng.
Nó đưa cả class vào điều phối thông điệp cho mọi thứ Objective-C nhìn thấy được, kèm những cái giá đã
mô tả ở chương điều phối.
Các kiểu được bắc cầu
Một số kiểu tự chuyển đổi ở ranh giới:
| Swift | Objective-C |
|---|---|
String |
NSString |
Array |
NSArray |
Dictionary |
NSDictionary |
Set |
NSSet |
Int, Double |
NSNumber |
Data |
NSData |
Error |
NSError |
Việc chuyển đổi không phải lúc nào cũng miễn phí, và đây là phần khiến người ta bất ngờ. String
sang NSString thường rẻ vì phần lưu trữ hay dùng chung được. Array sang NSArray có thể phải sao
chép từng phần tử — và nếu các phần tử là kiểu giá trị của Swift thì mỗi cái phải được đóng hộp riêng.
let points: [CGPoint] = …
someObjectiveCAPI.process(points) // chi phí bắc cầu tỷ lệ với số phần tử
Trong một vòng lặp trên một mảng lớn, cái giá đó là có thật. Cách chữa không phải là né API mà là vượt ranh giới một lần với cả tập hợp thay vì vượt nhiều lần với từng mẩu.
Cảnh báo
Những phép ép kiểu dạng as? NSString giữa các kiểu được bắc cầu trông như chuyển kiểu miễn phí,
nhưng không phải. Một phép ép kiểu trong vòng lặp nóng là một thao tác bắc cầu ở mỗi vòng. Hãy
đưa nó ra ngoài.
Tính nullable, và một header không có chú thích thì sao
Tham chiếu trong Objective-C có thể null; của Swift thì không. Cây cầu dựa vào các chú thích trong header:
- (nullable NSString *)nameForID:(NSInteger)identifier; // nhập vào thành String?
- (nonnull NSString *)displayName; // nhập vào thành String
- (NSString *)legacyName; // nhập vào thành String!
Cái thứ ba mới nguy hiểm. Một phương thức không có chú thích được nhập vào thành một optional được mở ngầm — Swift sẽ không bắt bạn kiểm tra nó, và nó sẽ sập ngay tại chỗ dùng nếu nó là nil.
Khi bọc một thư viện Objective-C cũ, thay đổi có giá trị cao nhất là thêm NS_ASSUME_NONNULL_BEGIN /
NS_ASSUME_NONNULL_END vào các header rồi đánh dấu những chỗ thật sự nullable. Việc đó biến cả một
lớp các cú sập lúc chạy thành những optional lúc biên dịch.
Lỗi đi qua ranh giới
Objective-C báo lỗi bằng một tham số ra NSError ** và một giá trị trả về BOOL. Swift nhập khuôn
mẫu đó vào thành throws:
- (BOOL)saveToURL:(NSURL *)url error:(NSError **)error;
try document.save(to: url)
Bản dịch mang tính máy móc và chạy tốt. Theo chiều ngược lại, một Error của Swift trở thành một
NSError với domain, code và userInfo — nên một lỗi kiểu enum của Swift khi đi sang
Objective-C sẽ mất các giá trị đi kèm, trừ khi kiểu đó tuân thủ CustomNSError và cung cấp chúng
trong errorUserInfo.
C, và các kiểu con trỏ
Các API của C được nhập vào với con trỏ ánh xạ sang họ Unsafe của Swift:
| C | Swift |
|---|---|
const void * |
UnsafeRawPointer |
void * |
UnsafeMutableRawPointer |
const int * |
UnsafePointer<Int32> |
int * |
UnsafeMutablePointer<Int32> |
char * |
UnsafeMutablePointer<CChar> |
Quy tắc tối quan trọng về tất cả những thứ này: một con trỏ lấy được bên trong một closure with…
chỉ hợp lệ bên trong closure đó.
// Sai — con trỏ treo lơ lửng ngay khi closure trả về
var escaped: UnsafePointer<UInt8>?
data.withUnsafeBytes { escaped = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) }
// Đúng — hãy làm việc ngay bên trong
data.withUnsafeBytes { buffer in
cLibraryFunction(buffer.baseAddress, buffer.count)
}
Swift có thể cấp phát một vùng đệm tạm trong suốt lời gọi rồi giải phóng nó sau đó, nên con trỏ đã thoát ra không chỉ là cũ — nó trỏ vào vùng nhớ đã được trả lại. Lỗi sinh ra thì lúc có lúc không và trông như dữ liệu bị hỏng, tổ hợp tệ nhất có thể.
Với chuỗi kiểu C, withCString lo cả phần mã hóa lẫn phần vòng đời:
name.withCString { pointer in
c_register_name(pointer)
}
Khi nào nên tự viết cây cầu
Gọi thẳng một API C hay Objective-C từ code ứng dụng sẽ rải sự mất an toàn ra khắp codebase. Khuôn mẫu giữ nó lại một chỗ là một lớp bọc Swift mỏng ở ranh giới:
struct ImageDecoder {
func decode(_ data: Data) throws -> Image {
try data.withUnsafeBytes { buffer in
var handle: OpaquePointer?
guard c_decode(buffer.baseAddress, buffer.count, &handle) == 0 else {
throw DecodeError.malformed
}
defer { c_free(handle) }
return Image(handle: handle)
}
}
}
Một kiểu sở hữu các lời gọi không an toàn, các vòng đời và phần dịch lỗi, còn mọi thứ phía trên nó là
Swift bình thường với một chữ ký throws. Cái ranh giới ấy là nơi defer dọn dẹp thuộc về, và nó là
chỗ duy nhất trong codebase cần được xem lại khi thư viện C thay đổi.