将C语言字符数组转换为字符串

34

我有一个Swift程序,与一个C库进行Interop。这个C库返回一个结构体,其中包含一个char[]数组,就像这样:

struct record
{
    char name[8];
};

定义已经在Swift中正确导入。但是,该字段被解释为8个Int8元素的元组(类型为(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)),我不知道如何用Swift将其转换为String

没有接受Int8元组的String初始化程序,并且似乎无法获取元组的第一个元素的指针(由于类型可以是异构的,这并不令人惊讶)。

目前,我最好的想法是创建一个小的C函数,该函数接受指向结构本身的指针,并将name作为char*指针而不是数组返回,然后使用它。

但是,是否有一种纯Swift的方法来做到这一点呢?


2
你确定Interop让它成为了C问题吗?或者你的解决方法能够让它变成C问题吗?特别是当你想要一个纯Swift的解决方案时... - Deduplicator
1
这个字节数组与C语言没有什么关系,但如果你在C、C++、Objective-C或Objective-C++等语言中有一个有效的描述,这并不意味着它是一个C语言问题。 - Deduplicator
2
我不认识任何一个人用"C++数组"或"Objective-C数组"或"Objective-C ++数组"这样的称呼,也不知道"C数组"有其他定义。当我寻找解决方案时,我在搜索中使用了"C数组"这个关键词,除非我是例外,否则我相信下一个遇到同样问题的人会做同样的事情。我认为标签对于搜索请求最为重要,而它们的分类目的其次。 - zneak
2
如果将C语言经常被描述为低级和本地的同义词,标记为C,那么这将淹没与本地交互有关的几乎所有内容。非常糟糕的想法。 - Deduplicator
这个问题在SO经常出现。问题是标签真的是标签,还是它们是频道?如果它们是主题标签,那么它属于C和Swift搜索术语。如果用户订阅标签像频道一样,那么他们会收到他们不感兴趣的东西...如果是这种情况,标签就被过载了,这是SO实现上的缺陷。 - Troy Harvey
显示剩余3条评论
9个回答

41

C语言的数组char name[8]在Swift中被导入为元组:

(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)

name的地址和name[0]的地址相同,而Swift保留了从C导入的结构体的内存布局,正如苹果工程师Joe Groff所证实的那样:...

...您可以将在C中定义的结构导入到Swift中。 Swift将尊重C的布局。

因此,我们可以将转换为UInt8指针的record.name的地址传递给String初始化程序。以下代码已更新为Swift 4.2 及更高版本:

let record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
        String(cString: $0)
    }
}

注意: 假定name[]中的字节是一个有效的以NUL结尾的UTF-8序列。

对于早期版本的Swift:

// Swift 2:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(&record.name) {
    String.fromCString(UnsafePointer($0))!
}

// Swift 3:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: &record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: record.name)) {
        String(cString: $0)
    }
}

1
是的,这个可以工作。我只是想补充一下,record 需要是可变的(用var声明),否则Swift编译器会报一个奇怪的错误。 - zneak
@WilShipley:当然,这就是我在回答中加入最后一句话的原因。 - Martin R
1
@NateCook:如果结构是从C导入的,那么它确实是有保证的。我已经更新了答案,并提供了参考资料,并简化了代码。 - Martin R
不要在 MemoryLayout.size(..) 调用中引用 record.name -- 使用 $0 来避免同时访问。 - MechEthan
1
@MechEthan:感谢您的提醒!因为(从Swift 4.2开始),您可以获取不可变值的地址,即您可以使用withUnsafePointer(to:)来处理常量值,这也避免了同时(变异)访问的问题。我已经相应地更新了代码。 - Martin R
显示剩余4条评论

4
你可以使用Swift的可变参数语法将元组收集到数组中:

您可以通过使用Swift的可变参数语法将元组收集到数组中:

let record = getRecord()
let (int8s: Int8...) = myRecord          // int8s is an [Int8]
let uint8s = int8s.map { UInt8($0) }
let string = String(bytes: uint8s, encoding: NSASCIIStringEncoding)
// myString == Optional("12345678")

