Swift
Swift4.2

【Swift 4.2】Implicitly Unwrapped Optional(IUO)が再実装されたらしいので Xcode で試した

はじめに

Swift.org の Reimplementation of Implicitly Unwrapped Optionals によると、Swift 4.2 で implicitly unwrapped optionals (IUOs) を再実装したみたいです。

  • !ImplicitlyUnwrappedOptional ではなく Optional の糖衣構文に変更
  • 型としての ImplicitlyUnwrappedOptional は廃止
  • ! で宣言したオプショナル型は、暗黙的にアンラップできる

暗黙的にアンラップができるオプショナル型は Swift の型として表現されていましたが、これをやめて(型としては Optional で統一してしまって)型の末尾に ! を付けたオプショナル型は『暗黙的にアンラップができる』という属性が付いているオプショナル型とするみたいです。

普段色々なところで使っているオプショナル型の変更なので Xcode で実際に試してみました。

本記事で扱わないこと

変更点や既存コードへの影響以外のことは扱わないことにします。例えば下記の内容です。

  • なぜ IUO を型から属性に変更したの?
  • IUO の使いどころ
  • IUO ポエム

Swift 3.0 で IUO を属性にしたのでは?

ところで、IUO を属性とする仕様は Swift 3.0 の時点で実装されたものとカン違いしていました。というのも、Evolution proposals included in Swift 3.0 の実装済みプロポーザルに SE-0054(Abolish ImplicitlyUnwrappedOptional type) が含まれているからです。

実際は Swift 3.0 から Swift 4.2 で段階的に実装したみたいです。1
3.0 で『IUOを属性としたときのSwiftの書き方』を実装しておき、4.2 で ImplicitlyUnwrappedOptional 型を廃止したようです。

属性としての IUO

『IUOを属性としたときのSwiftの書き方』とは型の宣言時に IUO 属性をつけるスタイルです。イメージとしてはこんな具合です。

@_autounwrapped var foo: Int? // イメージを掴むための疑似コード

このように特別なオプショナル型として宣言した場合に、暗黙的にアンラップできるようにしたいのです。

IUO が型ではなく属性ならば、ImplicitlyUnwrappedOptional 型は不要です。この型を廃止して !Optional 型の糖衣構文としてしまえば、擬似コードは下記のコードで実現できます。

var foo: Int! // IUO属性付きの Int? 型

このような宣言方法にすれば、IUO属性付きのオプショナル型は、いままでの ImplicitlyUnwrappedOptional 型の宣言と全く同様です。

これを実現するために Swift 3.0 では、! の使い所を見直しました。
下記の場所でのみ ! を使用可能にして、それ以外の用途で使用した場合に警告を出すようにしました。

  • 変数(プロパティ)
  • 関数(メソッド)の引数・戻り値
  • 失敗可能なイニシャライザ

また、Swift 3.0 では IUO を型ではなく、あたかも属性のように見せるために、型推論時に『暗黙的なアンラップが可能である』という情報を落として Optional 型に型推論するようにしました。

func maybeNone() -> Int! { return 1 }
let foo = maybeNone() // Int? 型に推論(Swift 2.2 までは Int! 型に推論)
print("\(maybeNone())") // Optional(1) を出力(Swift 2.2 までは 1 を出力)

このようにして Swift 3.0 以降は型としての IUO は残っていたものの、
IUOを属性のように見せることに成功しました。

型としての IUO は廃止予定のままだった

しかし、ImplicitlyUnwrappedOptional 型は Swift 4.1 まで廃止されず、!ImplicitlyUnwrappedOptional 型の糖衣構文のままでした。

Swift 4.1 では、下記のようなコードを実行することで確認できます。

// Optional<Any> の id
print(Int(bitPattern: ObjectIdentifier(Optional<Any>.self)))

// Optional<Any> と同じ id
print(Int(bitPattern: ObjectIdentifier(Any?.self))) 

// Optional<Any> と異なる id
print(Int(bitPattern: ObjectIdentifier(ImplicitlyUnwrappedOptional<Any>.self)))

// ImplicitlyUnwrappedOptional<Any> と同じ id
print(Int(bitPattern: ObjectIdentifier(Any!.self)))

// Optional<ImplicitlyUnwrappedOptional<Int>>
print(type(of: (Int!?).none))

Swift 4.2 でついに ImplicitlyUnwrappedOptional 型が廃止されて、! は IUO属性付きの Optional 型の糖衣構文になりました。

同様のコードで Swift 4.2 の変更内容を確認できます。

// Optional<Any> の id
print(Int(bitPattern: ObjectIdentifier(Optional<Any>.self)))

// Optional<Any> と同じ id
print(Int(bitPattern: ObjectIdentifier(Any?.self)))

// Optional<Any> と同じ id
print(Int(bitPattern: ObjectIdentifier(Any!.self)))

// コンパイルエラー
// print(Int(bitPattern: ObjectIdentifier(ImplicitlyUnwrappedOptional<Any>.self)))

// Optional<Optional<Int>>
print(type(of: (Int!?).none))

