20190226のSwiftに関する記事は3件です。

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でも使われています。

https://github.com/apple/swift-package-manager/blob/master/Sources/PackageDescription4/PackageRequirement.swift#L14

変換に関しては、共通のメソッドを与える事で簡単になります。

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などで使われています。

https://github.com/apple/swift-corelibs-foundation/blob/620066ffaebbfab7099fe7b961c2eca3e62b374e/Foundation/Data.swift#L1866

共通のフィールド

全ての個別の値で共通なフィールド(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
共通のフィールド
共通のメソッド

  1. 対策があるので○にしました。記事をお待ち下さい。 

  2. 対策はありますが現実味が薄いので△にしました。 

  3. 対策はありますが向いていないと言えるので△にしました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift
import 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と同じディレクトリに入れておくと,ファイル指定が楽ですね.
ちなみに,コマンドライン引数をとりたい時はこちらの記事を参考にしてみてください.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tableview auto layoutのcellでreloadや遷移のタイミングでscrollのpositionがずれる

言葉じゃ伝えづらいので絵にしました。(力作)

スクリーンショット 2019-02-26 3.27.46.png

こんな感じでauto layout使用した可変の、それぞれの高さがだいぶ違うcellがあったとき。
上だとAのcellは300にもなるけどBは40みたいなとき

このとき画面遷移やreloadをするとcellがずれる事案が起こりました。。(力作2)

スクリーンショット 2019-02-26 3.27.40.png

なぜこうなる

どうやらestimatedHeightで決め打ちの高さを決めてもそこからサイズが離れすぎると起こる模様。

cellの高さを持ってあげて、estimatedHeightでそれらをしっかり返してあげればOK

HogeViewController.swift
    private 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
    }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む