kentana20 技忘録

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

詳解Swift#2(chapter3 構造体~5 基本的なデータ型)

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

詳解 Swift 改訂版

詳解 Swift 改訂版

chapter3 構造体

構造体(Structure)についての章。

構造体の定義

struct 型名 {
    変数/定数定義
    イニシャライザ定義
    メソッド定義
    その他の定義(型、プロパティなど)
}

で書く。

  • 用語の話

    • イニシャライザ: 構造体のインスタンス生成時の手続き
    • 既定イニシャライザ: 各プロパティの初期値を記述してある構造体を引数なしで生成できるイニシャライザ
    • 全項目イニシャライザ: 既定イニシャライザの逆。個々のプロパティの値を指定して生成するイニシャライザ
  • シンプルな構造体 / 初期値 / イニシャライザ

// 初期値のない構造体
struct Date {
    var year: Int
    var month: Int
    var day: Int
}

// 初期値のある構造体
struct DateInit {
    var year: Int = 2016
    var month: Int = 5
    var day: Int = 3
}

var dx = DateInit()
print(dx, terminator:"")

var d = Date(year: 2016, month: 5, day: 3)

// "."でプロパティ名を指定して参照可能
print(String(d.year), terminator:"") // "2016"

// 演算もできる
d.day++
++d.day
print(String(d.day), terminator:"") // "5"

構造体の要素(プロパティ)は初期値があったり、なかったりする。構造体を定義しておくと、構造体のインスタンスを生成して、各プロパティに".プロパティ名"でアクセスできるし、プロパティの値を変更したりもできる。

  • 定数プロパティと構造体
struct DateX {
    var year: Int
    var month: Int
    var day: Int
}

// 定数としても代入できる
let l = DateX(year: 2016, month: 5, day: 3)
//l.day++ <- error (変更できない)

struct Time {
    let in24h: Bool = false
    var hour = 0, min = 0
}

var t1 = Time()
var t2 = Time(hour: 11, min: 0)
//var t3 = Time(in24h: true, hour: 11, min: 0) <- error

プロパティには定数も定義できる。当然、定数のプロパティは変更不可。イニシャライザによってインスタンス生成時に定数のプロパティに値をセットするケースと、初期値が定義されているケースとある。

  • イニシャライザの定義
struct Date {
    var year, month, day: Int
    init() {
        year = 2016
        month = 5
        day = 3
        self.year++
    }
}

var d = Date()
d.year // 2017

構造体に init キーワードでイニシャライザを定義できる。

  • 複数のイニシャライザ
struct Time {
    let in24h: Bool
    var hour = 0, min = 0
    
    // 1つめのイニシャライザ
    init(oHour: Int, oMin: Int) {
        in24h = false
        self.hour = oHour
        self.min = oMin
    }
    
    // 2つめのイニシャライザ
    init(hourIn24 h: Int) {
        in24h = true
        self.hour = h
    }
    
    // 3つめのイニシャライザ
    init(_ hour: Int) {
        self.init(hourIn24: hour)
    }
}

var a = Time(oHour: 11, oMin: 30)
var b = Time(hourIn24: 12)
var c = Time(13)
//var d = Time() <- error
//var e = Time(in24h: true, hour: 12, min: 0) <- error

イニシャライザは複数定義できる。またイニシャライザが定義されている場合は既定イニシャライザと全項目イニシャライザは使えなくなる。

  • 構造体を含む構造体とネスト型
struct DateWithTime {
    var date = Date()
    var time = Time(oHour: 12, oMin: 30)
}

var u = DateWithTime()
u.date.year
u.time.hour

構造体を含む構造体も作ることができる。"."の繰り返しで必要なプロパティにアクセスできるので宣言的に書ける。

struct SimpleTime  {
    var hour, min: Int
    init(_ hour:Int, _ min: Int) {
        self.hour = hour
        self.min = min
    }
}

struct PointOfTime {
    struct Date {
        var year, month, day: Int
    }
    
    typealias Time = SimpleTime
    var date: Date
    var time: Time
    init(year: Int, month: Int, day: Int, hour: Int, min: Int) {
        date = Date(year: year, month: month, day: day)
        time = Time(hour, min)
    }
    
}

メソッド

