Optional型 -Swiftでnilを許容する特殊な型-

以前投稿した[Swiftってどんな言語?]の記事で、Swiftはnil(値が存在しないことを表す)の許容度をコントロール可能であるという特徴があると紹介しました。

関連記事

今回はiOSやMacOSなどのApple製品上のアプリを作成する上で外せないプログラミング言語Swiftについて、どんな言語なのかをまとめていきたいと思います。実際の文法などには触れずに言語の特徴などをまとめていきます。 言語の概要[…]

Swiftロゴ

Swiftでは普通に変数・定数を定義するとnilを許容しない設計になっています。
そして、nilを許容する場合はOptional<Wrapped>型を使います。
今回の記事では、このOptional<Wrapped>型についてまとめていきます。

Optional<Wrapped>型とは?

Optional<Wrapped>型とは

Optional<Wrapped>型とは、先ほども説明した通りnilを許容する必要がある場合に利用する型になります。
実際に利用する際は、”Wrapped”の部分を実際に利用する”Int”や”String”に置き換えます。
例えば、String型を扱う場合は”Optional<String>”というようになります。
また、Optional<Wrapped>型にはWrapped?という糖衣構文が用意されているため、上記のString型を扱う場合は”String?”と表記することもできます。

値の存在と不在の2つのケースを持つOptional<Wrapped>型

Optional<Wrapped>型がどのようにnilを許容しているのかを見ていきましょう。
実はOptional<Wrapped>型の中には値の不在(nil)と存在(通常の値)を表す2通りの識別子が列挙子として定義されています。

// Optional<Wrapped>型の内部に定義されている列挙子
enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

noneが値の不在を、someが値の存在をそれぞれ定義しています。
そのためOptional<Wrapped>型のnoneは常にnilを表し、someは任意の型の値を表すことができます。
型として値の不在と存在の2通りの列挙子が定義されているため、Optional<Wrapped>型の変数や定数はnilを許容することができるのです。

Optional<Wrapped>型を利用する

変数・定数定義

Optional<Wrapped>型の変数・定数の定義方法は以下の通りです。

// 変数定義
var a: Optional<String> // String型の値を持つOptional<Wrapped>型の変数
var b: Int? // Int型の値を持つOptional<Wrapped>型の変数 糖衣構文による定義方法

// 定数定義
let c: Optional<Bool> // Bool型の値を持つOptional<Wrapped>型の定数
let d: Int8? // Int8型の値を持つOptional<Wrapped>型の定数 糖衣構文による定義方法

普通の変数・定数定義と同じなので難しくはないかと思います。
型を指定する箇所にOptional<Wrapped>型を指定すれば良いだけです。
糖衣構文を利用することでよりシンプルにコードを書くことができます。

nilの代入

nilを代入する記述方法は以下の通りです。

// nilの代入
var a: String?
a = Optional<String>.none // Optional<Wrapped>型のnoneにアクセスして代入
a = nil // nilリテラルによる代入

nilを代入するにはOptional<Wrapped>型のnoneにアクセスするか、nilリテラルを呼び出すことで実現できます。

some(Wrapped)の値の代入

Optional<Wrapped>型の変数・定数には、Wrappedの部分で指定した型の値を代入することもできます。
記述方法は以下の通りです。

// some(Wrapped)の代入
var a: String?
a = Optional<String>.some("some string") // Optional<Wrapped>型のsomeにアクセスして代入
a = Optional("a string") // Optional型のイニシャライザからsomeを生成
a = "string" // 値をそのまま代入しsomeを生成

var b: Int?
b = Optional(123)
b = 456

some(Wrapped)を生成し代入するには、イニシャライザを利用するか、値のリテラルをそのまま代入することで実現できます。

型推論

Optional<Wrapped>型の型推論はsome(Wrapped)の値をイニシャライザを利用して生成する場合のみ行うことができます。

// some(Wrapped)のイニシャライザによる型推論
let a = Optional("a string") // Optional<String>型
let b = Optional(123) // Optional<Int>型

nilを代入する場合、Optional<Wrapped>型なのは推論可能ですが、Wrapped型が推論できないためコンパイルエラーになります。

// nilの代入は型推論不可
let a = nil // Wrapped型が推論できないためコンパイルエラー

