ほとんどのSwift開発者が犯していることを知らない間違い
公開: 2022-03-11Objective-Cのバックグラウンドから来て、最初は、Swiftが私を抑制しているように感じました。 Swiftは、強いタイプの性質のために私が進歩することを許可していませんでした。
Objective-Cとは異なり、Swiftはコンパイル時に多くの要件を適用します。 id
タイプや暗黙の変換など、Objective-Cで緩和されるものは、Swiftではありません。 Int
とDouble
があり、それらを合計したい場合でも、それらを明示的に単一の型に変換する必要があります。
また、オプションは言語の基本的な部分であり、単純な概念ですが、慣れるまでに時間がかかります。
最初は、すべてを強制的にアンラップすることをお勧めしますが、それは最終的にクラッシュにつながります。 言語に精通するにつれて、コンパイル時に多くの間違いが検出されるため、ランタイムエラーがほとんど発生しないことが好きになります。
ほとんどのSwiftプログラマーは、Objective-Cの経験が豊富で、特に、他の言語で慣れ親しんでいるのと同じ方法でSwiftコードを記述できる可能性があります。 そして、それはいくつかの悪い間違いを引き起こす可能性があります。
この記事では、Swift開発者が犯す最も一般的な間違いとそれを回避する方法について概説します。
1.オプションの強制アンラップ
オプション型の変数(たとえば、 String?
)は、値を保持する場合と保持しない場合があります。 それらが値を保持しない場合、それらはnil
に等しくなります。 オプションの値を取得するには、最初にそれらをアンラップする必要があります。これは、2つの異なる方法で行うことができます。
1つの方法はif let
またはguard let
を使用したオプションのバインディングです。
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
2つ目は、 !
を使用して強制的にアンラップすることです。 演算子、または暗黙的にアンラップされたオプション型( String!
など)を使用します。 オプションがnil
の場合、アンラップを強制するとランタイムエラーが発生し、アプリケーションが終了します。 さらに、暗黙的にラップされていないオプションの値にアクセスしようとすると、同じことが発生します。
class / struct初期化子で初期化できない(または初期化したくない)変数がある場合があります。 したがって、それらをオプションとして宣言する必要があります。 場合によっては、コードの特定の部分でnil
にならないことを前提としているため、オプションのバインドを常に行うよりも簡単なため、強制的にアンラップするか、暗黙的にアンラップされたオプションとして宣言します。 これは注意して行う必要があります。
これは、ペン先またはストーリーボード内のオブジェクトを参照する変数であるIBOutlet
の操作に似ています。 これらは、親オブジェクト(通常はビューコントローラーまたはカスタムUIView
)の初期化時に初期化されませんが、 viewDidLoad
(ビューコントローラー内)またはawakeFromNib
(ビュー内)が呼び出されたときにnil
にならないことを確認できます。安全にアクセスできるようになります。
一般に、ベストプラクティスは、アンラップを強制したり、暗黙的にアンラップされたオプションを使用したりしないことです。 オプションはnil
である可能性があることを常に考慮し、オプションのバインディングを使用するか、アンラップを強制する前にnil
でないかどうかを確認するか、暗黙的にアンラップされたオプションの場合は変数にアクセスして適切に処理します。
2.強力な参照サイクルの落とし穴を知らない
強力な参照サイクルは、オブジェクトのペアが相互に強力な参照を維持する場合に存在します。 Objective-Cにも同じ問題があり、経験豊富なObjective-C開発者がこれを適切に管理することが期待されているため、これはSwiftにとって新しいことではありません。 強い参照と何が何を参照するかに注意を払うことが重要です。 Swiftのドキュメントには、このトピック専用のセクションがあります。
クロージャを使用するときは、参照を管理することが特に重要です。 デフォルトでは、クロージャ(またはブロック)は、クロージャ内で参照されるすべてのオブジェクトへの強力な参照を保持します。 これらのオブジェクトのいずれかがクロージャー自体への強い参照を持っている場合、強い参照サイクルがあります。 参照のキャプチャ方法を適切に管理するには、キャプチャリストを利用する必要があります。
ブロックが呼び出される前に、ブロックによってキャプチャされたインスタンスの割り当てが解除される可能性がある場合は、弱参照としてキャプチャする必要があります。これは、 nil
である可能性があるため、オプションになります。 これで、キャプチャされたインスタンスがブロックの存続期間中に割り当て解除されないことが確実な場合は、所有されていない参照としてキャプチャできます。 weak
の代わりにunowned
を使用する利点は、参照がオプションではなく、値をアンラップせずに直接使用できることです。
Xcode Playgroundで実行できる次の例では、 Container
クラスに配列と、配列が変更されるたびに呼び出されるオプションのクロージャがあります(プロパティオブザーバーを使用して変更します)。 Whatever
クラスにはContainer
インスタンスがあり、その初期化子で、 arrayDidChange
にクロージャーを割り当て、このクロージャーはself
を参照するため、 Whatever
インスタンスとクロージャーの間に強力な関係が作成されます。
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
この例を実行すると、出力されないものはdeinit whatever
であることがわかります。つまり、インスタンスw
はメモリから割り当て解除されません。 これを修正するには、キャプチャリストを使用してself
を強くキャプチャしないようにする必要があります。
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
この場合、クロージャーの存続期間中はself
がnil
になることはないため、 unowned
されていないを使用できます。
ほとんどの場合、キャプチャリストを使用して参照サイクルを回避することをお勧めします。これにより、メモリリークが減少し、最終的にはより安全なコードになります。
3.どこでもself
を使う
Objective-Cとは異なり、Swiftでは、メソッド内のクラスまたは構造体のプロパティにアクセスするためにself
を使用する必要はありません。 それはself
を捕らえる必要があるので、私たちはクロージャーでそうする必要があるだけです。 必要のない場所でself
を使用することは間違いではなく、問題なく機能し、エラーや警告は発生しません。 しかし、なぜあなたがしなければならないより多くのコードを書くのですか? また、コードの一貫性を保つことも重要です。
4.タイプがわからない
Swiftは値型と参照型を使用します。 さらに、値型のインスタンスは、参照型のインスタンスの動作がわずかに異なります。 各インスタンスがどのカテゴリに当てはまるかわからないと、コードの動作に誤った期待が生じます。
ほとんどのオブジェクト指向言語では、クラスのインスタンスを作成して他のインスタンスに渡し、メソッドの引数として、このインスタンスはどこでも同じであると期待しています。 つまり、実際には、まったく同じデータへの参照の集まりにすぎないため、変更はどこにでも反映されます。 この動作を示すオブジェクトは参照型であり、Swiftでは、 class
として宣言されたすべての型が参照型です。
次に、 struct
またはenum
を使用して宣言される値型があります。 値型は、変数に割り当てられたとき、または関数やメソッドに引数として渡されたときにコピーされます。 コピーしたインスタンスで何かを変更しても、元のインスタンスは変更されません。 値型は不変です。 CGPoint
やCGSize
などの値タイプのインスタンスのプロパティに新しい値を割り当てると、変更を加えて新しいインスタンスが作成されます。 そのため、配列のプロパティオブザーバーを使用して(上記のContainer
クラスの例のように)、変更を通知できます。 実際に起こっていることは、変更を加えて新しいアレイが作成されることです。 プロパティに割り当てられてから、 didSet
が呼び出されます。
したがって、処理しているオブジェクトが参照型または値型であることがわからない場合は、コードが何を実行するかについての期待が完全に間違っている可能性があります。