看起来不错(之前不知道可变元组语法),但在实际情况下,元组有32个元素,我可以想象自己需要更大的数组(如128个元素)。这将导致类型注释非常繁琐。您是否知道如何使其与元素数量无关? - zneak
let (int8s: Int8...) 是什么结构?let (name : type) = exp 通常与 let name : type = expr 相同吗? - Martin R
@zneak 如果是这样,你需要在内联中执行它,因为使用函数时,您必须键入正确数量的元素以匹配每个特定元组的arity。我已经修改了我的答案,以展示您可以做什么。 - Nate Cook
1
@MartinR 它的名称为元组分解。您可以使用此技术以有趣的方式提取内容,例如:let (_, second, _, fourth, theRest: Int...) = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - Nate Cook
@NateCook:谢谢!我还没有在文档中找到元组赋值的可变参数部分,但我会继续深入挖掘。 (这看起来几乎像Perl:my ($a, undef, @b) = (1, 2, 3, 4, 5, 6); :)) - Martin R
3
自从Swift 2在Xcode 7b1 beta中发布以来,这个方法已经不再适用。 - zneak

3

我也对这个很感兴趣,所以我添加了一个新的函数:

func asciiCArrayToSwiftString(cString:Int8...) -> String
{
    var swiftString = String()            // The Swift String to be Returned is Intialized to an Empty String
    var workingCharacter:UnicodeScalar = UnicodeScalar(UInt8(cString[0]))
    var count:Int = cString.count

    for var i:Int = 0; i < count; i++
    {
        workingCharacter = UnicodeScalar(UInt8(cString[i])) // Convert the Int8 Character to a Unicode Scalar
        swiftString.append(workingCharacter)             // Append the Unicode Scalar

    }

    return swiftString                     // Return the Swift String
}

我使用以下代码调用该函数:

    let t:Int8 = Int8(116)
    let e:Int8 = Int8(101)
    let s:Int8 = Int8(115)
    let testCString = (t, e, s, t)
    let testSwiftString = wispStringConverter.asciiCArrayToSwiftString(testCString.0, testCString.1, testCString.2, testCString.3)
    println("testSwiftString = \(testSwiftString)")

生成的输出为:
testSwiftString = test

2

我刚刚使用Swift 3 (3.0.2)遇到了类似的问题。我试图将一个CChar数组 [CChar] 转换为Swift中的String类型。结果发现,Swift 3有一个String初始化器可以接受一个cString。

举个例子:

let a = "abc".cString(using: .utf8) // type of a is [CChar]
let b = String(cString: a!, encoding: .utf8) // type of b is String
print("a = \(a)")
print("b = \(b)")

结果为

a = 可选项([97, 98, 99, 0])

b = 可选项("abc")

请注意,对String类型使用cString函数会导致可选项。在使用String.init函数创建b时,必须进行强制解包。而且b也是可选项...这意味着两者都可能为空,因此应该使用错误检查。


1
这里是我想到的解决方案,它使用反射将元组转换为[Int8](参见Any way to iterate a tuple in swift?),然后使用fromCString...()方法将其转换为字符串。
func arrayForTuple<T,E>(tuple:T) -> [E] {
    let reflection = reflect(tuple)
    var arr : [E] = []
    for i in 0..<reflection.count {
        if let value = reflection[i].1.value as? E {
            arr.append(value)
        }
    }
    return arr
}

public extension String {
    public static func fromTuple<T>(tuple:T) -> String? {
        var charArray = arrayForTuple(tuple) as [Int8]
        var nameString = String.fromCString(UnsafePointer<CChar>(charArray))
        if nameString == nil {
            nameString = String.fromCStringRepairingIllFormedUTF8(UnsafePointer<CChar>(charArray)).0
        }
        return nameString
    }
}

1

Swift 3. 只使用反射。当遇到空字节时,此版本会停止构建字符串。已经过测试。

func TupleOfInt8sToString( _ tupleOfInt8s:Any ) -> String? {
    var result:String? = nil
    let mirror = Mirror(reflecting: tupleOfInt8s)

    for child in mirror.children {
        guard let characterValue = child.value as? Int8, characterValue != 0 else {
            break
        }

        if result == nil {
            result = String()
        }
        result?.append(Character(UnicodeScalar(UInt8(characterValue))))
    }

    return result
}