また、リテラルな値をそのまま代入する場合、Optional<Wrapped>型ではなく基本型に型推論が行われるため、Optional<Wrapped>型への型推論はできません。

// リテラルな値の型推論は基本型になる
let a = "String" // String型になる

上記のようにイニシャライザを利用すればOptional<Wrapped>型を型推論することはできますが、そもそもnilを明示的に許容する型として設計されているためコードを書く際も明示的に型を指定した方が良いでしょう。

Optional<Wrapped>型のアンラップ

アンラップとは、Optional<Wrapped>型の変数・定数に格納されている値を取り出すことです。
Optional<Wrapped>型はnilの可能性があるため、そのまま演算などに利用することはできません。

// Optional<Wrapped>型はそのままでは演算に利用できない
let a: Int? = 2
let b: Int? = 4
let c = a + b // コンパイルエラーになる

アンラップの方法として3つ紹介します。

アンラップ方法① オプショナルバインディング

オプショナルバインディングは、条件分岐や繰り返し文の条件にOptional<Wrapped>型の値を指定し、値が存在する場合は分岐や繰り返しの処理に入りその値を利用した処理を行い、nilの場合は処理を行わないことでOptional<Wrapped>型の値の存在を保証するアンラップの方法です。
オプショナルバインディングはif-let文を用いて記述します。
記述方法は以下の通りです。

let optionalA: Int? = 2
let optionalB: Int? = 4
let optionalC: Int? = nil
var sumAandB = 0
var sumAandC = 0
// ifの後にOptional<Wrapped>型の値を指定する
// 複数の値を指定する場合は","で繋ぐ
if let a = optionalA, let b = optionalB {
    // optionalAとoptionalBともに値が存在しているため分岐内の処理を行う
    sumAandB = a + b
}
print(sumAandB) // 6

if let a = optionalA, let c = optionalC {
    // optionalCがnilなので分岐内には入らず処理を行わない
    sumAandC = a + c
}
print(sumAandC) // 0

アンラップ方法② ??演算子

??演算子は中値演算子といい、Optional<Wrapped>型の値がnilの場合のデフォルト値を指定することができます。
??演算子は左辺にOptional<Wrapped>型の値を、右辺にデフォルト値とするWrapped型の値を取ります。

let optionalA: Int? = 2
let optionalB: Int? = 4
let optionalC: Int? = nil
var sumAandB = 0
var sumAandC = 0

// optionalA・optionalBがnilの場合は0として演算する
// この場合は両方ともnilではないのでそれぞれの値で演算が行われる
sumAandB = (optionalA ?? 0) + (optionalB ?? 0)
print(sumAandB) // 6

// optionalA・optionalBがnilの場合は0として演算する
// この場合はoptionalCのみnilなので0となる
sumAandC = (optionalA ?? 0) + (optionalC ?? 0)
print(sumAandC) // 2

ちなみに、??演算子は算術演算子より優先される、というようなことはないので上記の例のように()で囲まないとコンパイルエラーになります。

アンラップ方法③ 強制アンラップ

最後に強制アンラップを紹介します。
強制アンラップは!演算子をOptional<Wrapped>型の右側に付けることで実現できます。
その名の通り、値がnilかなどを考慮することなく強制的にOptional<Wrapped>型の値をアンラップすることができ、コードを記述する際にコンパイルエラーなどは発生しません。
しかし、値がnilの場合にはランタイムエラー(実行中に発生する例外)が発生してしまうため、実質的にエラーチェックをコンパイル時から実行時に先延ばしにしている状態となってしまいます。
Swiftの設計から考えても、よほどnilでないことが明確出ない限りは使うべきではないでしょう。

let optionalA: Int? = 2
let optionalB: Int? = 4
let optionalC: Int? = nil
var sumAandB = 0
var sumAandC = 0

// Optional<Wrapped>型を強制的にアンラップする
sumAandB = optionalA! + optionalB!
print(sumAandB) // 6

// optionalCはnilだが強制アンラップではコンパイルエラーにならない
sumAandC = optionalA! + optionalC! // 実行時にエラーになる
print(sumAandC)

オプショナルチェイン

