Swift/オブジェクト指向

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

型エイリアス[編集]

エイリアス
typealias Long = Int64

列挙型[編集]

Swiftでは、重複しない識別子の集合を型を宣言するために列挙型(enum 型)が用意されています[1]

形式
// 列挙型の定義
enum 列挙型名 {
  case 列挙ケース名1
  case 列挙ケース名2
  case 列挙ケース名3
  func メソッド名1(引数リスト1) -> 戻値型1 { 処理1 }
  func メソッド名2(引数リスト2) -> 戻値型2 { 処理2 }
}

// 列挙型定数の宣言
let 定数名 : 列挙型名 = 列挙型名.列挙ケース名
let 定数名 : 列挙型名
let 定数名 = 列挙型名.列挙ケース名

// 列挙型変数の宣言
var 変数名 : 列挙型名
var 変数名 = 列挙型名.列挙ケース名
var 変数名 : 列挙型名 = 列挙型名.列挙ケース名
列挙型 enum は、有限の識別子の集合を型として定義する機能です。
列挙ケース(集合要素の識別子)にはディフォルトでは型はありません。
後述の #関連値 を使えば列挙ケースに型と値を与えられます。
4.2以降は、CaseIterable プロトコルが使えます。
enum/case と switch/caseの組合わせ例
// 方位
enum Azimuth {
  case North
  case South
  case East
  case West
  func string() -> String {
    switch self {
    case .North: return "北"
    case .South: return "南"
    case .East: return "東"
    case .West: return "西"
    }
  }
  func deg() -> Int {
    switch self {
    case .North: return 0 * 90
    case .South: return 2 * 90
    case .East: return 1 * 90
    case .West: return 3 * 90
    }
  }
}

let azi = Azimuth.West
print("\(azi)は、\(azi.string())で、方位角は \(azi.deg())° です。")
print(String(describing: type(of: azi)))
実行結果
Westは、西で、方位角は 270° です。 
Azimuth
この例では、「方位」を列挙型で定義し文字列化をメソッドとしています。
このように列挙型は列挙ケースだけでなく、メソッドも定義できるので一見すると後述の#3クラスと構造体に似ていますが、

switch文の条件式にenum型の値を渡すと、switch文のcase節enum型を網羅しているか検査されます(網羅性の担保)。 もし、網羅していないと

