为什么我不能将可选的Swift String传递给允许NULL指针的C函数?
我有一个处理C字符串的C函数。 该函数实际上允许字符串为NULL指针。 声明如下:
size_t embeddedSize ( const char *_Nullable string );
在C中使用这样的函数没问题:
size_t s1 = embeddedSize("Test"); size_t s2 = embeddedSize(NULL); // s2 will be 0
现在我正试图从Swift中使用它。 以下代码有效
let s1 = embeddedSize("Test") let s2 = embeddedSize(nil) // s2 will be 0
但什么不起作用是传递一个可选的字符串! 这段代码不会编译:
let someString: String? = "Some String" let s2 = embeddedSize(someString)
编译器抛出一个关于可选的未被解包的错误,Xcode询问我是否可能忘记添加!
还是?
。 但是,为什么我要打开它呢? NULL
或nil
是要提供给函数的有效值。 看到上面,我只是直接传递给它,并且编译得很好并返回了预期的结果。 在我的真实代码中,字符串是从外部提供的,它是可选的,我不能强行打开它,如果字符串nil
则会破坏。 那么如何用可选字符串调用该函数呢?
最可能的答案是,虽然字符串文字可以转换为UnsafePointer
,而nil
可以转换为UnsafePointer
,而String
也是, String?
可能不在Swift 2中。
在Swift 2中,C函数
size_t embeddedSize ( const char *_Nullable string );
映射到Swift as
func embeddedSize(string: UnsafePointer) -> Int
并且您可以传递一个(非可选的)Swift字符串作为参数,如“使用Swift with Cocoa和Objective-C”参考中的C ++ 交互中所述:
恒指针
当函数声明为采用
UnsafePointer
参数时,它可以接受以下任何一种:
- …
String
Type
,如果Type
为Int8
或UInt8
。 该字符串将在缓冲区中自动转换为UTF8,并将指向该缓冲区的指针传递给该函数。- …
你也可以传递nil
因为在Swift 2中, nil
是UnsafePointer
的允许值。
正如@zneak指出的那样,UTF-8的“自动转换”对Swift 2中的可选字符串不起作用,因此你必须(有条件地)打开字符串:
let someString: String? = "Some String" let s2: size_t if let str = someString { s2 = embeddedSize(str) } else { s2 = embeddedSize(nil) }
使用Optional
和nil-coalescing运算符的map
方法??
,这可以写得更紧凑
let someString: String? = "Some String" let s2 = someString.map { embeddedSize($0) } ?? embeddedSize(nil)
@zneak提出了一个通用的解决方案。
这是另一种可能的解决方案 String
有一个方法
func withCString(@noescape f: UnsafePointer throws -> Result) rethrows -> Result
它使用指向字符串的UTF-8表示的指针调用闭包,通过执行f
延长寿命。 因此,对于非可选字符串,以下两个语句是等效的:
let s1 = embeddedSize("Test") let s1 = "Test".withCString { embeddedSize($0) }
我们可以为可选字符串定义类似的方法。 由于generics类型的扩展只能将类型占位符限制为协议而不是具体类型,因此我们必须定义String
符合的协议:
protocol CStringConvertible { func withCString(@noescape f: UnsafePointer throws -> Result) rethrows -> Result } extension String: CStringConvertible { } extension Optional where Wrapped: CStringConvertible { func withOptionalCString(@noescape f: UnsafePointer -> Result) -> Result { if let string = self { return string.withCString(f) } else { return f(nil) } } }
现在可以使用可选的字符串参数调用上面的C函数
let someString: String? = "Some String" let s2 = someString.withOptionalCString { embeddedSize($0) }
对于多个C字符串参数,闭包可以嵌套:
let string1: String? = "Hello" let string2: String? = "World" let result = string1.withOptionalCString { s1 in string2.withOptionalCString { s2 in calculateTotalLength(s1, s2) } }
显然,问题已经在Swift 3中解决了。这里C函数被映射到
func embeddedSize(_ string: UnsafePointer?) -> Int
并传递一个String?
对于nil
和非零参数,编译和按预期工作。
通过额外的快速级别间接调用的所有解决方案,如果您只有一个参数,则工作正常。 但我也有这样的C函数( strX
不是真正的参数名称,调用实际上是简化的):
size_t calculateTotalLength ( const char *_Nullable str1, const char *_Nullable str2, const char *_Nullable str3, const char *_Nullable str4, const char *_Nullable str5 );
在这里,这种间接变得不切实际,因为我需要一个每个参数的间接,上面的函数的5个间接。
这是迄今为止我提出的最好(丑陋)“hack”,它避免了这个问题(我仍然很高兴看到任何更好的解决方案,也许有人会看到这个代码):
private func SwiftStringToData ( string: String? ) -> NSData? { guard let str = string else { return nil } return str.dataUsingEncoding(NSUTF8StringEncoding) } let str1 = SwiftStringToData(string1) let str2 = SwiftStringToData(string2) let str3 = SwiftStringToData(string3) let str4 = SwiftStringToData(string4) let str5 = SwiftStringToData(string5) let totalLength = calculateTotalLength( str1 == nil ? UnsafePointer(nil) : UnsafePointer (str1!.bytes), str2 == nil ? UnsafePointer (nil) : UnsafePointer (str2!.bytes), str3 == nil ? UnsafePointer (nil) : UnsafePointer (str3!.bytes), str4 == nil ? UnsafePointer (nil) : UnsafePointer (str4!.bytes), str5 == nil ? UnsafePointer (nil) : UnsafePointer (str5!.bytes), )
如果有人想到将data.bytes
的结果data.bytes
回给调用者,这是一个非常糟糕的主意! data.bytes
返回的指针只保证有效,只要data
本身保持活动状态,ARC就会尽快data.bytes
data
。 所以以下是无效的代码:
// --- !!! BAD CODE, DO NOT USE !!! --- private func SwiftStringToData ( string: String? ) -> UnsafePointer? { guard let str = string else { UnsafePointer (nil) } let data = str.dataUsingEncoding(NSUTF8StringEncoding) return UnsafePointer (data.bytes) }
当此方法返回时,无法保证数据仍处于活动状态,返回的指针可能是悬空指针! 然后我想到了以下几点:
// --- !!! BAD CODE, DO NOT USE !!! --- private func DataToCString ( data: NSData? ) -> UnsafePointer? { guard let d = data else { UnsafePointer (nil) } return UnsafePointer (d.bytes) } let str1 = SwiftStringToData(string1) let cstr1 = DataToCString(str1) // (*1) // .... let totalLength = calculateTotalLength(cstr1, /* ... */)
但这并不能保证安全。 编译器看到str1
在到达(*1)
时不再被引用,因此它可能无法保持活动状态,当我们到达最后一行时, cstr1
已经是一个悬空指针。
它只是安全的,如我的第一个示例所示,因为NSData
对象( str1
等)必须保持活动到calculateTotalLength()
函数调用,并且某些方法(如NSData
bytes
或NSString
UTF8String
)被标记为返回内部指针,在这种情况下,只要仍引用对象或此类内部指针,编译器将确保在当前作用域中扩展对象的生命周期。 这个机制确保返回的指针( str1.bytes
等)肯定会保持有效,直到C函数调用返回。 没有这种特殊的标记,甚至没有保证! 在检索字节指针之后但在进行函数调用之前,编译器可能会直接释放NSData
对象,因为它不知道释放数据对象会使指针悬空。