(WeChat 公開アカウント) [ https://mp.weixin.qq.com/s/BuRr6EobhmgAu0XRLu9EgA ]
変革のプロセス中、Swift の効率、安全性、利便性、およびいくつかの優れた機能はチームに深い印象を残しました。開発者が ObjC を作成するときにあまり考慮しない機能がたくさんあります。たとえば、Swift の静的ディスパッチ メソッド、値型の使用、静的ポリモーフィズム、エラー + スロー、カリー化と関数合成、豊富な高階関数など、OOP と比較して、Swift はプロトコル指向プログラミング、ジェネリック プログラミングもより適切にサポートできます。より抽象的な関数型プログラミングは、ObjC 時代に開発者が直面する多くの問題点を解決します。
Swift と ObjC の類似点と相違点を組み合わせて、Swift の利点を出発点として、プロジェクトの関数コードを再検討して最適化しました。
一部のメソッドの動的ディスパッチを静的ディスパッチに置き換えます。
Swift が ObjC よりも高速に動作する理由の 1 つは、静的ディスパッチ (値型) と関数テーブル ディスパッチ (参照型) というディスパッチ方法にあります。静的ディスパッチ ARM アーキテクチャを使用すると、bl 命令を直接使用して対応する関数アドレスにジャンプできます。これは最も効率的な呼び出しであり、コンパイラのインライン最適化に役立ちます。値の型は親クラスから継承できません。型はコンパイル時に決定され、静的ディスパッチの条件を満たすことができます。参照型の場合、さまざまなコンパイラ設定もディスパッチ方法に影響します。たとえば、WMO のフル モジュール コンパイルでは、システムはサブクラスに継承されないクラスを変更するために暗黙的な Final などのキーワードを自動的に入力し、可能な限り静的なディスパッチを使用します。
私たちのプロジェクトでは、Class を使用するすべてのクラスに対して全体的なチェックが行われました。必要な場合を除き、NSObject から継承し、NSObject サブクラスの使用は控えめにしてください。継承やポリモーフィズムを考慮する必要がないシナリオでは、final や private などのキーワード変更をできる限り使用してください。
もう 1 つ注意すべき点は、ObjC ではメソッドの静的ディスパッチも導入されていることです。Xcode12 に統合された最新の LLVM は、メソッドで __attribute__((objc_direct)) を指定することで、元の動的メッセージ ディスパッチを静的ディスパッチに変更することで、すでに ObjC をサポートしています。
すべてのクラスを確認し、可能であれば構造体または列挙型に置き換えます。
Swift の構造体と列挙型は値型であり、Class は参照型です。Swift で値型と参照型のどちらを使用するかは、開発者が検討して評価する必要があります。
私たちが開発した JD Logistics ウィジェットと SwiftUI に基づいて開発された macOS アプリケーションでは、現在、より多くの構造と列挙を使用しています。まず、値の型と参照型、値の型 (Struct Enum など) の違いを比較します。
-
スタック上に作成されるため、作成速度が速い
-
メモリ使用量が小さい。占有されるメモリ全体は、アライメント後の内部属性メモリのサイズになります。
-
メモリのリサイクルは高速で、スタック フレームを使用してスタックのプッシュとポップを制御するだけで、ヒープ メモリの処理によるオーバーヘッドはありません。
-
参照カウントは必要ありません (構造内の属性として参照型を使用する場合を除く)。
-
一般に、静的にディスパッチされ、高速に実行され、インライン化などのコンパイラの最適化にも便利です。
-
課題中のディープコピー。システムは、コピーオンライトを使用して、不必要なコピーを回避し、コピーのオーバーヘッドを削減します。
-
暗黙的なデータ共有なし、独立した不変性
-
構造内のプロパティは、突然変異によって変更できます。このようにして、値の型の独立性を確保しながら、一部の属性の変更もサポートできます。
-
スレッド セーフ。一般に、競合状態やデッドロックはありません (値は各サブスレッドでコピーする必要があることに注意してください)。
-
OOP サブクラスが親クラスに結合しすぎる問題を回避するために、継承はサポートされていません。
-
抽象化はプロトコルとジェネリックを通じて実現できます。ただし、プロトコルを実装する構造はメモリ サイズが異なるため、配列に直接配置することはできません。ストレージの一貫性を確保するために、システムはパラメータを渡したり値を割り当てるときに中間層の存在コンテナを導入します。ここに多くの構造属性がある場合、少し複雑になりますが、Apple も最適化 (コピーオンライトによる間接ストレージ) し、オーバーヘッドを削減します。一般に、値型の多態性にはコストがかかり、システムはそれを最適化するために最善を尽くします。開発者が考慮すべきことは、動的なポリモーフィズムを減らし、プロトコルをクラスとして直接使用することであり、より静的なポリモーフィズムを考慮し、それを一般的な制約と組み合わせて使用する必要があります。
参照型 (クラス関数クロージャなど):
-
参照型は値型ほどメモリ使用効率が高くなく、ヒープ上に作成されるため、この領域を指すスタック ポインタが必要となり、ヒープ メモリの割り当てとリサイクルのオーバーヘッドが増加します。
-
代入はほとんどコストを消費せず、一般にポインタの浅いコピーです。ただし、参照カウントのコストが発生します
-
複数のポインタが同じメモリを指す可能性があり、独立性が低く、誤動作が起こりやすくなります。
-
非スレッドセーフ、アトミック性を考慮する必要がある、マルチスレッドにはスレッドロックの連携が必要
-
メモリ解放を制御するには参照カウントが必要ですが、不適切に使用すると、ワイルド ポインタ、メモリ リーク、循環参照のリスクが生じる可能性があります。
-
継承は許可されていますが、継承の副作用として、サブクラスと親クラスの間の密結合が発生します。たとえば、システムの UIStackView の主な目的はレイアウトの使用のみですが、UIView のすべてのプロパティとメソッドを継承する必要があります。
Swift は、サブクラスと親クラス間の密結合、オブジェクトの暗黙的なデータ共有、非スレッド安全性、参照カウントなど、ObjC 時代の OOP の典型的な問題点を解決するために、より強力な値型を提供していることがわかります。 。Swift の標準ライブラリを見ると、主に値型で構成されていることがわかりますが、Int、Double、Float、String、Array、Dictionary、Set、Tuple などの基本的な型のコレクションも構造体です。もちろん、値型には多くの利点がありますが、クラスを完全に放棄する必要があるという意味ではなく、実際の状況に基づいて分析する必要があります。 OOPをまったく使用しないでください。
構造メモリを最適化する
C 言語構造を使用する場合と同様に、Swift 構造のサイズは内部プロパティ メモリ アラインメントのサイズです。構造内でプロパティを異なる順序で配置すると、最終的なメモリ サイズに影響します。システムが提供する MemoryLayout を使用して、対応する構造体が占有するメモリ サイズを確認できます。
Int32 が完全に満たされるシナリオでは Int を使用する必要がないこと、Bool を使用する必要があるシナリオでは Bool の代わりに String または Int を使用しないこと、メモリの少ない属性は後ろに配置する必要があることなど、いくつかの詳細を確認しました。できるだけ、など。
struct GameBoard {
var p1Score: Int32
var p2Score: Int32
var gameOver: Bool
}
struct GameBoard2 {
var p1Score: Int32
var gameOver: Bool
var p2Score: Int32
}
//基于CPU寻址效率考虑,GameBoard2字节对齐后占用空间更多
MemoryLayout<GameBoard>.self.size //4 + 4 + 1 = 9(bytes)
MemoryLayout<GameBoard2>.self.size //4 + 4 + 4 = 12(bytes)
動的ポリモーフィズムの代わりに静的ポリモーフィズムを使用する
上で値の型について言及したとき、静的多態性について言及しましたが、静的多態性とは、コンパイラがコンパイル時に型を決定できる多態性を指します。このようにして、コンパイラは型をダウングレードし、コンパイル時に特定の型のメソッドを生成できます。
特定のプロトコルの制約に準拠するジェネリックスを定義すると、パラメーターを渡すクラスとしてプロトコルを直接使用することを回避できます。そうしないと、コンパイラーがエラーを報告します。これはポリモーフィズムをサポートするインターフェースと同等ですが、特定の型で呼び出す必要があります。これにより、静的多態性の目的が達成されます。静的ポリモーフィズムの場合、コンパイラはその静的特性を最大限に活用して最適化を行い、同時に WMO モジュール全体の最適化が設定されている場合は、コードの増大の可能性を制御しようとします。
つまり、開発者は静的ポリモーフィズムを可能な限り考慮する必要があります。たとえば、関数のパラメータとしてプロトコルを使用する場合、ジェネリックスを導入できます。WWDC には非常に古典的な議論があります。
プロトコル ドローアブル {
func draw()
}
struct Line: Drawable {
var x: Double = 0
func draw() {
}
}
func drawACopy<T: Drawable>(local: T) {//指定T必须遵守Drawable
local.draw()
}
let line = Line()
drawACopy(local: line)//Success (传入具体的实现了Drawable的结构体,编译器可推断其类型)
let line2: Drawable = Line()
drawACopy(local: line2)//Error,编译器不允许直接使用Drawable协议作为入参
プロトコル指向はプロトコルの拡張デフォルト実装を提供します
親クラスから継承し、プロトコルに準拠するクラスの場合、Swift は後者を優先します。ObjC の OOP の形式では、基本的に Structs/Enums + プロトコル + プロトコル拡張 + ジェネリックスを使用して、Swift で論理抽象化を実現できます。
プロジェクトでは OOP の使用を最小限に抑え、可能な限りプロトコルとジェネリックの値型のみを使用しました。これにより、コンパイラーはより静的な最適化を実行し、OOP スーパー クラスによって引き起こされる密結合を軽減できます。
同時に、プロトコル拡張はプロトコルのデフォルト実装を提供できます。これは、ObjC プロトコルとは異なる非常に重要な最適化でもあります。
これを使用する場合は、型推論によって取得したプロトコルを使用するのではなく、プロトコル拡張内のメソッドを呼び出すために特定の型を使用する必要があることに注意してください。プロトコルを使用して呼び出す場合、メソッドがプロトコルで定義されていない場合は、対応するメソッドが特定の型で実装されている場合でも、プロトコル拡張のデフォルト実装が呼び出されます。現時点ではコンパイラはデフォルトの実装しか見つけることができないためです。
エラー処理を最適化する
ObjC と比較して、Swift はエラーとスローをより完全に処理します。この明らかな利点は、API がより使いやすく、可読性が向上し、エディター検出を使用してエラーの可能性が低減されることです。ObjC 時代には、例外をスローするという操作が考慮されていないことが多いため、ObjC コーディングに慣れているプログラマは、基盤となる API をカプセル化するときに注意する必要があります。Error プロトコルを継承する Enum を使用するのが一般的です。
enum CustomError: Error {
case error1
case error2
}
エラーが生成された後、外部処理のためにエラーをスローすることもできます。 throw メソッドがサポートされると、コンパイラーはスローが処理されたかどうかをより適切に検出できるようになります。() throws -> Void と () -> Void は異なる関数タイプであることに注意してください。
//(Int)->Void可以赋值给(Int)throws->Void
let a: (Int) throws -> Void = { n in
}
//反之类型不匹配 编译报错
let b: (Int) -> Void = { n throws in
}
rethrows: 関数の入力パラメータが throw をサポートする関数である場合、rethrows を使用して、その関数がエラーをスローすることもできることを識別できます。このようにして、この関数を使用すると、コンパイラは try-catch が必要かどうかを検出します。
これは、基本関数をカプセル化するときに考慮する必要があることです。システム内のマップ関数の定義など、システムにはわかりやすい例が多数あります。
public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
let a = [1, 2, 3]
enum CustomError: Error {
case error1
case error2
}
do {
let _ = try a.map { n -> Int in
guard n >= 0 else {
//如果map接受的closure内部有抛出throw,编译器会强制检测外部是否有try-catch
throw CustomError.error1
}
return n * n
}
} catch CustomError.error1 {
} catch {
}
ネスティングの場合はガードを使用して削減します
Guard は重大度の検出に使用でき、可読性を高め、ネスティングの過剰を軽減できるという利点があります。ガードを使用する場合、一般的にelseはリターン、スロー、コンティニュー、ブレイクなどになります。
//if嵌套过多,难以阅读,增加后期维护成本
if (xxx){
if (xxx) {
if (xxx) {
}
}
}
//使用Guard,整体更清晰,便于后期维护
let dict : Dictionary = ["key": "0"]
guard let value1 = dict["key"], value == "0" else {
return
}
guard let value2 = dict["key2"], value == "0" else {
return
}
print("\(value1) \(value2)")
延期を使用する
defer によって変更されたクロージャは、現在のスコープが終了するときに呼び出されます。主に、返される前に実行する必要があるコードを繰り返し追加することを避け、読みやすさを向上させるために使用されます。
たとえば、macOS アプリケーションでは、ファイルの読み取りと書き込みの操作がありますが、このとき defer を使用すると、ファイルを閉じ忘れることがなくなります。
func write() throws {
//...
guard let file = FileHandle(forUpdatingAtPath: filepath) else {
throw WriteError.notFound
}
defer {
try? file.close()
}
//...
}
もう 1 つの一般的なシナリオは、ロックや非エスケープ クロージャ コールバックなどを解放するときです。
ただし、defer を使いすぎないでください。使用するときは、クロージャ キャプチャ変数とスコープの問題に注意してください。
たとえば、if ステートメントで defer を使用すると、if が終了したときに defer が実行されます。
すべての必須のアンラップをオプションのバインディングに置き換えます。
オプションの値の場合、強制アンパックは可能な限り、あるいは完全に回避する必要があります。! を使用しなければならない状況に遭遇した場合、ほとんどの場合、元の設計に無理がある可能性があります。downCasting を含める場合、型変換自体が失敗する可能性があるため、as! の使用は避けて as? を使用するようにしてください。もちろん、try! も避けてください。
オプションの値の場合は、続行する前に常にオプションのバインディング検出を使用して、オプションの変数が実際の値であることを確認してください。
var optString: String?
if let _ = optString {
}
遅延読み込みをさらに検討する
プロジェクト内の作成する必要のないプロパティを遅延読み込みに変更します。Swift の遅延読み込みは、ObjC よりも読みやすく実装が簡単なので、遅延変更を使用するだけです。
lazy var aLabel: UILabel = {
let label = UILabel()
return label
}()
関数型プログラミングを使用して状態変数の宣言とメンテナンスを削減する
クラス内で宣言する状態変数が多すぎると、後のメンテナンスに役立ちません。Swift の関数は、関数パラメーター、戻り値、変数として使用でき、関数プログラミングを適切にサポートできます。関数式を使用すると、グローバル変数または状態変数を効果的に削減できます。
命令型プログラミングでは、問題を解決するためのステップに重点が置かれます。機械語命令シーケンスを直接反映します。変数(記憶装置に相当)、代入文(取得・記憶命令に相当)、式(命令の算術計算に相当)、制御文(ジャンプ命令に相当)があります。
関数型プログラミングでは、データのマッピング関係とデータの流れ、つまり入力と出力にさらに注意が払われます。関数は変数として扱われ、他の関数への引数 (入力値) として使用したり、関数から返す (出力値) ことができます。計算を式の評価として説明します。x が与えられた場合、独立変数 f(x)->y のマッピングは y に安定してマッピングされます。関数内の関数スコープ外の変数にはアクセスしないようにし、状態変数の宣言と保守を減らすために入力パラメーターのみに依存するようにしてください。同時に、変更可能性の低い変数 (オブジェクト) を使用し、不変可能性の高い変数 (構造体) を使用します。こうすることで、他の副作用による干渉はなくなります。
カリー化を使用して、複数のパラメータを受け入れる関数を 1 つのパラメータを受け入れる関数に変換し、一部のパラメータを関数内にキャッシュします。同時に、関数合成を使用して可読性を高めます。たとえば、加算と乗算の計算を行う場合、加算と乗算の関数をカプセル化し、それらを 1 つずつ呼び出すことができます。
func add(_ a: Int, _ b: Int) -> Int { a + b }
func multiple(_ a: Int, _ b: Int) -> Int { a * b }
let n = 3
multiple(add(n, 7), 6) //(n + 7) * 6 = 60
次の関数も使用できます。
//柯里化add和multiple函数: 由两个入参改为一个并返回一个(Int)->Int类型函数
func add(_ a: Int) -> (Int) -> Int { { $0 + a} }
func multiple(_ a: Int) -> (Int) -> Int { { $0 * a} }
//函数合成 自定义中置运算符 > 增加可读性
infix operator > : AdditionPrecedence
func >(_ f1: @escaping (Int)->Int,
_ f2: @escaping (Int)->Int) -> (Int) -> Int {
{f2(f1($0))}
}
//生成新的函数 newFn
let n = 3
let newFn = add(7) > multiple(6) // (Int)->Int
print( newFn(n) ) //(n + 7) * 6 = 60
multiple(add(n, 7), 6) を使用して newFn = add(7) > multiple(6), newFn(n) とすると、特により複雑なシナリオで、全体的な状況がより明確になることがわかります。メリットがより明らかになるでしょう。
要約する
Swift は、Swift を簡単に始めることができる、シンプルな糖衣構文と強力な型推論を豊富に提供します。ただし、パフォーマンスの考慮やより完璧な API の設計の観点からは、さらに練習が必要です。発注チームは、iOS ウィジェット、AppClips、JD ワークステーション (macOS デスクトップ アプリケーション) などのシナリオで可能な限り Swift および SwiftUI 開発を使用するよう努めており、開発効率とプロジェクトの安定性は良好です。現在、JD グループ内の Swift のインフラストラクチャは徐々に改善されており、将来的にはグループ内のより多くの学生が Swift の開発に参加することを期待しています。