構造体とクラスは、Swiftプログラミングの主要な構成要素の2つです。どちらも、関連するデータと関数をまとめて、自己完結した機能の単位にするための仕組みです。そして、構造体やクラスを定義することは、Swiftの中で使う新しい型を定義することです。これまでに扱ってきたIntやStringのような型を使うのと同じことです。
Swiftの構造体とクラスには、多くの共通点があります。いくつもの共通点のなかでも、どちらも値を_プロパティ_に保存でき、_メソッド_を通じて機能を提供できます。
それぞれに追加の機能もありますが、それはこの演習の範囲外です。
構造体もクラスも、ほぼ同じ方法で定義します。まず適切なキーワードを書き、その後に定義する型の名前を続けます。そして構造体やクラスの本体を、波括弧の間に書きます。
本体には格納プロパティを書くことができます。格納プロパティは、通常の定数や変数と同じように定義し、同じように振る舞います。
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id = makeRandomID()
}
前述のとおり、構造体やクラスを定義することは、新しい_型_を定義することにすぎません。それはその型の値がどんな形になるかを示す設計図にすぎず、実際にその型の値を生成して使えるようにするものではありません。
その型の_インスタンス_を生成するには、型の名前を書き、その後に括弧(())を続けます。
let someStats = CharacterStats()
let someCharacter = GameCharacter()
こうすると、これらの型の値が生成され、プロパティには定義で指定したデフォルト値が設定されます。GameCharacterのnameプロパティのようなオプショナルの場合は、値を指定しない限り、プロパティはデフォルトでnilになります。これは、値をすぐに指定しない通常のオプショナル型を定義するときと同じです。
構造体では、Swiftが自動的に_メンバーワイズイニシャライザー_を提供します。これを使うと、括弧の中に構造体のプロパティの値を指定でき、それが定義内のデフォルト値を上書きします。
let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)
構造体やクラスのプロパティには、_ドット記法_を使ってアクセスできます。値の名前の後にドット(.)とプロパティの名前を書きます。ある構造体やクラスのプロパティがさらに自分のプロパティを持っている場合は、このドット記法でそうした入れ子のプロパティにもアクセスできます。
この記法は、プロパティの値を取得するのにも、許可されている場合は変更するのにも使えます。
someStats.health
// => 0
someCharacter.name
// => nil
someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"
someCharacter.id = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant
構造体やクラスにデータを保存するプロパティと同様に、構造体やクラスに関数を保存する_メソッド_も定義できます。
メソッドは通常の関数と同じ方法で定義しますが、構造体やクラスの本体の内部に書く点だけが違います。構造体のプロパティの値を変更する関数には、先頭にmutatingキーワードを付ける必要があります。さらに、メソッドから変更できるプロパティは、通常の変数と同じように、letではなくvarを使って定義しなければなりません。
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
mutating func takeHit(_ damage: Double) {
health = max(0.0, health - damage)
}
func canLift(_ weight: Int) -> Bool {
weight < strength * 100
}
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id: String = makeRandomID()
func takesDamage(_ damage: Double) {
stats.takeHit(damage)
if stats.health <= 0 {
active = false
}
}
func sayName() -> String {
return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
}
func lift(_ weight: Int) -> String {
if stats.canLift(weight) {
return "No problem!"
} else {
return "Ooof! No way."
}
}
}
こうしたメソッドは、プロパティと同じようにドット記法で呼び出せます。
var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true
myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false
構造体やクラスのすべてのインスタンスには、selfという暗黙のプロパティがあり、これはインスタンス自身を指します。
selfの最も一般的な用途は、インスタンスプロパティと、同じ名前を持つ仮引数とを区別することです。
struct Position {
var x = 0
mutating func move(x: Int) {
// Here, 'self.x' refers to the struct's property,
// while 'x' refers to the parameter passed to the method.
self.x = x
}
}
Swiftの型には、大文字で始まるSelfキーワードもあり、これはインスタンス自身ではなく、そのインスタンスの型を指します。
この演習では、ウィンドウシステムを備えたコンピューターをシミュレーションします。 移動したりサイズを変更したりできるウィンドウを作成し、その内容を表示させます。 次の図は、このあと扱う値を示したものです。
<--------------------- screenSize.width --------------------->
^ ╔════════════════════════════════════════════════════════════╗
| ║ ║
| ║ position.x,_ ║
| ║ position.y \ ║
| ║ \<----- size.width -----> ║
| ║ ^ *──────────────────────┐ ║
| ║ | │ title │ ║
| ║ | ├──────────────────────┤ ║
screenSize.height ║ | │ │ ║
| ║ size.height │ │ ║
| ║ | │ contents │ ║
| ║ | │ │ ║
| ║ | │ │ ║
| ║ v └──────────────────────┘ ║
| ║ ║
| ║ ║
v ╚════════════════════════════════════════════════════════════╝
Sizeという名前の構造体を定義し、ウィンドウの現在の幅と高さを格納する2つのIntプロパティ、widthとheightを持たせましょう。
幅と高さの初期値は、それぞれ80と60にします。
新しい幅と高さをパラメータとして受け取り、プロパティを新しいサイズに更新するメソッドresize(newWidth:newHeight:)も用意しましょう。
let size1080x764 = Size(width: 1080, height: 764)
// Returns Size
var size1200x800 = size1080x764
// Returns Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// Returns 800
Positionという名前の構造体を定義し、ウィンドウの左上隅の現在の水平位置と垂直位置を格納する2つのIntプロパティ、xとyを持たせましょう。
xとyの初期値は、それぞれ0にします。
位置(0, 0)は画面の左上隅で、右へ移動するほどxの値が大きくなり、下へ移動するほどyの値が大きくなります。
新しいxとyをパラメータとして受け取り、プロパティを新しい位置に更新するメソッドmoveTo(newX:newY:)も用意しましょう。
var point = Position(x: 10, y: 20)
// Returns Position
point.moveTo(newX: 100, newY: -100)
point.y
// Returns -100
次のプロパティを持つウィンドウクラスを定義しましょう。
title : String、初期値は"New Window"screenSize : Size、width = 800、height = 600の定数値size : Size、初期値はSize構造体のデフォルト値position : Position、初期値はPosition構造体のデフォルト値contents : String?、初期値はnil
このクラスのために、空のイニシャライザも定義しましょう。
let window = Window()
window.title
// Returns "New Window"
resize(to:) : (Size) -> () - このメソッドはSize構造体を入力として受け取り、ウィンドウを指定されたサイズに変更しようとします。ただし、新しいサイズは一定の範囲を超えることはできません。x = 400、y = 300で、height = 400、width = 300へのサイズ変更が要求された場合、y方向に画面が十分な大きさでないため、ウィンドウはheight = 300、width = 300に変更されます。move(to:) : (Position) -> () - これはresize(to:)と似ていますが、サイズではなく、ウィンドウの_位置_を要求された値に調整するメソッドです。resizeと同様に、新しい位置も一定の範囲を超えることはできません。xとyのどちらも0です。x = 250、y = 100で、x = 600、y = 200への移動が要求された場合、x方向に画面が十分な大きさでないため、ウィンドウはx = 550、y = 200に移動されます。update(title:) : (String) -> () - このメソッドは、渡された文字列の値をtitleプロパティに設定します。update(text:) : (String?) -> () - このメソッドは、渡されたオプショナルな文字列の値をcontentsプロパティに設定します。display() : () -> String - このメソッドは、ウィンドウの現在の状態を表す文字列を返します。たとえば、ウィンドウのtitleが"My First Window"、位置がx = 10、y = 100、サイズがwidth = 200、height = 150、contentsが"I 😍 my window"の場合、次の文字列を返します。"My First Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n" - contentsがnilの場合、最後の行は"[This window intentionally left blank]"となります。ウィンドウシステムには、独自の入力でウィンドウを作成できるイニシャライザが必要です。
Windowクラスにもう1つイニシャライザを作成しましょう。
このイニシャライザは、title、contents、そして2つのオプショナルなパラメータsizeとpositionを取ります。
sizeとpositionパラメータのデフォルト値は、それぞれSize構造体とPosition構造体のデフォルト値にします。
let window = Window(title: "My First Window", contents: "I 😍 my window")
window.display()
// Returns "My First Window\nPosition: (0, 0), Size: (80 x 60)\nI 😍 my window\n"
let window2 = Window(title: "My Second Window", contents: "I 😍 my window", size: Size(width: 200, height: 150), position: Position(x: 10, y: 100))
window2.display()
// Returns "My Second Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"