詳解Swift#2(chapter3 構造体~5 基本的なデータ型)
昨日に続いて、詳解Swiftの読み込みと写経を続ける。
- 作者: 荻原剛志
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/12/25
- メディア: 単行本
- この商品を含むブログを見る
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扱いやすくて、且つ型定義に厳しいので保守性高いコードが継続的に書けそうで、多少人数増えても統一性のあるコードにできそう
- 以下細かな疑問
明日も進めるぞ。