为什么我不能将可选的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询问我是否可能忘记添加! 还是? 。 但是,为什么我要打开它呢? NULLnil是要提供给函数的有效值。 看到上面,我只是直接传递给它,并且编译得很好并返回了预期的结果。 在我的真实代码中,字符串是从外部提供的,它是可选的,我不能强行打开它,如果字符串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 ,如果TypeInt8UInt8 。 该字符串将在缓冲区中自动转换为UTF8,并将指向该缓冲区的指针传递给该函数。

你也可以传递nil因为在Swift 2中, nilUnsafePointer的允许值。

正如@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 bytesNSString UTF8String )被标记为返回内部指针,在这种情况下,只要仍引用对象或此类内部指针,编译器将确保在当前作用域中扩展对象的生命周期。 这个机制确保返回的指针( str1.bytes等)肯定会保持有效,直到C函数调用返回。 没有这种特殊的标记,甚至没有保证! 在检索字节指针之后但在进行函数调用之前,编译器可能会直接释放NSData对象,因为它不知道释放数据对象会使指针悬空。