5.列挙型の可能性を最大限に活用しない
列挙型について話すとき、私たちは一般的に基本的なC列挙型について考えます。これは、その下にある整数である関連する定数の単なるリストです。 Swiftでは、列挙型ははるかに強力です。 たとえば、各列挙ケースに値を付加できます。 列挙型には、メソッドと読み取り専用/計算済みのプロパティもあり、各ケースをより多くの情報と詳細で強化するために使用できます。
列挙型に関する公式ドキュメントは非常に直感的であり、エラー処理ドキュメントは、Swiftでの列挙型の追加機能のいくつかのユースケースを示しています。 また、Swiftで列挙型を広範囲に探索した後、それらを使用して実行できるほとんどすべてのことを確認してください。
6.機能機能を使用しない
Swift標準ライブラリは、関数型プログラミングの基本であり、map、reduce、filterなど、1行のコードで多くのことを実行できる多くのメソッドを提供します。
いくつかの例を見てみましょう。
たとえば、テーブルビューの高さを計算する必要があります。 次のようなUITableViewCell
サブクラスがあるとします。
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
モデルインスタンスの配列modelArray
があるとします。 1行のコードでテーブルビューの高さを計算できます。
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
map
は、各セルの高さを含むCGFloat
の配列を出力し、 reduce
はそれらを合計します。
配列から要素を削除したい場合は、次のことを行うことになります。
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
この例は、アイテムごとにindexOf
を呼び出しているため、エレガントでも効率的でもありません。 次の例を考えてみましょう。
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
これで、コードはより効率的になりましたが、 filter
を使用することでさらに改善できます。
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
次の例は、特定の長方形と交差するフレームなど、特定の条件を満たすUIView
のすべてのサブビューを削除する方法を示しています。 次のようなものを使用できます。
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
ただし、これらのメソッドへの2回の呼び出しを連鎖させて、派手なフィルタリングと変換を作成したくなる可能性があるため、注意が必要です。これにより、1行の読み取り不能なスパゲッティコードが生成される可能性があります。
7.快適ゾーンにとどまり、プロトコル指向プログラミングを試さない
SwiftセッションでのWWDCプロトコル指向プログラミングで述べられているように、Swiftは最初のプロトコル指向プログラミング言語であると主張されています。 基本的に、これは、プロトコルに準拠して拡張するだけで、プロトコルを中心にプログラムをモデル化し、型に動作を追加できることを意味します。 たとえば、 Shape
プロトコルがある場合、 CollectionType
( Array
、 Set
、 Dictionary
などのタイプに準拠)を拡張し、交差点を考慮した総面積を計算するメソッドを追加できます。
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
where Generator.Element: Shape
が、拡張機能のメソッドが、 Shape
に準拠する型の要素を含むCollectionType
に準拠する型のインスタンスでのみ使用可能になることを示す制約であるステートメント。 たとえば、これらのメソッドはArray<Shape>
のインスタンスで呼び出すことができますが、 Array<String>
のインスタンスでは呼び出すことができません。 Shape
プロトコルに準拠するクラスPolygon
がある場合、それらのメソッドはArray<Polygon>
のインスタンスでも使用できます。
プロトコル拡張を使用すると、プロトコルで宣言されたメソッドにデフォルトの実装を与えることができます。これにより、それらのタイプ(クラス、構造体、または列挙型)に変更を加えることなく、そのプロトコルに準拠するすべてのタイプで使用できるようになります。 これはSwift標準ライブラリ全体で広く行われています。たとえば、 map
とreduce
はCollectionType
の拡張で定義されており、この同じ実装は、追加のコードなしでArray
やDictionary
などのタイプによって共有されます。
この動作は、RubyやPythonなどの他の言語のミックスインに似ています。 デフォルトのメソッド実装を備えたプロトコルに準拠するだけで、タイプに機能を追加できます。
プロトコル指向プログラミングは、一見すると非常に扱いにくく、あまり役に立たないように見えるかもしれません。そのため、それを無視したり、試してみたりすることすらできない可能性があります。 この投稿では、実際のアプリケーションでのプロトコル指向プログラミングの使用についてよく理解しています。
私たちが学んだように、Swiftはおもちゃの言語ではありません
Swiftは当初、多くの懐疑論を持って受け入れられました。 人々は、AppleがObjective-Cを子供向けのおもちゃの言語またはプログラマー以外の人向けの言語に置き換えようとしていると考えているようでした。 ただし、Swiftは、プログラミングを非常に快適にする真面目で強力な言語であることが証明されています。 強く型付けされているため、間違いを犯しにくく、そのため、言語で犯す可能性のある間違いをリストすることは困難です。
Swiftに慣れてObjective-Cに戻ると、違いに気付くでしょう。 Swiftが提供する優れた機能を見逃し、同じ効果を実現するには、Objective-Cで面倒なコードを記述する必要があります。 また、コンパイル中にSwiftがキャッチしたであろうランタイムエラーに直面することもあります。 これはAppleプログラマーにとって素晴らしいアップグレードであり、言語が成熟するにつれて、まだまだたくさんのことがあります。