オプショナルチェインとは、アンラップを伴わずにWrapped型のメンバにアクセスする記法のことです。
Wrapped型のメンバにアクセスする場合、上記のアンラップを行ってからアクセスする必要があります。
オプショナルチェイン記法を用いると、nilの場合はnilを返却して値が存在する場合のみメンバにアクセスする処理を簡潔に記述することができます。
オプショナルチェイン記法は、メンバにアクセスする値の後ろに?演算子を記述し、続けてメンバへのアクセスを記述します。

let optionalA: Int? = 2
// 値が存在するためメンバへのアクセスが実施される
let distanceA = optionalA?.distance(to: 3)
print(distanceA) // Optional(1)

let optionalB: Int? = nil
// nilなのでメンバへのアクセスは実施されずにnilを返却
let distanceB = optionalB?.distance(to: 3)
print(distanceB) // nil

map(_:)メソッドとflatMap(_:)メソッド

map(_:)メソッドとflatMap(_:)メソッドはOptional<Wrapped型>のメソッドで、アンラップを伴わずに値を変更することができます。
map(_:)メソッドとflatMap(_:)メソッド両方とも引数には値を変換するクロージャ(処理のまとまりのようなもの)を渡し、値が存在する場合はクロージャの処理を実行して値の変換を行い、nilの場合は何も行いません。
map(_:)メソッドとflatMap(_:)メソッドの違いは、map(_:)メソッドはクロージャで返却される値をOptional<Wrapped>型の<Wrapped>型の部分にそのまま代入するのに対して、flatMap(_:)メソッドはクロージャで返却される値がOptional<Wrapped>型である場合、Optional<Wrapped>型が二重にならないように1つにまとめてくれるようになっています。

let intValue: Int? = 2
let stringValue: String? = "2"
let nilValue: Int? = nil

// intValueの値を2倍するクロージャを引数で渡す
let a = intValue.map({value in value * 2})
print(a) // Optional(4)
let b = intValue.flatMap({value in value * 2})
print(b) // Optional(4)

// nilの場合はクロージャは実施されない
let c = nilValue.map({value in value * 2})
print(c) // nil
let d = nilValue.flatMap({value in value * 2})
print(d) // nil

// stringValueの型をInt型に変換するクロージャを引数で渡す
let e = stringValue.map({value in Int(value)})
print(e) // Optional(Optional(2))
let f = stringValue.flatMap({value in Int(value)})
print(f) // Optional(2)

暗黙的なアンラップ

Optional<Wrapped>型には?を利用した糖衣構文がありましたが、実は!を利用した糖衣構文もあります。
?でも!でもOptional<Wrapped>型の値を扱うことになるため、どちらで宣言した変数・定数も互いに代入することが可能です。
!で宣言した値は値へのアクセス時に強制アンラップを行うという特徴があります。
強制的なアンラップが必ず行われるため、暗黙的にアンラップが行われるOptional<Wrapped>型と言えます。

let a: Int? = 2
let b: Int! = 3
let c: Int! = nil

// aは?で宣言されているのでアンラップが必要
if let unWrappedA = a {
    print(unWrappedA) // 2
    print(unWrappedA * 2) // 4
}

// bは!で宣言されているためアンラップは不要
print(b) // Optional(3)
print(b * 2) // 6

// cのようにnilの場合、暗黙的なアンラップでは実行時エラーになる可能性がある
print(c) // nil
print(c * 2) // 実行時エラー発生

暗黙的なアンラップも強制アンラップと同様にnilの場合、実行時エラーが発生します。
強制アンラップの利用する時と同じように、nilでないことが明らかな場合やnilの場合に実行時エラーを発生させたい場合など、適した場所でのみ利用するのが望ましいです。

まとめ

今回はSwiftの特徴である「値のnilの許容度をコントロール可能である」ということを体現するOptional<Wrapped>型についてまとめてきました。
nilを許容する値をOptional<Wrapped>型という特殊な形で表現することで、コンパイル時に未然にエラーを発見できるようになっており、安全性の高いコードを書くことができます。
その分、アンラップなど記述する必要があるなど、コードの量の増加や記述方法のパターンが多くどのようにコードで表現するかなどを考える必要が出てくるので、コーディング中の機能の設計やSwiftnの思想などをしっかりと把握した上でコードを書いていきたいですね。
ここまで読んでいただき、ありがとうございます。
それでは、また。