1

试试这个:

func asciiCStringToSwiftString(cString:UnsafePointer<UInt8>, maxLength:Int) -> String
{
    var swiftString = String()  // The Swift String to be Returned is Intialized to an Empty String
    var workingCharacter:UnicodeScalar = UnicodeScalar(cString[0])
    var count:Int = 0           // An Index Into the C String Array Starting With the First Character

    while cString[count] != 0             // While We Haven't reached the End of the String
    {
        workingCharacter = UnicodeScalar(cString[count]) // Convert the ASCII Character to a Unicode Scalar
        swiftString.append(workingCharacter)             // Append the Unicode Scalar Version of the ASCII Character
        count++                                          // Increment the Index to Look at the Next ASCII Character

        if count > maxLength                            // Set a Limit In Case the C string was Not NULL Terminated
        {
            if printDebugLogs == true
            {
                swiftString="Reached String Length Limit in Converting ASCII C String To Swift String"
            }
            return swiftString
        }
    }

    return swiftString                     // Return the Swift String
}

C字符串不是一个UnsafePointer<UInt8>,它是由8个Int8元素组成的元组,因此这种方法不能解决我的问题。此外,String类有一个init?(UTF8String: UnsafePointer<CChar>)可失败初始化程序,可以完美地实现这一点。 - zneak
在我的应用程序中,我需要记录当前的OpenGL版本,以确保像素格式设置正确。我使用let glVersionCString:UnsafePointer<UInt8> = glGetString(GLenum(GL_VERSION))来实现这一点。编译器不允许我将glGetString的返回值强制转换为UnsafePointer<CChar>,因此我无法使用Swift字符串初始化程序。这就是这个函数存在的原因。 - jwlaughton
我并不质疑你的函数在特定用例中的有用性,但它在这里不适用。这对你有效是因为glGetString返回一个指针。而我处理的结构体有一个数组字段,这与指针字段非常不同。正如我所说,Swift看到的字段类型是(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8),而不是UnsafePointer<UInt8> - zneak

1

关于这个话题已经有多个答案了,但没有一个是简单的一行代码,也没有一个解决非空终止字符串的问题。

假设字符串以NULL结尾:

struct record {
    char name[8];
};

//Might by unsafe, depends
String(cString: &record.name.0)

//Safe
String(cString: unsafeBitCast(UnsafePointer(&record.name), to: UnsafePointer<Int8>.self))

对于没有以NULL 结尾的字符串:

//Might by unsafe, depends
String(cString: &record.name.0).prefix(MemoryLayout.size(ofValue: record.name))

//Safe
String(bytesNoCopy: UnsafeMutableRawPointer(mutating: &record.name), length: MemoryLayout.size(ofValue: record.name), encoding: .utf8, freeWhenDone: false)

––––

关于@MartinR的担忧,您也可以传递整个变量的指针,但就我个人而言,我从未遇到过Swift只传递一个字节的情况,所以应该是安全的。

请注意,您的解决方案存在两个未定义行为方面。首先,您只将单个字符作为inout表达式传递给String初始化程序,请参阅OOP在此处的评论:https://dev59.com/d57ha4cB1Zd3GeqPeh0O#41599023。其次,如果字符串没有以NULL结尾,则可能读取未定义内存的内容。 - Martin R
@MartinR 那么将整个变量的引用传递过去怎么样? - John Smith
那应该可以工作,而且大多数情况下没有运行时差异。我仍然更喜欢使用withMemoryRebound而不是unsafeBitCast,因为它被记录为“仅在通过其他方式无法进行转换时才使用此函数将传递的实例转换为布局兼容类型。...调用此函数会破坏Swift类型系统的保证;请极度小心使用。” - Martin R
@MartinR 关于非空终止字符串,确实,人们应该谨慎使用我的方法,在某些情况下可能是危险的,但在像我这样的情况下,它是完全安全的。我有一个带有两个char[]和其他一些东西的结构体。在数组和其他内容之间有填充,并且在填充之前将结构体清零,因此保证它将停止在那里读取,并且不会读取未分配的内存。 - John Smith

