最近有在写一些 TypeScript。虽然在我觉得 Swift 的表达能力要比 TypeScript 好太多,常常会陷入不知该怎么用 TypeScript 表达的状况(当然我并不怎么熟 TypeScript),唯独 TypeScript 的 utility type 和 mapped type 一直让我觉得很神奇。
比如说我有一个用来表示配置的类型:
1 2 3 4 5 6
| type Configuration { foregroundColor: string; backgroundColor: string; font: string; ... other_999_properties; }
|
本身这个配置会有它们的默认值,我们也允许外部去修改部分配置。鉴于这个配置有 1002 个配置项,让别人直接构造整个配置,再传递给我们略显麻烦。
一种做法是我们可以提供一个默认配置,别人在此基础上做出改动之后再传递给我们。但 TypeScript 中有一种更有趣的方法,我们可以创建一个类型 Partial<Configuration>
。
1 2 3 4 5 6 7 8 9
| type PartialConfiguration = Partial<Configuration>;
configure({ font: 'Comic Sans' });
|
就能得到了一个与原本拥有相同属性,但所有属性都为可选的类型。
在 Swift 中实现
那么在 Swift 上是否能做到相同的事情呢?仔细一想,说不定可以用 dynamic member lookup 做到。
1 2 3 4 5 6 7 8 9 10 11 12
| @dynamicMemberLookup struct Partial<T> { private var storage = [AnyKeyPath: Any]() subscript<V>(dynamicMember member: WritableKeyPath<T, V>) -> V? { get { storage[member] as? V } set { storage[member] = newValue } } }
var configuration = Partial<Configuration>() configuration.font = "Comic Sans" configure(with: configuration)
|
但是初始化还是有点麻烦。比较简单的方法是增加这样的一个初始化方法来进行初始化:
1 2 3 4 5 6 7
| init(_ configure: (inout Self) -> Void) { var partial = Self() configure(&partial) self = partial }
configure(with: .init { $0.font = "Comic Sans" })
|
这样一来我们就可以手动将 1002 个属性复制到我们的配置里面了(
Object.assign?
在 TypeScript 里,我们可以通过 Object.assign
将两个对象合并,在 Swift 中我们又能怎么做呢?
1 2 3 4
| struct Partial<T> { ... func assign(to object: inout T) { ??? } }
|
这里最大的问题是,我们要怎么找回 storage
里的值的类型,一番尝试之后,我觉得这大概是很难做到的。在脱离 subscript
方法之后,我们就会丢失储存的值的类型,那么我们是否可以在离开 subscript
之前做些什么呢?
我们可以选择不仅仅储存值,而是同时储存一个将值写回对象的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| struct Value<Root> { let value: Any let assignLogic: (inout Root, AnyKeyPath, Any) -> Void func assign(to object: inout Root, keyPath: AnyKeyPath) { assignLogic(&object, keyPath, value) } }
struct Partial<T> { ... subscript<V>(dynamicMember member: WritableKeyPath<T, V>) -> V? { get { (storage[member] as? Value<T>)?.value as? V } set { if let newValue = newValue { storage[member] = Value<T>( value: newValue, assignLogic: { object, keyPath, value in guard let value = value as? V, let keyPath = keyPath as? WritableKeyPath<T, V> else { return } object[keyPath: keyPath] = value } ) } else { storage[member] = nil } } } }
|
那么 assign
方法的实现也就呼之欲出了。
1 2 3 4 5 6 7 8 9 10
| func assign(to object: inout T) { for (keyPath, value) in storage { guard let value = value as? Value<T> else { continue } value.assign(to: &object, keyPath: keyPath) } }
func configure(with options: Partial<Configuration>) { options.assign(to: &self.configuration) }
|
最后
写着写着就会发现,configure
方法这样写不就好了吗。
1 2 3
| func configure(with options: (inout Configuration) -> void) { options(&self.configuration) }
|
但 Partial
也不是一无是处,至少它限制了你只能去读写可以被修改的配置,避免了你去读取只读的属性甚至是调用方法。如果我们再对 Partial
做进一步定制,还可以借助 property wrapper 选择希望导出的属性。
最后我们可以为 wrapped type 为引用类型的情况做一些适配。也可以想想怎么实现 DeepPartial
。
我看了下其他的一些 utility type,除了 Readonly
这种之外,觉得在 Swift 中大概都实现不了。