kentana20 技忘録

技術ネタを中心に、セミナー、勉強会、書籍、会社での出来事を綴っていきます。不定期更新。

詳解Swift#3(chapter6 パターン)

昨日に続いて、詳解Swiftの写経を続ける。

詳解 Swift 改訂版

詳解 Swift 改訂版

chapter6 パターン

いくつかのデータを組にして扱うことができる タプルと列挙型 についての章。

タプル

複数個のデータを組にしてまとめたもの。データは値型。

  • 複数個の値(データ)をまとめて扱いたい
  • 構造体・クラスを作る程でもない
  • 関数の戻り値を複数返却したい

といったシーンで活躍する。

  • シンプルなタプルの例
let m = ("monkey.jpg", 161_022)
let m2: (String, Int) = ("monkey.jpg", 161_022)

let cat: (String, Int, Int) = ("cat.jpg", 1024, 768)
var img = cat
//img = m2 <- error
var img2 = cat

print(cat.0) // cat.jpg
img.1 = 1920
// img == img2 <- error

let m3 = (file: "tiger.jpg", width: 1024, height: 768)
m3.file
m3.0

変数名: (1つ目の要素の型, 2目の要素の型, .....) = (要素1の値, 要素2の値, .....) で記述する。タプルの要素へのアクセスは タプル変数.添え字 と書く。要素にキーワードが付いている場合は タプル変数.要素キーワード名 でもアクセスできる。要素の値を書き換えることもできる。タプルの要素の1つにタプルを入れることもできる。

タプルの代入には要素の個数と型が一致していなければいけない、タプルの比較はできないなどの制約がある。

  • タプルを返す関数
func BMI(tall:Double, _ weight:Double) -> (bmi: Double, idealWeight: Double) {
    let ideal = 22.0                  // 理想的な値
    let t2 = tall * tall / 10000.0    // cm を m に換算して二乗
    let index = weight / t2           // BMIを計算
    return (index, ideal * t2)        // 目標体重も計算して返す
}

var ret = BMI(172.0, 58.5)
print(ret.bmi)
print(ret.idealWeight)

関数の戻り値を (Double, Double) とすることで、2つのDouble型の値を含むタプルを返却できる。これは便利。

  • キーワード付きのタプル
let img = ("phoenix.png", 1200, 800)
let photo = (file:"tiger.jpg", width:640, height:800)
print(img)
print(photo)
var v1 : (String, Int, Int) = img
var v2 : (file:String, width:Int, height:Int) = img
var v3 : (image:String, x:Int, y:Int) = img
v1 = photo
v2 = photo
// v3 = photo  // エラーになる
print("v1 = \(v1)")
print("v2 = \(v2)")

v3 = photo as (String, Int, Int)
print("v3 = \(v3)")

キーワード付きのタプルへの代入は同じキーワードと型を持つタプルかキーワードを持たないタプルでないとできない。↑の例だと異るキーワードを持つタプルv3への代入はエラーとなる。

列挙型

Swiftでの列挙型は列挙型の中にメソッドやプロパティの定義ができたり、イニシャライザも定義できたり拡張性が高い。

  • シンプルな列挙型
enum Direction {
    case Up
    case Down
    case Right
    case Left
}

Direction.Up
print(Direction.Up, terminator: "") // Up
enum Direction {
    case Up, Down, Right, Left
    func clockwise() -> Direction {
        switch self {
        case Up:    return Right
        case Down:  return Left
        case Right: return Down
        case Left:  return Up
        }
    }
    
    mutating func changeClock() {
        switch self {
        case Up: self = Right
        case Down: self = Left
        case Right: self = Down
        case Left:  self = Up
        }
    }
}

let d = Direction.Up
var d2 = Direction.Up
d.clockwise()
print(d) // 値は変わらない
//d.changeClock() <- error
d2.changeClock() // ここで値を変更(Up -> Right)
print(d2)

メソッド付きの列挙型。 mutating キーワードを付けると列挙型自身の値を変更するメソッドも作れる。計算型のプロパティも定義できる。

  • 列挙体のタイプメソッド、タイププロパティ
enum Direction : Int {
    case Up = 0, Right, Down, Left
    static var defaultDirection = Direction.Up
    init() {
        self = Direction.defaultDirection
    }
    static func arrow(d:Direction) -> String {
        return ["↑", "→", "↓", "←"][d.rawValue]
    }
    func clockwise() -> Direction {
        let t = (self.rawValue + 1) % 4
        return Direction(rawValue:t)!
    }
    var horizontal: Bool {
        switch self {
        case Right, Left: return true
        default: return false
        }
    }
    mutating func turnBack() {
        self = Direction(rawValue:((self.rawValue + 2) % 4))!
    }
}