1

详情

  • Xcode 11.2.1 (11B500),Swift 5.1

解决方案

extension String {
    init?(fromTuple value: Any) {
        guard let string = Tuple(value).toString() else { return nil }
        self = string
    }

    init?(cString: UnsafeMutablePointer<Int8>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init?(cString: UnsafeMutablePointer<CUnsignedChar>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init? (cString: Any) {

        if let pointer = cString as? UnsafeMutablePointer<CChar> {
            self = String(cString: pointer)
            return
        }

        if let pointer = cString as? UnsafeMutablePointer<CUnsignedChar> {
            self = String(cString: pointer)
            return
        }

        if let string = String(fromTuple: cString) {
            self = string
            return
        }

        return nil
    }
}

// https://dev59.com/rWAf5IYBdhLWcg3wwk-5#58869882

struct Tuple<T> {
    let original: T
    private let array: [Mirror.Child]
    init(_ value: T) {
        self.original = value
        array = Array(Mirror(reflecting: original).children)
    }
    func compactMap<V>(_ transform: (Mirror.Child) -> V?) -> [V] { array.compactMap(transform) }

    func toString() -> String? {

        let chars = compactMap { (_, value) -> String? in
            var scalar: Unicode.Scalar!
            switch value {
            case is CUnsignedChar: scalar = .init(value as! CUnsignedChar)
            case is CChar: scalar = .init(UInt8(value as! CChar))
            default: break
            }
            guard let _scalar = scalar else { return nil }
            return String(_scalar)
        }
        if chars.isEmpty && !array.isEmpty { return nil }
        return chars.joined()
    }
}

使用方法(完整示例)

C语言代码(Header.h)

#ifndef Header_h
#define Header_h

#ifdef __cplusplus
extern "C" {
#endif

char c_str1[] = "Hello world!";
char c_str2[50] = "Hello world!";
char *c_str3 = c_str2;

typedef unsigned char UTF8CHAR;
UTF8CHAR c_str4[] = {72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0};
UTF8CHAR *c_str5 = c_str4;
UTF8CHAR c_str6[] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\0'};
UTF8CHAR *c_str7 = 0;
UTF8CHAR *c_str8 = "";

#define UI BYTE

#ifdef __cplusplus
}
#endif

#endif /* Header_h */

``` "...-Bridging-Header.h" ```
"...-Bridging-Header.h"
#include "Header.h"

Swift 代码
func test() {
    printInfo(c_str1)
    printInfo(c_str2)
    printInfo(c_str3)
    printInfo(c_str4)
    printInfo(c_str5)
    printInfo(c_str6)
    printInfo(c_str7)
    printInfo(c_str8)

    print(String(fromTuple: c_str1) as Any)
    print(String(fromTuple: c_str2) as Any)
    print(String(cString: c_str3) as Any)
    print(String(fromTuple: c_str4) as Any)
    print(String(cString: c_str5) as Any)
    print(String(fromTuple: c_str6) as Any)
    print(String(fromTuple: c_str7) as Any)
    print(String(cString: c_str8) as Any)
}

var counter = 1;

func printInfo(_ value: Any?) {
    print("name: str_\(counter)")
    counter += 1
    guard let value = value else { return }
    print("type: \(type(of: value))")
    print("value: \(value)")
    print("swift string: \(String(cString: value))")
    print("\n-----------------")
}

输出

name: str_1
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_2
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
swift string: Optional("Hello world!\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")

-----------------
name: str_3
type: UnsafeMutablePointer<Int8>
value: 0x000000010e8c5d40
swift string: Optional("Hello world!")

-----------------
name: str_4
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0)
swift string: Optional("Hello world \0")

-----------------
name: str_5
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c5d80
swift string: Optional("Hello world ")

-----------------
name: str_6
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_7
name: str_8
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c0ae0
swift string: Optional("")

-----------------
Optional("Hello world!\0")
Optional("Hello world!\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
Optional("Hello world!")
Optional("Hello world \0")
Optional("Hello world ")
Optional("Hello world!\0")
Optional("")
Optional("")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接