如何在Swift中轻松扩展现有类而不需要考虑冲突

不知各位同学是否有感觉,类似:RxSwift中的xxx.rx.xxx以及Kingfisher中的image.kf.xxx这种api使用起来就很爽。那么这种类似命名空间的东西是怎么实现的呢?今天一起来扒扒。我们的目标是实现字符串的截取,可以像下面这样调用:

1
2
let string = "Hello, World!"
string.ns.substring(from: 1) // ello, World!

啥也不管,就这样来行不行?

string.ns.substring(from: 1)从表面看,就是调用了String的一个属性,然后得到一个对象,这个对象上面有一个方法substring(from:)。所以,只管梭哈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StringApi {
    /// 由于后续需要操作String,这里通过构造函数将其传递进来
    let string: String
    init(_ string: String) {
        self.string = string
    }

    func substring(from: Int) -> String? {
        let start = string.index(string.startIndex, offsetBy: from)
        let x = string[start..<string.endIndex]
        return String(x)
    }
}

extension String {
    /// 通过ns将支持的Api都返回
    var ns: StringApi {
        return StringApi(self)
    }
}

nice!很简单的嘛。但是,我们要想扩展其他类,怎么办?比如扩展CGSize。是不是要向下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CGSizeApi {
    let size: CGSize
    init(_ size: CGSize) {
        self.size = size
    }
    func rotate() -> CGSize {
        return CGSize(width: size.height, height: size.width)
    }
}

extension CGSize {
    var ns: CGSizeApi {
        return CGSizeApi(self)
    }
}

看,还是很简单嘛!好像,只要我们想扩展某种类型,只需定义api所依赖的对象,然后再通过extension将其返回就可以了。真的是这样吗?让我们试试扩展Array-w799

可以看到当我们需要操作具体类型的时候,就无法搞定了。当然你可能说使用类型约束,可以搞定。但是这种方式还是有太多的弊端:大量的重复代码、不方便扩展,每次新扩展类型,都需要写对应的api包装类、不易维护。

新思路

观察上面的StringApiCGSizeApiArrayApi他们本质上就是对后续想操作的数据的包装。所以我们实现一个包装类:

1
2
3
4
5
6
7
8

class Wrapper<T> {
    let value: T
    init(_ value: T) {
        self.value = value
    }
}

这样我们就可以包装任意值。

下一步就是通过.pns(为了和之前的ns区别)返回这个Wrapper对象了。这次我们选择protocol

1
2
3
4
protocol NamespaceCompatible {
    associatedtype Value
    var pns: Wrapper<Value> { get set }
}

在提供下默认实现:

1
2
3
4
5
6
extension NamespaceCompatible {
    var pns: Wrapper<Self> {
        get { return Wrapper(self) }
        set { }
    }
}

这样,我们就可以很方便的扩展其他类型了。比如String:

1
extension String: NamespaceCompatible {}

再比如CGSize

1
extension CGSize: NamespaceCompatible {}

最后,我们的api要放在何处?又该如何组织?答案还是extension

我们通过.pns返回的是Wrapper,所以这里应该扩展Wrapper。但是需要加上约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// String
extension Wrapper where T == String {
    func substring(from: Int) -> String? {
        let start = value.index(value.startIndex, offsetBy: from)
        let x = value[start..<value.endIndex]
        return String(x)
    }
}
/// CGSize
extension Wrapper where T == CGSize {
    func rotate() -> CGSize {
        return CGSize(width: value.height, height: value.width)
    }
}
/// Sequence
extension Wrapper where T: Sequence, T.Element == String {
    func concat() -> String {
        return value.reduce("") { result, element in
            return result + element
        }
    }
}

好了,这样就可以快乐的玩耍了。下次我们再扩展其他类型时,只需:

  1. extension Type: NamespaceCompatible {}
  2. extension Wrapper where T == Type 或者 extension Wrapper where T: Type

RxSwift & Kingfisher

最后我们一起来看看第三方库的实现方式,是否和我们的思路一样:

RxSwift

-w902

整个实现都在Reactive.swift文件中:

  1. public struct Reactive<Base> {}相当于我们这里的class Wrapper<T> {}。选择struct而不是class是一个优化的点。额外的subscriptRx的内容了,这里不再分析。
  2. ReactiveCompatible相当于我们这里的NamespaceCompatible。这里多了通过类型访问.rx的支持。可以按需加入。

Kingfisher

该框架的实现在Kingfisher.swift文件中: -w598

  1. KingfisherWrapper跟我们的Wrapper相同
  2. 我们的NamespaceCompatible协议在这里被分成KingfisherCompatibleKingfisherCompatibleValue。前者专门负责引用类型,后者负责值类型。不知这样区分有何考究?
  3. 这里的KingfisherCompatibleKingfisherCompatibleValue都是空协议。这里将Wrapper的泛型规定成了遵循协议的那个类型。 而R小Swift中在协议中明确规定了ReactiveBase,更加灵活。

我还是不理解。怎么办?

在如何实现这里,需要一定的抽象能力。需要多看多思考多动手。

还有一部分可能是因为 Swift 的语法问题:

  1. Self & self的使用方法。 参考这里
  2. Swift的协议约束不熟悉。参考这里

好了,再也不担心我的扩展和别人的扩展冲突了!