struct Clock {
    var hour = 0
    var min = 0
    func time() -> Clock {
        return Clock(hour: hour, min: min)
    }
    
    mutating func advance(min: Int) {
        let m = self.min + min
        if m >= 60 {
            self.min = m % 60
            self.hour = (self.hour + m / 60) % 24
        } else {
            self.min = m
        }
    }
    
    func nonAdvande(min: Int) {
        //self.min += min <- error
    }

    static func isPm(h: Int) -> Bool {
        return h > 12
    }

}

構造体にはメソッドを定義できる。構造体の内容を変更するメソッドには mutating キーワードを付与する必要がある。オブジェクト指向でのクラスメソッドに相当するタイプメソッドstatic キーワードを付与して定義する。タイプメソッドはイニシャライザの中でも使えるが、インスタンスメソッドはプロパティの初期設定が完了していないと使えない。

プロパティ

  • タイププロパティ

タイププロパティはクラス変数的なもの。

イニシャライザからタイププロパティ/タイプメソッドにアクセスするときは構造体の修飾子が必要。一報でタイプメソッドからタイププロパティにアクセスする場合は修飾子不要。タイププロパティを変更するメソッドには mutating キーワードは不要。

  • 格納型プロパティ
var serialNumber = 2127

struct LCD {
    struct Size { var width, height : Int }
    static var stdHeight = 1080
    static var stdWidth = 1920
    static var stdSize = Size(width: stdWidth, height:stdHeight )
    var size: Size
    let serial = "CZ:\(++serialNumber)"
    init(_ w:Int, _ h:Int) {
        size = Size(width: w, height: h)
    }
}

let small = LCD(800, 600)
print(small.serial)
LCD.stdHeight = 1200
print(LCD.stdSize)
LCD.stdWidth = 2560
print(LCD.stdSize)

プロパティに式を指定することもできる。タイププロパティの初期化のための式は値を必要としたタイミングで初めて評価され、その後は使われない(変更されない)。値を都度変更して使いたい場合は 計算型プロパティ を使う。

この辺り、慣れないと読み違えそうな雰囲気...

  • 計算型プロパティ
struct Ounce {
    var ml: Double = 0.0
    static let ounceUS = 29.5735
    
    var ounce: Double {
        get { return ml / Ounce.ounceUS }
        set { ml = newValue * Ounce.ounceUS }
    }
    
    init(ounce: Double) {
        self.ounce = ounce
    }
}

var a = Ounce(ounce: 1.5)
a.ml // 44.3625
a.ounce = 3 // ounce代入によってmlも値が変わる
a.ml // 88.7205
a.ounce // 3

クラス変数のアクセサ的なもの。set節とget節によって参照/更新を行う。set節の引数は省略可能で、 newValue という名前で引数を利用できる。

読み書き可能なプロパティを関数のinout引数に渡すこともできる。この辺り、多分実戦になると構造体のプロパティを引数に関数を呼ぶことがある気がするのでなかなか気の利いた設計になってるように感じた。

  • プロパティオブザーバ
struct Stock {
    let buyingPrice: Int
    var high = false
    var count = 0
    init(price:Int) {
        buyingPrice = price
        self.price = price
    }
    var price:Int {
        // プロパティオブザーバ(更新前に呼び出される)
        willSet {
            ++count
            high = newValue > buyingPrice
        }
        // プロパティオブザーバ(更新後に呼び出される)
        didSet {
            print("\(oldValue) => \(price)")
        }
    }
}

var st = Stock(price:400)
st.price = 410
st.price = 380
st.price = 430
print("\(st.count), \(st.high)")
st.price++
print("\(st.count), \(st.high)")

格納型プロパティの値が更新されるタイミングで別の手続きを呼び出す。更新前、更新後それぞれの手続を定義できる。Railsで言うところのフィルタみたいな機能。 willSet , didSet というコードブロックで定義する。インスタンス生成時にはプロパティオブザーバは発動しない。


添字付けはあんまり用途なさそう、というか実装したあとで可読性下がりそうなので説明は割愛


プロトコル

後のchapterで説明があるので、ここでは概念だけ。 型が持つべきメソッドやプロパティなどの宣言をまとめる仕組みプロトコルと呼ぶ。インタフェースみたいなやつ。

