在网络请求或接口签名中,通常要求将参数按照一定规则拼接成字符串。一个常见的做法是对字典的 key 进行排序,然后按照 “key=value” 的格式拼接,多个参数之间以特定符号(例如 &
)连接。
如果参数中包含嵌套的字典或数组,则需要递归展开。这样的处理不仅可以保证字符串的唯一性,同时也方便后续的加密或签名操作。
- 入参
- 一个可以转成json的字典或者数组
- 比如:["b": 2, "a": 1]
- 返回值
- 按照key进行排序后的字符串
-
比如:a=1&b=2
下面的算法不仅支持简单的键值对,还能够递归处理嵌套结构,使得所有数据都能被有序地转换为字符串形式,从而满足不同业务场景下的参数签名需求。
为什么非要自己实现呢?
- 这是因为字典是无序的,一个key-value完全相等的字典,转成jsonString是不一样的。
- 平台差异,iOS平台转成的json字符串和Java平台、go平台生成的字符串规则是不一样的,需要一个规则把无序的对象处理成有序的字符串
同一个平台,对同一个字典,生成json字符串结果都不一样。比如:
func testOrign() { let dictionary: [String: Any] = ["b": [1, ["d": 4, "c": 3] ], "a": ["e": 5, "f": [6, 7], "uid": 8, "age": 20.124, "student": true, "teacher": false, "dic":["rID":"123", "author":"gcs", ] ], "image": ["10", "11", "12", "15"], "array":[ [1,2,3], ["1","2","3"], ["rID":"123","author":"gcs"], ["rID":"1234","author":"g"], ] ] // 把这个字典转出josn字符串,然后再转回字典,看看是否和原来的字典一样 let jsonData = try! JSONSerialization.data(withJSONObject: dictionary, options: []) let jsonString = String(data: jsonData, encoding: .utf8)! print("jsonString",jsonString) do { let dictionary2 = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any] let jsonData2 = try! JSONSerialization.data(withJSONObject: dictionary2, options: []) let jsonString2 = String(data: jsonData2, encoding: .utf8)! print("jsonString2",jsonString2) } }
实现思路
SignTool
类的核心在于两个方法:一个是用于处理字典的 concatenate(dictionary:)
,另一个是私有方法 concatenate(array:)
,用于处理数组。整个实现思路可以分为以下几个步骤:
-
字典的排序与遍历
- 首先,对传入的字典的所有 key 进行排序,确保每次拼接后的结果一致。
- 遍历排序后的 key,根据对应的 value 类型分别处理:
- 如果 value 是字典,则递归调用自身,将字典内容转换为字符串;
- 如果 value 是数组,则调用数组处理方法;
- 如果 value 是基本类型(例如数字或字符串),直接转换为字符串;
- 其他类型则按默认方式拼接,最后兜底。
-
数组的递归拼接
- 数组中的每个元素可能又是字典或数组,需要递归调用对应的方法。
- 每个元素转换后,通过逗号分隔,并在结束时移除多余的逗号。
-
格式规范
- 字典的 key-value 对之间使用
&
符号分隔; - 数组的元素间使用
,
分隔,并在嵌套拼接时加上{}
或[]
表示不同的数据结构。
- 字典的 key-value 对之间使用
代码实现
下面是完整的代码实现,并附有详细注释:
import Foundation
class SignTool {
// 类方法,接收一个字典作为参数,返回拼接好的字符串
class func concatenate(dictionary: [String: Any]) -> String {
// 对字典的key进行排序
let sortedKeys = dictionary.keys.sorted()
var result = ""
// 遍历排序后的key
for (index, key) in sortedKeys.enumerated() {
if let value = dictionary[key] {
// 如果value是字典类型,递归调用concatenate方法
if let valueDict = value as? [String: Any] {
result += "\(key)={\(self.concatenate(dictionary: valueDict))}"
// 如果value是数组类型,递归调用concatenate方法
} else if let valueArray = value as? [Any] {
result += "\(key)=[\(self.concatenate(array: valueArray))]"
// 如果value是NSNumber类型,直接拼接
} else if let numberValue = value as? NSNumber {
result += "\(key)=\(numberValue)"
// 如果value是字符串类型,直接拼接
} else if let stringValue = value as? String {
result += "\(key)=\(stringValue)"
// 其他类型的value,直接拼接
} else {
result += "\(key)=\(value)"
}
}
// 在key-value对之间添加&符号
if index < sortedKeys.count - 1 {
result += "&"
}
}
return result
}
// 私有类方法,接收一个数组作为参数,返回拼接好的字符串
private class func concatenate(array: [Any]) -> String {
var result = ""
// 遍历数组中的每个元素
for value in array {
// 如果元素是字典类型,递归调用concatenate方法
if let valueDict = value as? [String: Any] {
result += "{\(self.concatenate(dictionary: valueDict))},"
// 如果元素是数组类型,递归调用concatenate方法
} else if let valueArray = value as? [Any] {
result += "[\(self.concatenate(array: valueArray))],"
// 如果元素是NSNumber类型,直接拼接
} else if let numberValue = value as? NSNumber {
result += "\(numberValue),"
// 如果元素是字符串类型,直接拼接
} else if let stringValue = value as? String {
result += "\(stringValue),"
// 其他类型的元素,直接拼接
} else {
result += "\(value),"
}
}
// 移除最后的逗号
if result.hasSuffix(",") {
result.removeLast()
}
return result
}
}
实际效果
通过上述实现,可以将复杂的字典与数组数据转换为统一的字符串格式。例如:
输出结果类似于:
a={age=20.124&e=5&f=[6,7]&student=1&teacher=0&uid=8}&b=[1,{c=3&d=4}]&image=[10,11,12,15]
其他测试用例及代码:
class SignToolTests: NSObject {
public static func test() {
print("start test")
let test = SignToolTests()
test.testConcatenateWithSimpleDictionary()
test.testConcatenateWithNestedDictionary()
test.testConcatenateWithArray()
test.testConcatenateWithNestedArray()
test.testConcatenateWithComplexStructure()
test.testConcatenate()
test.testMax()
print("end test")
}
func testConcatenateWithSimpleDictionary() {
let dictionary: [String: Any] = ["b": 2, "a": 1]
let result = SignTool.concatenate(dictionary: dictionary)
XCTAssertEqual(result, "a=1&b=2")
}
func testConcatenateWithNestedDictionary() {
let dictionary: [String: Any] = ["b": ["d": 4, "c": 3], "a": 1]
let result = SignTool.concatenate(dictionary: dictionary)
XCTAssertEqual(result, "a=1&b={c=3&d=4}")
}
func testConcatenateWithArray() {
let dictionary: [String: Any] = ["b": [1, 2, 3], "a": 1]
let result = SignTool.concatenate(dictionary: dictionary)
XCTAssertEqual(result, "a=1&b=[1,2,3]")
}
func testConcatenateWithNestedArray() {
let dictionary: [String: Any] = ["b": [1,
["d": 4,
"c": 3]
],
"a": 1]
let result = SignTool.concatenate(dictionary: dictionary)
XCTAssertEqual(result, "a=1&b=[1,{c=3&d=4}]")
}
func testConcatenateWithComplexStructure() {
let dictionary: [String: Any] = ["b": [1,
["d": 4, "c": 3]
],
"a": ["e": 5,
"f": [6, 7]
]
]
let result = SignTool.concatenate(dictionary: dictionary)
XCTAssertEqual(result, "a={e=5&f=[6,7]}&b=[1,{c=3&d=4}]")
}
func testConcatenate() {
let dictionary: [String: Any] = ["b": [1,
["d": 4, "c": 3]
],
"a": ["e": 5,
"f": [6, 7],
"uid": 8,
"age": 20.124,
"student": true,
"teacher": false,
],
"image": ["10", "11", "12", "15"],
]
let result1 = SignTool.concatenate(dictionary: dictionary)
// 把这个字典转出josn字符串,然后再转回字典,看看是否和原来的字典一样
let jsonData = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
let jsonString = String(data: jsonData, encoding: .utf8)!
let dict = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
let result2 = SignTool.concatenate(dictionary: dict)
print("jsonString",jsonString)
print("result1",result1)
print("result2",result2)
XCTAssertEqual(result1, result2)
}
func testMax() {
let dictionary: [String: Any] = ["b": [1,
["d": 4, "c": 3]
],
"a": ["e": 5,
"f": [6, 7],
"uid": 8,
"age": 20.124,
"student": true,
"teacher": false,
"dic":["roomID":"123",
"author":"gcs",
]
],
"image": ["10", "11", "12", "15"],
"array":[
[1,2,3],
["1","2","3"],
["roomID":"123","author":"gcs"],
["roomID":"1234","author":"g"],
]
]
let result1 = SignTool.concatenate(dictionary: dictionary)
// 把这个字典转出josn字符串,然后再转回字典,看看是否和原来的字典一样
let jsonData = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
let jsonString = String(data: jsonData, encoding: .utf8)!
let dict = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [String: Any]
let result2 = SignTool.concatenate(dictionary: dict)
print("jsonString",jsonString)
print("result1",result1)
print("result2",result2)
XCTAssertEqual(result1, result2)
}
@discardableResult
func XCTAssertEqual(_ str1: String, _ str2: String) -> Bool {
if str1 == str2 {
print("✅")
return true
}
print("❌ Expected: \(str1) but got: \(str2)")
return false
}
}