Swift 4.2 を試す準備

Swift 3.0 以降のプロジェクトで警告を解決していれば修正はあまりないかな?と思いつつも、Xcode を使って試してみました。

通常、Xcode 9.3 の Swiftのバージョンは 4.1(または 3.3)です。バージョン 4.2 を試すため、ツールチェーンを Xcode 標準のものから Swift 4.2 Snapshot に切り替えます。

その手順を示します。

ツールチェーンのインストール

Swift.org からダウンロードします。

ダウンロードしたファイルでツールチェーンをインストールします。

インストーラの指示に従ってインストールします。

スクリーンショット 2018-04-29 14.35.02.png

ツールチェーンの切り替え

Xcode を開いて、メニューバーから [Xcode] → [Preferences...] の順に選択して環境設定を開きます。
環境設定で Components、Toolchains とタブを切り替えて、
インストールしたツールチェーンを選択します。

スクリーンショット 2018-04-29 14.45.13.png

これで Xcode 9.3 を使って、Swift 4.2 の実装済みの機能を試すことが出来ます。
不要になったツールチェーンは削除することも可能です2

Swift 4.2 を試した

! が使える箇所

型の末尾に ! をつけることで、オプショナル型にIUO属性をつけることが出来ます。
IUO属性付けられるのは下記の場所です。

  • 変数(プロパティ)
  • 関数(メソッド)の引数・戻り値
  • 失敗可能なイニシャライザ
let bar: Int! = 3 // IUO 属性付きの Optional<Int>.some(3)

このように宣言をした場合、Swift 4.1 の ImplicitlyUnwrappedOptional 型と同様に、
メソッドやプロパティの呼び出し、関数の引数として渡す場合は、明示的なアンラップが不要になります。

bar.signum() // 1
bar.byteSwapped // 216172782113783808
Double(bar) // 3.0

もし barOptional.none が代入されている場合はプログラムは停止します。
この振る舞いも ImplicitlyUnwrappedOptional 型と同じです。

! が使えない箇所

前述以外の箇所で使用すると警告がでます。
基本的に Swift 4.1 と同様です。

as Int!

func maybeNone() -> Int! { ... }
let foo = maybeNone() as Int! // Using '!' here is deprecated and will be removed in a future release

上述の例は型推論によって下記と同様になります。

let foo: Int? = maybeNone() as Int!

as Int! という表現は無意味ですので、警告を除去するには、as Int! の除去します。
また、意図に合わせて宣言部分も修正も必要です。

let foo: Int = maybeNone() // 関数が .none を返すと停止する
let foo: Int? = maybeNone() // 関数が .none を返しても停止しない、Optional<Int> 
let foo: Int! = maybeNone() // 関数が .none を返しても停止しない、Optional<Int>(利用するときは暗黙的なアンラップが可能) 
let foo = maybeNone() // 型推論されて foo は Int? 型

関数から取得した結果を IUO 属性付きで扱いたい場合、型推論させずに明示的に型の宣言が必要ということですね。

その他

上述以外にも、! をあたかも型のように記述している場合は警告が出ます。

/* 警告が出る! */
let arr: [Int!] = [maybeNone()]
let fn: (Int!) -> Int! = { $0 }
let type = Int!.self

これらは ! を意図に合わせて ? に置き換えるか、除去します。

既存コードへ影響

既存コードへの影響が全くないかと言うと、そうでもなさそうです。Swift 4.2 に変更すると T!Optional<T> 型に変換されます。それに依る既存のコードに影響が出る可能性があります。

map

Swift 4.1 における ImplicitlyUnwrappedOptional 型の定義は下記です。

public enum ImplicitlyUnwrappedOptional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)

    public init(_ some: Wrapped)
    public init(nilLiteral: ())
}

Swift 4.1 では Optional 型にはある map メソッドもありません。

Swift 4.1 から Swift 4.2 に変更すると ImplicitlyUnwrappedOptionalOptional に変換されるので、
map メソッドが呼べて、下記のコードは Swift 4.2 からコンパイルが通ります。

let foo: Int! = 1
let bar = foo.map { $0.signum() } // こんな使い方は、実際のプロダクトではしない。

そして、問題になるのは下記のようなコードです。
下記のコードは Swift 4.1 から実行できて、Swift 4.2 で実行時エラーになります。

let foo: [Any]! = [1]
let bar = foo.map { $0 as! Int } // エラー

Swift 4.1 には ImplicitlyUnwrappedOptional 型に map がないため、まず暗黙的にアンラップされて、 [Any] 型の map が呼び出されます。

一方で、Swift 4.2 から [Any]!Optional<[Any]> 型のため、Optionalmap を呼ぼうとします。
そうすると、$0[Any] 型のため、Int型へキャスト出来ず、実行時にエラーになります。

暗黙的なアンラップがされないことが原因のエラーですので、解決策としては、 foo! または ? で明示的にアンラップすることになります。

let foo: [Any]! = [1]
let bar = foo!.map { $0 as! Int } // アンラップしたので Swift 4.2 でもOK

