- 投稿日:2019-02-26T18:37:31+09:00
Swiftでenum, class, protocolのどれを使うかの判断材料の比較
導入
Swiftにおいて、ある共通の性質を持つ複数の個別の値を表そうとする場合、その共通の性質を型で表すために、言語機能としてenum, class, protocolのいずれかを使うことができます。この記事ではその選択の判断材料となる性質の違いを比較して整理します。また、その向いていない点であっても、それをカバーする実装パターンがある場合、それを紹介します。なお、この記事では「個別の型」と「個別の値」の語を意識して使い分けています。
性質の違い
この章では、様々な性質について、3つの方式を比較します。
網羅性
共通の型としての値があるとき、実際の個別の値に応じて分岐する処理を書く際、全ての可能性を網羅しているか、コンパイラで静的に検査できると便利です。
enumは言語機能としてコンパイラによる網羅検査が提供されています。
// じゃんけん enum Janken { case guu case choki case paa } let hand = Janken.guu // error: Switch must be exhaustive switch hand { case .guu: print("グー") case .choki: print("チョキ") }例えば上記のコードでは、
paa
の分岐を忘れている事をコンパイラが指摘しています。classとprotocolでは、そのような網羅性の検査機能はありません。対策として、classとprotocolでも網羅検査をするための実装パターンがありますが、長くなるので別の記事にします。
個別の型
個別の値に個別の型を与えたい場合があります。
classとprotocolにおいては個別の型があります。
enumについてはcaseには型がありません。対策として、caseごとに専用のassociated valueを与えるという方法があります。
// 素朴な実装 enum Pet { case cat(name: String, cuteness: Int) case dog(name: String, strength: Int) case ferret(name: String, length: Int) } // caseごとの型を与える enum Pet { struct Cat { var name: String var cuteness: Int } struct Dog { var name: String var strength: Int } struct Ferret { var name: String var length: Int } case cat(Cat) case dog(Dog) case ferret(Ferret) }caseごとの型を与えた場合、その個別の型から共通の型への変換において、一度caseにラップして変換する必要があります。この変換は型ごとに異なるので面倒です。
addPet(.cat(Pet.Cat(name: "tama", cuteness: 100))) let cat = Pet.Cat(name: "tama", cuteness: 100) addPet(.cat(cat)) let dog = Pet.Dog(name: "pochi", strength: 100) addPet(.dog(dog))対策として、直接生成する場合に関しては、enum自体にstaticメソッドとしてコンストラクタを実装すると記述が簡単になります。
extension Pet { static func cat(name: String, cuteness: Int) -> Pet { return .cat(Cat(name: name, cuteness: cuteness)) } } addPet(.cat(name: "tama", cuteness: 100))このパターンは、SwiftPMの
Package.Dependency.Requirement
でも使われています。変換に関しては、共通のメソッドを与える事で簡単になります。
extension Pet.Cat { func asPet() -> Pet { return .cat(self) } } extension Pet.Dog { func asPet() -> Pet { return .dog(self) } } let cat = Pet.Cat(name: "tama", cuteness: 100) addPet(cat.asPet()) let dog = Pet.Dog(name: "pochi", strength: 100) addPet(dog.asPet())class, protocolでは、個別の型から共通の型への変換に記述は不要です。
class Pet { final class Cat : Pet { let name: String let cuteness: Int init(name: String, cuteness: Int) { self.name = name self.cuteness = cuteness } } final class Dog : Pet { ... } final class Ferret : Pet { ... } } let cat = Pet.Cat(name: "tama", cuteness: 100) addPet(cat)protocol Pet { } struct Cat : Pet { var name: String var cuteness: Int } struct Dog : Pet { ... } struct Ferret : Pet { ... } let cat = Cat(name: "tama", cuteness: 100) addPet(cat)ネームスペース
これまでの例で示しているように、enumとclassは個別の値を共通の型の内部のネームスペースで宣言する事ができます。
protocolはできません。
ジェネリクス
enumとclassはジェネリックパラメータを取る事ができます。
protocolはできません。
// ここではPetAssetに着目してください enum PetAsset<T: Pet> { case food(calory: Int) case toy(size: Int) } protocol Pet {} struct Cat : Pet { func take(asset: PetAsset<Cat>) {} } struct Dog : Pet { func take(asset: PetAsset<Dog>) {} } let cat = Cat() cat.take(asset: PetAsset<Cat>.food(calory: 100))protocolでやろうとした場合、associated typeを与える事になりますが、そうするとexistentialが使えなくなるので、type erasureが必要になります。そうすると、個別の型からtype erasureへの変換が必要になります。
protocol PetAsset { associatedtype TargetPet : Pet } struct PetFood<T: Pet> : PetAsset { typealias TargetPet = T var calory: Int } struct PetToy<T: Pet> : PetAsset { typealias TargetPet = T var size: Int } struct AnyPetAsset<T: Pet> { ... } struct Cat : Pet { func take(asset: AnyPetAsset<Cat>) {} } struct Dog : Pet { func take(asset: AnyPetAsset<Dog>) {} } let cat = Cat() cat.take(asset: AnyPetAsset(PetFood<Cat>(calory: 100))type erasureの実装方法についてはこちらに書いています。
https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0値型で扱う
その型を値型として扱いたいとします。
enumとprotocolは値型として扱われます。
ただし、protocolの場合は、個別の型がclassで実装されていた場合、参照型として振る舞うので注意が必要です。
protocol Pet {} class Cat : Pet { var name: String init(name: String) { self.name = name } } func addPet(_ pet: Pet) { (pet as? Cat)?.name = "たまちゃま" } let cat: Cat = Cat(name: "tama") let pet: Pet = cat addPet(pet) print(cat.name) // たまちゃまclassは参照型になってしまいます。対策として、値型でラップしてcopy on writeを実装することで、実装本体にclassを使いつつ値型にすることができます。
copy on writeの実装方法についてはこちらに書いています。
https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cdまた、enumではありませんが、copy on writeを実装した連結リストと順序付き辞書の自作のライブラリがあるので、参考にしてください。
https://github.com/omochi/OrderedDictionary/tree/master/Sources/OrderedDictionary参照型で扱う
その型を参照型として扱いたいとします。
classは参照型として扱われます。
protocolは、
AnyObject
制約をつけることで参照型として扱われます。protocol Pet : AnyObject {} // error: Non-class type 'Cat' cannot conform to class protocol 'Pet' struct Cat : Pet {}上記の例では、個別の型を誤って値型にしようとした事をコンパイラが指摘しています。
enumは値型になってしまいます。対策として、classでラップしてpropertyで持たせる事ができます。
// 元のenum実装 // (説明のためcaseを簡略化しています) enum JSON { case string(String) case array([JSON]) case object([String: JSON]) } // classでラップして参照型化した実装 class BoxedJSON { enum Value { case string(String) case array([BoxedJSON]) case object([String: BoxedJSON]) } var value: Value init(value: Value) { self.value = value } }この値型のJSONと参照型のJSONのペアは、私が作ったライブラリFineJSONで実際に使用しているので参考にしてください。
https://github.com/omochi/FineJSON/blob/master/Sources/FineJSON/BoxedJSON.swift外部拡張性
個別の値をユーザが追加できるようにしたい場合があります。
classとprotocolでは自然に追加可能です。
enumでは個別の型を追加することはできません。対策として、
custom
caseを始めから入れておいて、必要であればそれを使って個別の値を追加する事ができます。Foundation.Data.Deallocatorなどで使われています。
共通のフィールド
全ての個別の値で共通なフィールド(stored property)を持ちたい場合があります。
classでは親クラスで定義する事で実現できます。
class Pet { var name: String init(name: String) { self.name = name } } final class Cat : Pet {} final class Dog : Pet {} let cat = Cat(name: "tama") cat.name = "mike"protocolではproperty制約を書く事ができますが、個別の型で実装が必要です。
protocol Pet { var name: String { get set } } struct Cat : Pet { var name: String } var cat = Cat(name: "tama") cat.name = "mike"enumの場合、共通のフィールドを定義する直接の機能はありませんが、computed propertyを使って個別のcaseごとに対応する事で実現できます。
enum Pet { case cat(name: String, cuteness: Int) case dog(name: String, strength: Int) case ferret(name: String, length: Int) } extension Pet { var name: String { get { switch self { case .cat(name: let name, cuteness: _): return name case .dog(name: let name, strength: _): return name case .ferret(name: let name, length: _): return name } } set { switch self { case .cat(name: _, cuteness: let cuteness): self = .cat(name: newValue, cuteness: cuteness) case .dog(name: _, strength: let strength): self = .dog(name: newValue, strength: strength) case .ferret(name: _, length: let length): self = .ferret(name: newValue, length: length) } } } }しかし、共通でないassociated valueがあると、このようにとても冗長なコードになってしまいます。共通のフィールドが複数ある場合も考えると、caseごとの型を定義するパターンと合わせて、共通フィールドも型にまとめる書き方もできます。
enum Pet { struct Base { var name: String var weight: Int } struct Cat { var base: Base var cuteness: Int func asPet() -> Pet { return .cat(self) } } struct Dog { var base: Base var strength: Int func asPet() -> Pet { return .dog(self) } } struct Ferret { var base: Base var length: Int func asPet() -> Pet { return .ferret(self) } } case cat(Cat) case dog(Dog) case ferret(Ferret) } extension Pet { var base: Base { get { switch self { case .cat(let x): return x.base case .dog(let x): return x.base case .ferret(let x): return x.base } } set { switch self { case .cat(var x): x.base = newValue self = x.asPet() case .dog(var x): x.base = newValue self = x.asPet() case .ferret(var x): x.base = newValue self = x.asPet() } } } var name: String { get { return base.name } set { base.name = newValue } } var weight: Int { get { return base.weight } set { base.weight = newValue } } } var pet: Pet = Pet.Cat(base: .init(name: "tama", weight: 1), cuteness: 100).asPet() pet.name = "kuro"共通のメソッド
全ての個別の値で共通のメソッドを持ちたい場合があります。
protocolの場合、func制約を記述することで、個別の型での実装を強制する事ができます。
classの場合、親クラスで実装する事で実現できます。個別の型によって処理が異なる場合は、オーバーライドする事もできます。共通のフィールドや共通のメソッドの数が多い場合は、この親クラスに書くパターンが最も完結に記述できると思います。
ただ、オーバーライドするためには親クラスでの実装が必須なため、逆に言うと、必要な個別の実装をするのを忘れてしまい、親クラスの実装そのままになってしまうリスクがあります。
Swift以外の言語だと、abstract classという言語機能によって、サブクラスでの実装を強制する事ができる事が多いのですが、Swiftでは3年前にその機能が提案されてから今の所進捗がありません。
https://github.com/apple/swift-evolution/blob/master/proposals/0026-abstract-classes-and-methods.md
enumの場合、funcを実装して、その中で
switch self
して実装を与えることになります。そのようなメソッドがたくさんある場合、それぞれのfuncの中で毎回switchで分岐する形になるので、実装をcaseごとに局所化する事ができず、caseそれぞれの特性が捉えづらいソースコードになりがちに思います。観点の問題であって、関心が共通の型のenumの方にあると考える事もできると思います。まとめ
ここまでの観点を表にまとめます。参考になると幸いです。
enum class protocol 網羅性 ◎ ○1 ○1 個別の型 ○ ◎ ◎ ネームスペース ◎ ◎ × ジェネリクス ◎ ◎ × 値型で扱う ◎ △2 ○ 参照型で扱う ○ ◎ ◎ 外部拡張性 △3 ◎ ◎ 共通のフィールド ○ ◎ ○ 共通のメソッド ○ ○ ◎
- 投稿日:2019-02-26T05:00:28+09:00
Swift:自作のCommand Line ToolでCSVファイルを読み込む方法
Command Line Tool
iOSアプリしか開発しない人には縁がないかもしれませんが,Xcodeで新規プロジェクトを作る際,Command Line Toolのテンプレートを選択するとCUIベースのソフトウェアを作ることができます.普段からSwiftを使っていて,CSVの加工とかちょっとした文字列の加工処理などをしたいけれど,大きなGUIプロジェクトを立ち上げるほどでもないという状況にもってこいなので,どんどん使っていきましょう.今回はCommand Line ToolでCSVファイルを読み込むところまでを記述します.
プロジェクト立ち上げ
Xcodeの
File
->New
->Project...
->macOS
->Command Line Tool
から立ち上げられます.野良の.swift
ファイルを作ってVS CodeとかAtomとか外部のエディタでガリゴリ書いても良いのですが,やはりXcodeの強力な補完の恩恵は惜しいですからね.実装
今回はこちらのダミーCSVファイルを読み込むこととします.
一行目は各列の項目名になっています.valueA,valueB,valueC 1,3.14,pi 2,2.718,e 3,1.414,square root 2それでは
main.swift
ファイルを編集します.main.swiftimport Foundation struct Dummy { let a: Int let b: Float let c: String init(_ a: Int, _ b: Float, c: String) { self.a = a self.b = b self.c = c } var description: String { return "a: \(self.a), b: \(self.b), c: \"\(self.c)\"" } } let input = FileHandle.standardInput var data = [Dummy]() func scan() -> String { let text = String(data: input.availableData, encoding: String.Encoding.utf8) ?? "" return text.trimmingCharacters(in: CharacterSet.newlines) } func main() { let filePath = scan() guard let file = try? String(contentsOfFile: filePath, encoding: String.Encoding.utf8) else { exit(0) } var first: Bool = true file.enumerateLines { (line, stop) in if first { first = false } else { let item = line.components(separatedBy: ",") data.append(Dummy(Int(item[0])!, Float(item[1])!, c: item[2])) } } data.forEach { (dummy) in Swift.print(dummy.description) } } main()まず,CSVファイルに合わせて
Dummy
という構造体を用意しました.
scan()
メソッドは標準入力を受け付けて改行を入力すると文字列を返します.
\main()`メソッドではCSVファイルの中身をUTF-8で読み込んで一行ずつ処理を行い,Dummyのデータを配列に追加し,最終的に出力しています.実行
XcodeでRunしてもいいのですがXcodeのdebugエリアへの標準入力は文字が不可視となることがあり不安定なので,Terminal.appでmain.swiftを実行します.
Terminalコンソール$ cd ~/Desktop/CommandLine/CommandLine/ <-- [main.swiftが入っているディレクトリのパス] $ swift main.swift ./dummy.csv <-- [CSVファイルのパス] a: 1, b: 3.14, c: "pi" a: 2, b: 2.718, c: "e" a: 3, b: 1.414, c: "square root 2" $CSVファイルをmain.swiftと同じディレクトリに入れておくと,ファイル指定が楽ですね.
ちなみに,コマンドライン引数をとりたい時はこちらの記事を参考にしてみてください.
- 投稿日:2019-02-26T03:35:05+09:00
tableview auto layoutのcellでreloadや遷移のタイミングでscrollのpositionがずれる
言葉じゃ伝えづらいので絵にしました。(力作)
こんな感じでauto layout使用した可変の、それぞれの高さがだいぶ違うcellがあったとき。
上だとAのcellは300にもなるけどBは40みたいなときこのとき画面遷移やreloadをするとcellがずれる事案が起こりました。。(力作2)
なぜこうなる
どうやらestimatedHeightで決め打ちの高さを決めてもそこからサイズが離れすぎると起こる模様。
cellの高さを持ってあげて、estimatedHeightでそれらをしっかり返してあげればOK
HogeViewController.swiftprivate var cellHeightsDictionary: [IndexPath: CGFloat] = [:] func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { self.cellHeightsDictionary[indexPath] = cell.frame.size.height } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { if let height = self.cellHeightsDictionary[indexPath] { return height } return UITableView.automaticDimension }