- Int
- Double
- String

などのデータ型はComparableプロトコルに準拠しているので、比較演算を行うことができる。

chapter4 オプショナル

Swiftの特徴の1つ。「扱うデータがない」という状態をとりうる型。頻出、超重要。

オプショナル型

  • オプショナル型とnil
var a: Int = 0
var b: Int? = 100

b = nil
//a = nil <- error

// このようにも書ける
var c: Optional<Int> = 0
c = nil
c = 5

nilを取りうる変数は型名の末尾に ? をつける。オプショナルではない変数にはnilを代入できないので、Null Pointer例外による実行時エラーを少なくできる(と期待されている)。たとえば、nilを返す可能性がある関数やイニシャライザの戻り値を受け取る変数の型はオプショナルでなくてはいけない。

  • 開示
var a: Int = 10
var c: Int? = 5

//a + c <- error
a + c!

c = nil

//a + c!  <- error

オプショナルInt型とInt型は厳密には異なるので、そのまま演算できない。オプショナル型の変数に ! を付けてデータを開示して利用する。値がnilだった場合は開示しても実行時にエラーとなる。

オプショナル束縛構文

let year: Int? = 2016
if let y = year {
    print("yearはnilじゃない。\(y)だ。")
} else {
    print("yearはnil")
}

// letじゃなくてvarでも受けられる
if var ye = year {
    print("yearはnilじゃない。\(y)だ。")
} else {
    print("yearはnil")
}

// 特別な意味を持って開示された状態で代入されるのは if/while の条件式で書いた時のみ
var z = year  // これだとフツーにオプショナル型になる

とある変数がnilじゃなかったときに必要な処理をしたいユースケースに適用できる。if-let文というらしい。条件式で代入している変数(この例だと y )はオプショナル型ではなくなるので、開示は不要となる。オプショナル束縛構文ともいうらしい。ちなみにelseに入った場合は条件式で代入した変数は使えない。

これは結構使えそうだが、1つ残念なのは三項演算子では使えないこと。三項演算子で使えないのはなんでなんだろ。

var opv: Int? = 0

(opv != nil) ? opv! : 5
opv ?? 5 //↑と同じ意味

自己代入的なあれ。これも結構使いそう。

有値オプショナル型

var year: Int! = 2020

print("あと\(year - 2016)年")
//year++ <- error
year = nil
var month: ImplicitlyUnwrappedOptional<Int> = 2020

開示しなくても通常の変数のように使える型のこと。nilを代入できてしまうが、開示は不要。はじめはnilを許可しておくのだけど、どこかのタイミングで値が入ることが保証されている場合に開示を意識せずにコードを書けるので、実戦では役に立ちそう。

func nickname(name:String?, age:Int) -> String {
    let s = name ?? "名無し"
    return "浪速の" + s + "(\(age)歳)"
}

これはめっちゃ実践的なコードな気がする。関数の引数にnilが入って来る場合を考慮して let s で受けて自己代入して実際の処理を行うパターン。

失敗のあるイニシャライザ

struct Time {
    let in24h: Bool
    var hour = 0, min = 0
    init?(_ h:Int, _ m:Int, in24h:Bool = false) {
        let maxh = in24h ? 23 : 11
        if h < 0 || h > maxh || m < 0 || m > 59 {
            return nil
        }
        self.in24h = in24h
        hour = h
        min = m
    }
    init(time:Time, in24h:Bool) {
        var h = time.hour
        if !in24h && time.hour > 11 {
            h -= 12
        }
        self.in24h = in24h
        hour = h
        min = time.min
    }
}

インスタンス初期化時にnilを返す可能性があるイニシャライザ。データが作れなかったり、引数に不備があったりして初期化に失敗した場合にnilを返す。 init? でイニシャライザを定義する。イニシャライザの中で例外ケースの場合に return nil する。

chapter5 基本的なデータ型

この章はデータ型メインの話なのでさらっと。

整数と実数

  • 数値型リテラル
    • 桁数の大きい数値を見やすくするために _ を使える
    • 16進数の場合は 0x , 8進数の場合は 0o で表す(両方小文字)
  • Swiftには暗黙の型変換がないので、異なる型の演算には型変換が必要
    • 型変換例
