Swift中关于泛型的一些注意点

1,882

阅读本文需要对 Swift 基本语法和泛型有基本的了解。
本文约840字,阅读大概需要 10 min。
收获:对 Swift 中的泛型使用更加深入。

尽量使用泛型来替代 Any

假设,在项目中我们需要实现一个 Stack 的数据结构,并且想支持任意基本类型。那么,我们可以使用 Any 实现下面的代码:

struct Stack {
    var data = [Any]()
    
    init(data: [Any]) {
        self.data = data
    }
    
    mutating func pop() {
        data.removeLast()
    }
    
    mutating func push(element: Any) {
        data.append(element)
    }
}

var stack = Stack(data: ["swift", "objective-c"])
stack.push(element: "iOS")

上述代码可以满足我们的需求,它可以支持任意类型。但是这种写法在使用中还是有很多缺点的:

  • 使用元素时需要解包、转类型
print(stack.data.first as! String)
  • 因为是 Any,所以可以赋值任意类型,导致代码可读性和健壮性不高
stack.push(element: 13)
//Error - Could not cast value of type 'Swift.Int' (0x10b4d7070) to 'Swift.String' (0x10b4d9190)
print(stack.data.last as! String) 

所以,遇到此类需求时,我们应该使用泛型来代替上面的 Any。

struct Stack<Element> {
    var data = [Element]()
    
    init(data: [Element]) {
        self.data = data
    }
    
    mutating func pop() {
        data.removeLast()
    }
    
    mutating func push(element: Element) {
        data.append(element)
    }
}

var stack = Stack(data: ["swift", "objective-c"])
stack.push(element: "iOS")
  • 只需解包无须转变类型
print(stack.data.first!)
  • 使用时已确定类型,赋值别的类型直接报错,从而提高代码的健壮性
// 编译不成功 - Cannot convert value of type 'Int' to expected argument type 'String'
stack.push(element: 13)

通过 泛型 和 Extension 实现通用算法

接着上面的例子,假如我们想在 Stack 的基础上,添加一个计算栈内所有数字类型元素的和,我们可以用以下代码实现:

extension Stack where Element: Numeric {
    func sum() -> Element {
        var total: Element = 0
        
        for num in data {
            total += num
        }
        return total
    }
}

var numStack = Stack(data: [1, 2])
numStack.sum() // 3
// strStack 是无法调用 sum 函数的,因为String 并没有遵守 Numberic 协议
var strStack = Stack(data: ["Swift", "iOS13"])

extension Stack where Element: Numeric 的意思是指:只有 Stack 的元素遵守 Numberic 协议,才能调用 sum() 函数。这么做是因为非数字类型是无法进行累加操作的。如果我们不添加该限制的话,total += num 这句代码会报错:Binary operator '+=' cannot be applied to two 'Element' operands

通过泛型 和 Extension 为 Protocol 扩展方法

map(系统标准库)

我们可以通过 泛型 + Extension 为 Protocol 扩展兼容多类型的方法,比如标准库提供的 map 函数,它的实现就是借助 泛型 + Extension 实现的:

extension Collection {
    func map<T>(_ transform: (Element) -> T) -> [T] {
        var result = [T]()
        
        result.reserveCapacity(self.count)
        var position = startIndex
        while position != endIndex {
            result.append(transform(self[position]))
            
            position = index(after: position)
        }
        return result
    }
}

因为在调用 map() 函数时,我们可以确定参数计算出 result 的大小,所以我们可以使用 result.reserveCapacity(self.count) 来优化代码效率。关于 reserveCapacity 更加详细的内容,请参见 Swift 中的 Array 性能比较: append vs reserveCapacity(译)

shuffle(系统标准库)

Fisher–Yates shuffle 算法是一个用来将一个有限集合生成一个随机排列的算法(数组随机排序)。

extension RandomAccessCollection where Self: MutableCollection {
    mutating func shuffle() {
        let n = count
        guard n > 1 else { return }
        
        for (i, pos) in indices.dropLast().enumerated() {
            let otherPos = index(startIndex, offsetBy: Int.random(in: i..<n))
            swapAt(pos, otherPos)
        }
    }
}

var arr = [1,2,3,4,5,6,7]
arr.shuffle()
//[4, 3, 5, 1, 6, 7, 2]
print(arr)

参考