[Any] ではなく、 具体的な型を指定した場合は
型推論によって Array の map を呼び出すことが出来る場合があります。

let foo: [Int]! = [1]
let bar = foo.map { $0.signum() } // 型推論によって Array の map が呼ばれる
let bar = foo!.map { $0 } // アンラップ後に [Int] の map が呼ばれて bar は [Int] 型
let bar = foo.map { $0 } // [Int]! の map が呼ばれて bar は [Int]? 型

AnyObject ルックアップ

AnyObject 型の変数に代入したオブジェクトは、@objc 属性の付いたプロパティやメソッドを IUO で取得することが出来きます。 3

class Cls: NSObject {
    @objc var p: Int = 0
}

let object: AnyObject = ...
let i: Int = object.p // 暗黙的にアンラップ
let j = object.p // 型推論で Int? 型

@IBOutlet@IBAction も暗黙的に @objc 属性が追加されるので、
これらのプロパティも同様に AnyObject 型のオブジェクトからアクセスすることができます。
プロパティに ! が付いている場合、Swift 4.2 からは 2重に IUO 属性がついた Optional 型になります。

class ViewController: UIViewController {
    @IBOutlet private weak var property: UILabel! // IUO 属性が付いた Optional 型
}

let object: AnyObject = ...
let foo: UILabel = object.property // 2回暗黙的にアンラップされて foo に代入
let bar = object.property // 型推論で IUO 属性のない UILabel?? 型の bar に代入

2重の Optional 型になったことで問題になるのは
if letguard let のようなオプショナルバインディング構文です。

下記のようなオプショナルバインディングは1段階のアンラップしかしません。
また、 バインディング先の変数には IUO 属性がつきません。

if let label = a.property { // 一度アンラップされて、 IUO属性のない UILabel? にバインディング
    print(label.text) // コンパイルエラー
}

下記のように 宣言時に型を指定 すれば、暗黙的にアンラップさせることができます。

if let label: UILabel = a.property {
    print(label.text) // OK
}

try?

関数が IUO を返す場合は、try? も2重の Optional 型です。
オプショナルバインディングで型を指定することになります。

func test() throws -> Int! { ... }

if let x = try? test() { // エラーがスローされない場合は、IUO属性のない Int? にバインディング
    let y: Int = x // コンパイルエラー
}

if let x: Int = try? test() { // エラーがスローされない場合は、Int にバインディング
    let y: Int = x // OK
}

switch

switch でバインディングした変数の型に変更があります。

func foo(input: String!) -> String {
    switch input {
    case let output:
        return output  // コンパイルエラー
    }
}

Swift 4.1 では型推論で String! にバインディングされます。
更に return で暗黙的に String にアンラップされてコンパイル可能です。

Swift 4.2 では型推論で IUO 属性のない String? にバインディングされます。
return で暗黙的にアンラップできなくなりコンパイルエラーです。
強制アンラップするか、 case のパターンを修正します。

func foo(input: String!) -> String {
    switch input {
    case let output?:
        return output
    case .none:
        return "アンラップ失敗時の代わりの値"
    }
}

Swift 4.1 で暗黙的にアンラップして none の場合を無視しているので、
強制アンラップした方が、もともとのコードの意図に近いかもしれません。

func foo(input: String!) -> String {
    switch input {
    case let output:
        return output! // 暗黙アンラップできなくなったので強制アンラップする
    }
}

また、Swift 4.2 からは ! で宣言しても IUO の属性のないオプショナル型の switch と変わらないので、
意図する内容にあわせて、先に StringString? の変数に代入した後に switch しても良いかもしれません。

// none の考慮が不要であればオプショナル型である必要はない
func foo(input: String) -> String {
    switch input {
    case let output:
        return output
    }
}
// none を考慮する必要があれば IUO 属性は不要のはず 
func foo(input: String?) -> String {
    switch input {
    case let output?:
        return output
    case .none:
        return "アンラップ失敗時の代わりの値"
    }
}

メソッドのオーバーロード

?! でオーバーロードしている場合、
Swift 4.2 からは型として同一のためコンパイルエラーになります。

/* Swift 4.2 からコンパイルエラー */
func bar(a: inout Int?) {}
func bar(a: inout Int!) {}

/* Swift 4.2 からコンパイルエラー */
func foo(a: Int??) {}
func foo(a: Int!!) {}

ちなみに、下記のようなコードは Swift 4.1 でもコンパイルエラーになります。

func foo(a: Int?) {}
func foo(a: Int!) {}

まとめ

  • Implicitly Unwrapped Optional(IUO)が Swift4.2 で再実装された
    • ImplicitlyUnwrappedOptional 型は廃止
    • IUO はオプショナル型の属性に変更
  • Swift 3.0 で部分的に実装は済んでいた
    • 属性としたときの書き方が出来ていない場合に警告を出すようになった
    • 事前に警告を除去していれば、Swift4.2 は大きな変更にはならない
  • ! は型としては Optional 型に変更
    • 既存コードへの影響はあるかもしれない

参考