Int.init()
Int.init(_: Double)
Int.init(_: UInt)
.
.
.
演算子 意味
半開区間型 A..<B A ≦ x < B
閉区間型 A...B A ≦ x ≦ B
範囲型 A..<B A ≦ x < B
範囲型 A...B A ≦ x ≦ B

半開区間型と閉区間型はわかるけど、範囲型で書き直す意味がよくわからない。

配列

コレクションの1種。最初に宣言した型の値しか格納できない。配列はCollectionTypeというプロトコルに準拠している。

  • 部分配列
var days = ["日", "月", "火", "水", "木", "金", "土"]
let sub = days[2..<5]   // sub は ArraySlice型
print(sub)          // [火, 水, 木] を出力
print(sub.count)        // 3 を出力。要素は3つ。
print(sub.startIndex)   // 2 を出力。配列の最初の添字を表すプロパティ。
print(sub[2])       // "火" を出力
print(sub[4])       // "木" を出力

添字を指定して元ある配列から部分的に抽出した配列を作れる。Array型ではなくてArraySlice型となる。添え字は0から始まらず、部分的に切り取った添え字がそのまま残る。

若干わかりづらくて、バグの温床になりそうな気配。あまり使いたくない印象。

  • 配列のプロパティ

    • count
    • first
    • isEmpty
    • last
  • 配列のメソッド

    • append
    • appendContentsOf
    • insert
    • removeAll
    • removeAtIndex
    • removeLast
    • reverse

だいたいどれもメソッド名見れば振る舞いがわかる。

  • 多次元配列
var table: [[String]] = [["A","B"],["X","Y","Z"]]
print(table[0])            // ["A", "B"] を出力
print(table[1][0])         // X を出力
table[0].removeAtIndex(1)  // "B" が削除される
print(table)               // [["A"], ["X", "Y", "Z"]] を出力

[[型]] で定義する。

文字列と文字

  • String型のプロパティ

    • isEmpty
    • characters
    • unicodeScalars
    • utf8
    • utf16
  • 式展開

    • \(変数) で式展開できる
var str: String = "kentana20"
print("わたしは \(str) です")

文字列のところは、実戦でつかうときに辞書的に引きそうなので割愛。文字コードとかUnicodeの概念がわかってれば大丈夫。

辞書

Dictionary, Hash的なもの。これも配列同様に始めに定義した型のデータのみを扱える。Swiftは型による制約が厳しい。

  • シンプルな辞書
var d: [String: Int]
d = ["swift": 2014, "Objective-C": 1983]
d["swift"] // 2014
  • 辞書とfor-in
let dic = ["Mars": "火星", "Venus": "金星"]
for (en, ja) in dic {
    print("\(en) = \(ja)")
}

// こんな感じでも書ける
for t in dic {
    print("\(t.0) = \(t.1)")
}
  • 辞書のプロパティとメソッド
    • updateValue
    • removeValueForKey
    • count

集合

配列に似ているが、以下の点で異る。

  • 同一の要素を重複格納できない
  • 要素の順序に意味が無い

集合演算を行う必要がある場合は集合を使う。

  • 集合のメソッド
    • contains
    • insert
    • remove
    • removeFirst
    • isSubsetOf
    • union

だいたいわかる。

所感・雑感

3~5章まで読了。気になったことをいくつか書いてみる。

  • 構造体はかなり便利に使えそう。イニシャライザが複数定義できるので、用途に応じて初期化の仕方が変わることは多々あるので良さそうな気配
  • オプショナル/有値オプショナルもうまく使えば厳密なコードになりそうなので好印象
  • 式展開や自己代入など、よく使う機能もしっかり整備されてるのでやっぱりSwift扱いやすくて、且つ型定義に厳しいので保守性高いコードが継続的に書けそうで、多少人数増えても統一性のあるコードにできそう
  • 以下細かな疑問
    • .class に相当するメソッドはないのか(まだそんなに調べてない)
    • 部分配列は便利に利用できそうな半面、見通しが悪くなりそうな懸念がある
    • パラメータ付き型指定ってどれくらい書かれているのか、もう少し読み進めたらCocoa Podsのライブラリのコードを読んでみよう

明日も進めるぞ。