コンパイル結果
main.swift:8:5: error: switch must be exhaustive
    switch self {
    ^
main.swift:8:5: note: add missing case: '.West'
    switch self { 
^
の様に網羅していないことをエラーとして報告しコンパイルが中断されます。
もし default: あるいは case _: のような総称てきなパターンを書いてしまうと網羅性の検査が働かなくなるので注意が必要です。

関連値[編集]

列挙ケースの型を列挙型宣言に時に指定する事ができます。

なお、下記コードの ”北” など、要素に関連づけさせた値のことを、英語で associated value といいです、和訳で「関連する値」だとか「関連値」などといいます[2]

定義した値へのアクセスには enum変数.rawValue を使います。

コード例
enum Compass: String {
  case N = "北"
  case S
  case E
  case W
}

var direction = Compass.N
print(direction)
print(direction.rawValue)

print(Compass.S)
print(Compass.S.rawValue)
実行結果
N
北
S 
S
値が定義されていない場合、rawValue は上述のように列挙ケース名の文字列を返し、コンパイル時にエラーにもならず警告も発しません。
このため実行するまで定義漏れに気づく機会がなく、その点でメソッドの中のswitch/caseでの網羅性の担保に劣ります。

enumにおいて値の型を整数型で宣言した場合、特に値を上書きしない限りは、列挙ケースの宣言順に 0,1,2,・・・の値がそれぞれの列挙ケースに割当てられます。

値の型を伴った列挙型の宣言
enum Compass: Int {
  case N
  case S
  case E
  case W
}

var direction = Compass.N
print(direction)
print(direction.rawValue)
print()

direction = Compass.S
print(direction)
print(direction.rawValue)
print()

direction = Compass.E
print(direction)
print(direction.rawValue)
print()
実行結果
N
0

S
1

E
2

[TODO:CaseIterable プロトコル]

クラスと構造体[編集]

インスタンスの初期値[編集]

Swift のクラスや構造体では、インスタンスのプロパティーが初期化されないと、コンパイル時にエラーとなります。

プロパティーの初期化漏れのあるコード
struct SampleStruct {
  var age: Int
}

var ss1 = SampleStruct()

print("ss1.age = \(ss1.age)")
コンパイル時のエラー
main.swift:5:24: error: missing argument for parameter 'age' in call
var ss1 = SampleStruct()
                       ^
                       age: <#Int#>
main.swift:1:8: note: 'init(age:)' declared here
struct SampleStruct {
       ^
init() の定義がないのは間違いではなく、Swiftコンパイラーは init() が未定義の場合 init(age: Int) { self.age = age }相当のコードを自動生成します。

このため、

インスタンス生成時のパエラメーターで初期値を渡す
struct SampleStruct {
  var age: Int
}

var ss1 = SampleStruct(age: 18)

print("ss1.age = \(ss1.age)")
実行結果
ss1.age = 18

あるいは、

init() に暗黙値を用意する
struct SampleStruct {
  var age: Int
  init(age: Int = 0) { self.age = age }
}

var ss1 = SampleStruct()

print("ss1.age = \(ss1.age)")
実行結果
ss1.age = 0

もしくは、

プロパティの暗黙値を用意する
struct SampleStruct {
  var age: Int = -1
}

var ss1 = SampleStruct()

print("ss1.age = \(ss1.age)")
実行結果
ss1.age = -1

の様に何らかの方法で初期値を与える必要があります。

ところで、このコード片はプロパティー名から年齢を扱っているようです。

ありがちで(マジックナンバーを使うという意味で)ダーティーなコーディングとして、未初期化の場合は -99 のような、年齢としてはありえない値を入れて区別する方法があります。

年齢を扱う場合はそれでも注意深くプログラミングすれば実装可能ですが、数学の問題のようにある範囲を数を稠密に扱いたい場合、このような方法では問題になります。その様な場合はプロパティーをOptional型にすることで問題を解決できます。

プロパティーをOptional型にする
struct SampleStruct {
  var age: Int?
}

var ss1 = SampleStruct()

print("ss1.age = \(ss1.age)")
ss1.age = 100
print("ss1.age = \(ss1.age)")
print("ss1.age! = \(ss1.age!)")

if let n = ss1.age {
  print(n)
} else {
  print("未初期化")
}

ss1.age = nil
if let n = ss1.age {
  print(n)
} else {
  print("未初期化")
}
実行結果
ss1.age = nil
ss1.age = Optional(100)
ss1.age! = 100
100 
未初期化
Optional型のプロパティーは初期化の必要はなく、初期化されない場合の初期値は nil です。
Optional型のプロパティーは、条件式にOptional bind condを使ったif文を使うと未初期化の判定とアンラップが同時に行え便利です。
未初期化が深刻なエラーな場合は、guard文の使用も検討に値します。

未初期化判定のコードを散りばめるのはスタイルとしては歓迎されませんが、-99 の様なマジックナンバーを使う場合も同じ量の判定が必要になるので if letguard let のイディオムで見通しがよくなる分、保守性が向上します(真面目に実装するならアクセサーを用意しその中で未初期化判定を行うべきでしょう)。

クラスと構造体の違い[編集]

クラスは参照型である一方、構造体は値型です。
class SampleClass {
  var age: Int
}

let sc1 = SampleClass(age: 1)
let sc2 = sc1
// sc1とsc2は同じオブジェクトを指す参照であるため、この代入は両方に影響します。
sc1.age = 2
print("sc1.age = \(sc1.age)")
print("sc2.age = \(sc2.age)")  // sc2.age = 2
sc1 も sc2 も let で「定数」として宣言されているにも関わらず、プロパティーの書換えが可能です。
これに対し、let で宣言されたstruct定数のプロパティーは書換えできません。
structは値型なので相互の書換えに影響はない
struct SampleStruct {
  var age: Int
}

var ss1 = SampleStruct(age: 3)
var ss2 = ss1
// ss2はss1からコピーされた別の構造体であるため、この代入はss2には影響しません。
ss1.age = 4
print("ss1.age = \(ss1.age)")  // ss1.age = 4
print("ss2.age = \(ss2.age)")  // ss2.age = 3
メソッドから見るとプロパティはイミュータブル[編集]

メソッドからプロパティの値を変更したい場合は、メソッド定義でmutatingを指定する必要があります。

mutatingなメソッドの例
struct AnotherSampleStruct {
  var age: Int

  mutating func increment() -> Int {
    self.age += 1
    return self.age
  }
}

var ass1 = AnotherSampleStruct(age: 3)
print("ass1.age = \(ass1.age)")  // ass1.age = 3
print(ass1.increment())         // 4
print("ass1.age = \(ass1.age)")  // ass1.age = 4

プロパティ[編集]

ストアド・プロパティ[編集]

プロパティはletまたはvarを使って、ローカル定数/変数と同じように定義できます。
ストアド・プロパティは他言語のインスタンス変数に近いが、遅延初期化や値の監視など、より高機能です。

ストアド・プロパティ
class SomeClass {
  var val1 = 0  // 宣言時に初期化する場合は、型推論により型を省略できる
  var val2: Int  // 宣言時ではなくイニシャライザで初期化する場合は、型を明示する

  let val3 = 0
  let val4: Int

  init() {
    // 宣言時に初期化せずにイニシャライザ内で初期化してもよい
    val2 = 0
    val4 = 0
  }
}
let obj = SomeClass()
print(obj.val1)
obj.val2 = 1
//obj.val3 = 1 // letで宣言したプロパティには再代入不可
遅延ストアド・プロパティ[編集]

ストアド・プロパティは定義の前にlazy修飾子を付けることで、初めてアクセスされる時まで初期化の実行を遅らせることができます。

遅延ストアド・プロパティ
class HeavyClass {
    init() { print("HeavyClassをインスタンス化") }
    func method() { print("HeavyClassのメソッド") }
}

class SomeClass {
    lazy var heavy: HeavyClass = HeavyClass()

    init() { print("SomeClassをインスタンス化") }
    func method() { print("SomeClassのメソッド") }
}

var obj = SomeClass()
obj.method()
obj.heavy.method()  // ←ここで初めてheavyプロパティが初期化される

// 実行結果:
// SomeClassをインスタンス化
// SomeClassのメソッド
// HeavyClassをインスタンス化
// HeavyClassのメソッド
コンピューテッド・プロパティ[編集]

ゲッター/セッターを介してアクセスするプロパティを定義できます。他言語の一般的なプロパティに近い。

コンピューテッド・プロパティ
import Foundation

class Square {
  var sideLength: Double

  var area: Double {
    get {
      return sideLength * sideLength
    }
    set {
      sideLength = sqrt(newValue)
    }
  }

  init(sideLength: Double) {
    self.sideLength = sideLength
  }

  public var description: String {
    return "sideLength=\(self.sideLength)"
  }
}

let sq = Square(sideLength: 5)
println(sq.area)  // 25.0

sq.area = 36
println(sq.sideLength)  // 6.0
型プロパティ[編集]

一つの型のインスタンス全てで共有するプロパティ。他言語でいうクラス変数に近い。

クラスではclassキーワードを、構造体ではstaticキーワードを使って宣言します。

型プロパティ
class MyClass {
    class var computedTypeProperty: Int {  }
}

struct MyStruct {
    static var storedTypeProperty = 42
    static var computedTypeProperty: Int {  }
}
サブスクリプト[編集]

配列と同じように、添字を使って要素にアクセスできます。

サブスクリプト
class CyclicSequence<T> {
  var content: [T]

  init(_ content: [T]) {
    self.content = content
  }

  subscript(index: Int) -> T {
    return content[index % content.count]
  }
}

var cycSeq = CyclicSequence<Int>([1, 4, 2, 8, 5, 7])

for i in 0..<10 {
  let item = cycSeq[i]
  print("cycSeq[\(i)] = \(item)")
}
弱い参照(weak)と非所有参照(unowned)[編集]

いずれも、ARCによって保持が必要な参照としてカウントされない参照。Optional型として宣言するか否かに違いがあります。

弱い参照(weak)と非所有参照(unowned)
import Foundation

class SomeClass {
  weak var wkStr: NSString?  // weak … Optional型として宣言する
  unowned var uoStr: NSString  // unowned … Optional型として宣言できない
  init(str: NSString) {
    wkStr = str
    uoStr = str
  }
}

var obj1 = SomeClass(str: "hello")
print("obj1.wkStr : \(obj1.wkStr)")  // "hello"は保持されないためnil
// print("obj1.uoStr : \(obj1.uoStr)") // 実行時エラー。unownedではnilを表現できない
print("-----")

var str = "hello" as NSString
var obj2 = SomeClass(str: str)
print("obj2.wkStr : \(obj2.wkStr)")
print("obj2.uoStr : \(obj2.uoStr)")
print("-----")

str = "goodbye" as NSString
print("obj2.wkStr : \(obj2.wkStr)")  // 元の値"hello"が解放されたため nil
// print("obj2.uoStr : \(obj2.uoStr)") // 実行時エラー。unownedではnilを表現できない
実行結果
obj1.wkStr : nil
-----
obj2.wkStr : Optional(hello)
obj2.uoStr : hello
-----
obj2.wkStr : nil

イニシャライザ[編集]

指定イニシャライザと簡易イニシャライザ[編集]

初期化が必要なプロパティのすべてが初期化されることを保証するイニシャライザのことを、指定イニシャライザ(designated initializer)と呼ぶ。

それに対して、プロパティの初期化を他のイニシャライザに委譲するイニシャライザを、簡易イニシャライザ(convenience initializer)と呼ぶ。簡易イニシャライザからは最終的に指定イニシャライザが呼び出されるようにしなければなりません。

指定イニシャライザと簡易イニシャライザ
class Color {
  var red, green, blue: Int  // 指定イニシャライザでの初期化が必要

  // 指定イニシャライザ
  init(red: Int, green: Int, blue: Int) {
    self.red = red
    self.green = green
    self.blue = blue
  }

  // 簡易イニシャライザ → 指定イニシャライザ
  convenience init(white: Int) {
    self.init(red: white, green: white, blue: white)
  }

  // 簡易イニシャライザ → 簡易イニシャライザ → 指定イニシャライザ
  convenience init() {
    self.init(white: 0)
  }
}

var brown = Color(red: 0xA5, green: 0x2A, blue: 0x2A)
var gray = Color(white: 0x80)
var black = Color()
全項目イニシャライザ[編集]

構造体の場合のみ、イニシャライザを定義しなかった場合は全てのプロパティ初期化のためのイニシャライザが自動的に実装されます。

全項目イニシャライザ
struct Point {
  var x: Int
  var y: Int
}

var p = Point(x: 2, y: 3)
失敗可能イニシャライザ[編集]
失敗可能イニシャライザ
extension Color {
  convenience init?(name: String) {  // 初期化に失敗しnilを返す可能性があるため、init? とする
    switch name {
    case "black": self.init()
    case "white": self.init(white: 0xFF)
    case "red": self.init(red: 0xFF, green: 0x00, blue: 0x00)
    case "green": self.init(red: 0x00, green: 0xFF, blue: 0x00)
    case "blue": self.init(red: 0x00, green: 0x00, blue: 0xFF)
    default: return nil  // nilはイニシャライザでreturnできる唯一の値です(成功時は何もreturnしません)
    }
  }
}

var color = Color(name: "orange")  // nil

デイニシャライザ[編集]

デイニシャライザ
class Person {
  var name: String

  init(name: String) {
    print("Hello, \(name)!")
    self.name = name
  }

  deinit {
    print("Goodbye, \(name)!")
  }
}

var person = Person(name: "Tom")
person = Person(name: "Mike")
実行結果
Hello, Tom!
Hello, Mike!
Goodbye, Tom!

継承[編集]

継承
class ColoredSquare: Square {
  var color: Color

  init(sideLength: Double, color: Color) {
    self.color = color
    super.init(sideLength: sideLength)
  }

  public override var description: String {
    return
      "sideLength=\(self.sideLength) color=(\(self.color.red), \(self.color.green), \(self.color.blue))"
  }
}

var coloredSquare1 = ColoredSquare(sideLength: 1.0, color: Color(white: 255))
print("coloredSquare1 : \(coloredSquare1.description)")

coloredSquare1.color = Color(red: 255, green: 0, blue: 0)
print("coloredSquare1 : \(coloredSquare1.description)")

var square2 = Square(sideLength: 2.0)
print("square2 : \(square2.description)")
実行結果
coloredSquare1 : sideLength=1.0 color=(255, 255, 255)
coloredSquare1 : sideLength=1.0 color=(255, 0, 0)
square2 : sideLength=2.0
// Square へのアップキャストは必ず成功する
var square1 = coloredSquare1 as Square
print("square1 : \(square1.description)")

// ColoredSquare へのダウンキャストは失敗する可能性があります(この場合は失敗します)
var coloredSquare2 = square2 as? ColoredSquare
print("coloredSquare2 : \(coloredSquare2?.description)")

// coloredSquare1 は元々 ColoredSquare なので、成功する
var coloredSquare1_2 = square1 as? ColoredSquare
print("coloredSquare1_2 : \(coloredSquare1_2?.description)")
実行結果
square1 : sideLength=1.0 color=(255, 0, 0)
coloredSquare2 : nil
coloredSquare1_2 : Optional("sideLength=1.0 color=(255, 0, 0)")

エクステンション(拡張)[編集]

エクステンション(拡張)
// 先述のColorクラスにメソッド追加
extension Color {
  var description: String {
    return String(format: "#%02X%02X%02X", self.red, self.green, self.blue)  // カラーコードを文字列で返す
  }

  func complement() -> Color {  // 補色
    return Color(red: 0xFF - self.red, green: 0xFF - self.green, blue: 0xFF - self.blue)
  }
}

var orange = Color(red: 0xFF, green: 0xA5, blue: 0x00)
print(orange.description)
var navyblue = orange.complement()
print(navyblue.description)
実行結果
#FFA500
#005AFF
// 先述のColorクラスを、Equatableプロトコルに準拠するように拡張
extension Color: Equatable {
  static func == (lhs: Color, rhs: Color) -> Bool {
    return (lhs.red == rhs.red) && (lhs.green == rhs.green) && (lhs.blue == rhs.blue)
  }
}

var black1 = Color()
var black2 = Color(red: 0, green: 0, blue: 0)

if black1 == black2 {
  print("same color")
} else {
  print("different color")
}
実行結果
same color

ジェネリック[編集]

ジェネリックタイプ[編集]

自分用のジェネリックタイプを定義できます。 それには、カスタム、構造体、列挙型等が配列や辞書と同じ様にどんなタイプとでも動く。

ジェネリックタイプ
struct Stack<T> {
  var items = [T]()
  mutating func push(item: T) {
    items.append(item)
  }
  mutating func pop() -> T {
    return items.removeLast()
  }
}
var stackOfStrings = Stack<String>()
stackOfStrings.push(item: "一")
stackOfStrings.push(item: "二")
型束縛[編集]

型パラメータに指定できる型を、プロトコルによって制限する例。

型束縛
struct EquatableStack<T: Equatable> {
  var items = [T]()
  mutating func push(item: T) {
    items.append(item)
  }
  mutating func pop() -> T {
    return items.removeLast()
  }
  func has(item: T) -> Bool {
    return items.contains(item)  // 型TはEquatableプロトコルに適合しているため、containsメソッドが使用可能
  }
}
var stackOfStrings = EquatableStack<String>()
stackOfStrings.push(item: "一")
stackOfStrings.push(item: "二")

// true
print(stackOfStrings.has(item: "一"))
print(stackOfStrings.has(item: "三"))  // false

ジェネリック関数[編集]

以下の例は、自由な型の2つの値を交換する関数です。 Tは型パラメーターで実際にはどんな型でも動く。

ジェネリック関数
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
  let t: T = a
  a = b
  b = t
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)

脚註[編集]

  1. ^ 過去の編集でenumを列挙体としていましたが、これはJISのC言語の用語でCのenumはstruct(構造体)と同じく型そのものではありませんが、Swiftでは型そのものなので列挙型としました。これは公式の日本語ドキュメントでも同じです。
  2. ^ https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md