Direction.defaultDirection = .Right
var e = Direction()
print(Direction.arrow(e))

構造体と同様にタイプメソッド、タイププロパティは static , イニシャライザは init で定義できる。

共用型の列挙型

enum WebColor {
    case Name(String)   // 色の名前
    case Code(String)   // 16進数によるカラーコード
    case White, Black, Red, Blue, Green, Yellow, Gray
}

let indigo = WebColor.Name("indigo")         // インディゴブルー
let turquoise: WebColor = .Code("#40E0D0")   // ターコイズ
let textColor = WebColor.Black

値型の列挙型とはまったくユースケースが異る。同じ役割のオブジェクトなんだけど、使い方(定義の仕方)が違う。みたいな場合に使えそう。

  • if-case文
enum Ticket {
    case 切符(Int, Bool, 回数券:Bool) // 普通券:価格、小人、回数券かどうか
    case カード(Int, Bool)            // プリペイドカード:残高、小人
    case 敬老パス                     // 敬老パス
}

var list:[Ticket] = [
    .切符(250, false, 回数券:false),
    .カード(3300, false),
    .敬老パス,
]

for p in list {
    /* エラー
      if p == .カード { print("プリペイドカード") }
    */
    switch p {                  // switch文ではこのように記述できる
    case .カード: print("プリペイドカード")
    default: break
    }
}

// if-case文を使うとよりシンプルに
for p in list {
    if case .カード = p {
        print("プリペイドカード")
    }
}

共用型の列挙型の配列から条件にあうものだけで処理したい場合に使える。残念だけど今のところ、どれくらい便利なのかがわからん。

  • for-in文でもcaseパターンを使える
enum Ticket {
    case 切符(Int, Bool, 回数券:Bool) // 普通券:価格、小人、回数券かどうか
    case カード(Int, Bool)            // プリペイドカード:残高、小人
    case 敬老パス                     // 敬老パス
}

var passes:[Ticket] = [
    .切符(320, false, 回数券:false),
    .切符(120, true, 回数券:true),
    .切符(240, false, 回数券:true),
    .切符(240, true, 回数券:false),
    .敬老パス,
    .カード(3300, false)
]

for case let .切符(fare, child, coupon) in passes where fare > 220 {
    var k = coupon ? "回数券" : "普通券"
    if child { k += "(小人)" }
    print(k, "\(fare)円")
}

ここまで読んで有用性が少し見えてきた気がする。列挙型の配列の中から複数条件を指定して処理を書く場合にうまく使えばシンプルに書ける。ただ、caseパターンが複数あったり、条件が複雑過ぎて横に記述が長くなるので万能ではない印象。

enum メッセージ : CustomStringConvertible {
    case 文書 (String, String)              // 差出人、文書
    case データ(String, [Int8])             // 差出人、データ列
    indirect case 転送(String, メッセージ)  // 差出人、メッセージ
    var description: String {
        switch self {
        case let 文書(from, str): return from + "(" + str + ")"
        case let データ(from, _): return from + "[データ]"
        case let 転送(from, msg): return from + "←\(msg)"
        }
    }
}

let m1 = メッセージ.文書("伊藤", "休みます")
print(m1) // 伊藤(休みます)
let m2 = メッセージ.転送("白石", m1)
print(m2) // 白石←伊藤(休みます)
let m3 = メッセージ.転送("山田", m2)
print(m3) // 山田←白石←伊藤(休みます)```

再帰的に列挙型のタプルに列挙型自身を要素として指定する場合は indirect キーワードをcaseパターンの前に記述する。

パターンマッチ

どの構造に何が当てはまるのかというルールを定義して、ルールに基づいたデータ処理をパターンマッチという。

  • タプル
  • 共用型の列挙型

で利用できる。

などでパターンを書いて、それぞれにマッチする場合の処理を記述する。

所感・雑感

  • タプル
    • 代入の要素/型制約などを見ても型についての厳格さを感じる。個人的には嫌いではないものの、実戦では少し窮屈に思う時があるかもしれない
    • タプルの要素にはキーワードを付けるのをデフォルトにしたい。添字だけだと、コードが大きくなった場合に見通しが悪くなりそう。
  • 列挙体
    • 構造体と同様に確かに拡張高い。コード系の管理とか、コードに関する振る舞いを列挙体自身のメソッドにしておくと、一貫性あるコードを書けそう。
    • 「共用型」という名前の意味がわからん。何を共用するのか。
    • if-case / for-in でのcaseなどはライブラリのコードを読んでみたい
  • パターンマッチ
    • 例が少なかったので、逆引きリファレンスとかレシピがほしい

今日はあんまり時間とれなかったので1章分。夜に時間あったらもうちょい進める。