Python實戰社羣
Java實戰社羣
長按識別下方二維碼,按需求添加
掃碼關注添加客服
進Python社羣▲
掃碼關注添加客服
進Java社羣▲
作者:zvving,iOS 開發者,現就職於字節跳動音樂團隊
Sessions: https://developer.apple.com/videos/play/wwdc2019/415/
本文發表於 2019/06/13 《WWDC19 內參》
前言
Swift 是一門簡潔同時富有表現力的語言,這其中隱藏着衆多的設計細節。
本文通過提出一個 Struct 的語義問題,在尋找答案的過程中逐步介紹這些概念:
DynamicMemberLookup 應用
PropertyWrapper 的實現原理
SwiftUI DSL 中 PropertyWrapper 的應用
來一起看看更現代的 API 背後的設計過程。
WWDC19 部分 sessions 和示例代碼中的 PropertyDelegate 即是 PropertyWrapper,後續會統一命名爲 PropertyWrapper
Clarity at the point of use
最大化使用者的清晰度是 API 設計的第一要義
Swift 的模塊系統可以消歧義
Swift-only Framework 命名不再有前綴
C & Objective-C 符號都是全局的
提醒:
每一個源文件都會將所有 import 模塊彙總到同一個命名空間下。你依舊應該謹慎的對待命名,以確保同一命名在複雜上下文依舊有清晰的語義。
選擇 Struct 還是 Class?
相很多類似問題一樣,你需要重新思考二者的語義。
默認情況下,你應該優先選擇 Struct。除非你必須要用到 Class 的特性。
比如這些需要使用 Class 的場景:
需要引用計數或者關心析構過程
數據需要集中管理或共享
比較操作很重要,有類似 ID 的獨立概念
很多文章有過討論,這裏不作過多介紹,下面我們看看實際問題。
Struct 中嵌套 Class 的拷貝問題
無論是基於歷史問題還是要對不同類型數據組合使用,常常碰到 Struct 和 Class 組合嵌套的情況。
Class 中存在 Struct,這種情況再正常不過,使用時也不會帶來什麼問題,不必討論
Struct 中存在 Class,這種情況破壞了 Struct 的語義,運行時拷貝也可能帶來不符合預期的情況,下面重點討論這個問題。
定義如代碼所示,Struct Material 有一個成員屬性 texture 是 Class 類型:
struct Material {
public var roughness: Float
public var color: Color
public var texture: Texture
}
class Texture {
var isSparkly: Bool
}
當 Material 實例發生拷貝時,會發生什麼?
很顯然,兩個 Material 實例持有同一個的 texture,所有 texture 引用所做的任何修改都會對兩個 Struct 產生影響,這破壞了 Struct 本身的語義。
今天我們重點看看如何解決這個問題。
一個思路:把 texture 設爲不可變類型?
如圖所示,並沒有什麼作用。
texture 對象的屬性依舊可以被修改,一個標記 immutable 的實例屬性還能被修改,這會帶來更多困擾。
另一個思路:修改時拷貝
struct Material {
private var _texture: Texture
public var texture {
get { _texture }
set { _texture = Texture(copying: newValue) }
}
}
隱藏存儲屬性,開放計算屬性。在計算屬性被賦值時進行拷貝。
針對修改 Material 實例的 texture 屬性這一場景,的確會生成單獨的拷貝。然而除此之外,有太多的問題。
texture 實例的內部屬性,依舊可能被意外修改
Material 發生寫時拷貝時,被拷貝的存儲屬性 _texture 依舊是同一個
再一個思路:模仿 Copy On Write
既然我們連 Texture 的內部屬性都要控制,開放 texture 訪問帶來太多問題,索性完全禁用 texture 的外部訪問,把 texture 的屬性(如 isSparkly)提升到 Material 屬性層級,在訪問 isSparkly 時,確保 _texture 引用唯一。
struct Material {
private var _texture: Texture
public var isSparkly: Bool {
get {
if !isKnownUniquelyReferenced(&_texture) { // 確保 _texture 引用計數爲 1
_texture = Texture(copying: _texture)
}
return _texture.isSparkly
}
set {
_texture.isSparkly = newValue
}
}
}
這樣的確完整實現了 Struct Material 語義。哪怕 Material 寫時拷貝有多個 _texture 引用,在訪問 isSparkly 屬性時也會發生拷貝,確保每個 Material 實例的 _texture 屬性唯一。
唯一(而且是很重要)的問題是如果 Class Texture 屬性很多,會引入大量相似代碼。『可行』不代表『可用』。
沒關係,我們再試試引入 DynamicMemberLookup。
初試 DynamicMemberLookup
DynamicMemberLookup 具體概念可以參考卓同學的這篇文章:細說 Swift 4.2 新特性:Dynamic Member Lookup[1]
DynamicMemberLookup 是 Swift4.2 引入的新特性,使用在什麼場景一度讓人困惑。這裏恰好能解決我們的問題。先上代碼:
@dynamicMemberLookup
struct Material {
public var roughness: Float
public var color: Color
private var _texture: Texture
public subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<Texture, T>) -> T {
get { _texture[keyPath: keyPath] }
set {
if !isKnownUniquelyReferenced(&_texture) { _texture = Texture(copying: _texture) }
_texture[keyPath: keyPath] = newValue
}
}
}
實現思路與之前的代碼完全一致,只是引入 dynamicMemberLookup 動態提供對 Texture 的屬性訪問,這樣無論 Class Texture 有多少屬性,幾行代碼輕鬆支持。
需要留意的是 Xcode 11 完全支持 dynamicMemberLookup,代碼提示也毫無壓力
至此,似乎『完美解決』了Struct 中嵌套 Class 的拷貝問題。
此處賣個關子,後面還有更簡潔的實現。先來看看 PropertyWrapper。
PropertyWrapper
Swift Evolution: SE-0258[2]
實際項目中有些屬性的初始化性能開銷較大,我們常常會用到懶加載:
public lazy var image: UIImage = loadDefaultImage()
如果不用 lazy
關鍵字,我們也可以這樣實現:
public struct MyType {
private var imageStorage: UIImage? = nil
public var image: UIImage {
mutating get {
if imageStorage == nil {
imageStorage = loadDefaultImage()
}
return imageStorage!
}
set { imageStorage = newValue }
}
}
基於同樣的思路,也會有另一些場景,比如需要的是延遲外部賦值,期望未賦值調用時拋出錯誤:
public struct MyType {
var textStorage: String? = nil
public var text: String {
get {
guard let value = textStorage else {
fatalError("text has not yet been set!")
}
return value
}
set { textStorage = newValue }
}
}
看起來不錯。支持延遲外部賦值又有檢查機制。唯一(而且是很重要)的問題是實現太臃腫。每個有同樣邏輯的屬性都需要大段重複代碼。
還記得我們說過的:保持使用者的清晰度是 API 設計的第一要義
我們更傾向於使用者看到這樣的代碼:
@LateInitialized public var text: String
非常棒!定義本身清晰說明語義。更棒的是,這裏的屬性註解,完全支持自定義。
PropertyWrapper 顧名思義:屬性包裝器,沒錯,從 Swift5.1 開始,屬性用這種方式支持自定義註解。
我們看看如何實現:
實現 @LateInitialized
註解,我們需要定義一個打上@propertyWrapper
註解的 struct LateInitialized<Value>
????,代碼如下:
// Implementing a Property Wrapper
@propertyWrapper
public struct LateInitialized<Value> {
private var storage: Value?
public var value: Value {
get {
guard let value = storage else {
fatalError("value has not yet been set!")
}
return value
}
set { storage = newValue }
}
}
實現原理也不復雜:
用 @LateInitialized
修飾屬性定義時,如:
@LateInitialized public var text: String
編譯器會把屬性代碼展開,生成如下代碼:
// Compiler-synthesized code…
var $text: LateInitialized<String> = LateInitialized<String>()
public var text: String {
get { $text.value }
set { $text.value = newValue }
}
二者完全等價。
你可以把 $text
看成 wrappedText
,又一次爲了代碼更清晰,蘋果把 $
專用在屬性註解場景,表達 wrapped 語義。
除此之外,PropertyWrapper 還支持自定義構造器:
@UserDefault(key: "BOOSTER_IGNITED", defaultValue: false)
static var isBoosterIgnited: Bool
@ThreadSpecific
var localPool: MemoryPool
@Option(shorthand: "m", documentation: "Minimum value", defaultValue: 0) // 命令行參數
var minimum: Int
還記得前面 Struct Material 中嵌套 Class 的拷貝問題 的例子嗎?
@CopyOnWrite:用 PropertyWrapper 帶來的思路
通過自定義 @CopyOnWrite
註解,我們可以更優雅的解決這個問題:
@propertyWrapper
struct CopyOnWrite<Value: Copyable> {
init(initialValue: Value) {
store = initialValue
}
private var store: Value
var value: Value {
mutating get {
if !isKnownUniquelyReferenced(&store) {
store = store.copy()
}
return store
}
set { store = newValue }
}
}
struct Material {
public var roughness: Float
public var color: Color
@CopyOnWrite public var texture: Texture
}
extension Texture: Copyable { ... }
// Copyable 具體實現略
代碼不必過多解釋,相信大家都能看懂。
PropertyWrapper 在 SwiftUI DSL 中的應用
SwiftUI 是 WWDC19 的最大亮點,來看一個典型的 View 聲明:
struct Topic {
var title: String = "Hello World"
var content: String = "Hello World"
}
struct TopicViewer: View {
@State private var isEditing = false
@Binding var topic: Topic
var body: some View {
VStack {
Text("Title: #\(topic.title)")
if isEditing {
TextField($topic.content)
}
}
}
}
@State
, @Binding
, $topic.title
?是不是似曾相識?
這些屬性都是基於 PropertyWrapper 來實現的(或者說再加上 dynamicMemberLookup)。
這裏以 @Binding
的大致實現爲例:
@propertyWrapper @dynamicMemberLookup
public struct Binding<Value> {
public var value: Value {
get { ... }
nonmutating set { ... }
}
public subscript<Property>(dynamicMember keyPath: WritableKeyPath<Value, Property>) {
...
}
}
屬性定義展開過程如下:
@Binding var topic: Topic
// 等價於
var $topic: Binding<Topic> = Binding<Topic>()
public var topic: Topic {
get { $topic.value }
set { $topic.value = newValue }
}
再來看看使用時的區別:
topic // Topic instance
topic.title // String instance
$topic // Binding<Topic> instance
$topic.title // Binding<String> instance
$topic[dynamicMember: \Topic.title] // Binding<String> instance
留意最後幾行實例對應的類型:
看到
$
不要意外,這是取屬性註解類型的實例,會想剛提到的代碼展開$topic
語義就是wrappedTopic
Struct Binding 實現了 dynamicMemberLookup,
$topic.title
可以正常調用,並且與$topic[dynamicMember: \Topic.title]
完全等價屬性註解是 Struct,對應方法,屬性,以及其它協議都可以支持,這裏有很多的可能性還有待挖掘
我迫不及待把 PropertyWrapper 用在我們項目中,至少簡化幾百行屬性相關的模板代碼,更關鍵的是,這會帶來能清晰的屬性定義。
如何使用協議和泛型,讓代碼更少困擾
這裏用新推出的向量數據 SIMD[3] 做示例,通用性不是很強,這裏不贅述。大體思想是:
不要無腦的從協議開始 Coding
從實際的使用場景開始分析問題,從嘗試合併重複代碼開始下手
優先嚐試組合已有的協議,新協議會有新的理解成本
更多的在協議中使用泛型,解決通用問題
總結
討論了這麼多,還記得最前面提到的嗎:保持使用者的清晰度是 API 設計的第一要義! 這個 session 討論的問題和新概念無不圍繞着這一目標:
DynamicMemberLookup 簡化動態成員屬性調用
PropertyWrapper 讓屬性可以自定義註解,統一屬性模板代碼並且提供文檔化的書寫方式
設計
$value
表達wrappedValue
語義
簡潔的背後往往蘊涵着複雜的探索和巧妙的設計過程。
這個 session 更側重介紹 Swift 語言細節的的設計理念,希望這些理念能幫助你用 Swift 在項目中設計出更現代、清晰度更高的 API。
參考資料
[1]
細說 Swift 4.2 新特性:Dynamic Member Lookup: https://juejin.im/post/5b24c9896fb9a00e69608a71
[2]Swift Evolution: SE-0258: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-delegates.md
[3]SIMD: https://developer.apple.com/documentation/swift/simd
程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣
近期精彩內容推薦:
在看點這裏好文分享給更多人↓↓