暗黙開封された Optionals の再実装


2018年4月26日 Mark Lacey



暗黙開封されたoptionals(IUO)の新しい実装は、今年初めに Swift コンパイラに上陸し、最近の Swift の スナップショット で試すことができます。これにより、SE-0054 - 暗黙開封される Optional 型を廃止 の実装が完了します。これは型チェックの不一致をなくし、一貫性があり、推論しやすいようにこれらの値をどのように扱うべきかのルールを明確にした言語にとって重要な変更です。詳細は、その提案の 動機付けのセクション を参照してください。


主な変更点は、診断で T! ではなく T? が印刷されることです。基になる型 T で暗黙開封された optional として宣言された値を参照するときにこれは発生します。コンパイルが正常に行われる前にコードを変更する必要があるソース互換性の問題が発生することがあります。


暗黙開封は宣言の一部です


暗黙に開封された optionals は、式がコンパイルされるために必要に応じて自動的に開封される optionals です。暗黙に開封された optional を宣言するには、型名の後に よりも ! を書きます。


暗黙に開封された optionals に多くの人が感じる精神モデルは、それらが型であり、通常の optionals とは異なります。Swift 3 では、それは正確にそのように働いきました。var a: Int? のような宣言は、Optional<Int> 型を持つ a となり、var b: String! のような宣言は、ImplicitlyUnwrappedOptional<String> 型の b になります。


IUO の新しい精神モデルは、宣言にフラグを追加して、宣言された値を暗黙開封できることをコンパイラーに知らせる、追加の ? と同じ意味の ! とあなたが考慮できます。


つまり、String! を、「この値には Optional<String> 型があり、必要に応じて暗黙開封できるという情報も含まれています" と読めるという意味です。


この精神モデルは新しい実装に適合します。T! を書くと誰でも、コンパイラーはそれを T? があるとみなし、型チェッカーに、必要に応じて暗黙的に値を開封できることを知らせるために、その宣言の内部表現にフラグを追加します。


この変更の最も目に見える結果は、今や T? について T! で宣言された値の T! よりむしろ話す診断が見れることです。T! よりむしろ T? での診断を見るのにはちょっと慣れが必要ですが、この新しい精神モデルを採用することはあなたを助けます。


ソース互換性


ほとんどのプロジェクトは互換性の問題にぶつかることなくビルドする必要があります。しかし、これらの実装の変更により、SE-0054 と一貫性がありますが、以前のリリースのコンパイラとは矛盾する動作に変更される可能性があります。


T! への強制


as T! の形式の強制は SE-0054 で許可されなくなりました。


Swift 4.1 では、これらの強制に対する廃止警告があります。多くの場合、as T?T! と置き換えて、または単に強制的に削除すると、コンパイルに成功します。


これらの 2 つの変更のうちの 1 つを使用して既存のコードをコンパイルできなかった場合、新しい実装でこれを処理する特別な場合があります。具体的には、x as T! と書くと、コンパイラはまずこれを x as T? として型チェックしようとします。それが失敗した場合にのみ、コンパイラは型を (x as T?)! としてチェックして、optional を強制します。


しかし、この形式の強制はやはり廃止とみなされ、この特別な処理は Swift の将来のバージョンでは削除される可能性があります。


宣言よりも型で ! の使用


T! への強制は、より一般的な問題の特別なケースです:型の一部として ! を使用します。


! の使用場所は 3 箇所あり、型の一部として許可されています:


  1. プロパティの宣言
  2. 関数宣言のパラメータ
  3. 関数宣言の戻り値

他の場所では、! はエラーとしてフラグを立てられ、Swift 4.1 以前のリリースはそうしようとしますが、いくつかのケースではミスしました:


let fn: (Int!) -> Int! = ...   // error: not a function declaration!


Swift 4.1 では、これらのシナリオでは、廃止する警告を発行しますが、引き続き暗黙の開封の動作を尊重します。最近のスナップショットの新しい実装では、! は、それが ? であったかのように取り扱い、何が起きているのか、! を使っている場所は廃止されるのを伝える診断を発します。


暗黙に開封された optional として宣言された値への map の呼び出し


以前は以下のようにコードしていました:


class C {}
let values: [Any]! = [C()]
let transformed = values.map { $0 as! C }


これは 強制開封の values が生じ、配列上で map(_:) を呼び出すことになりました。これは、 ImplicitlyUnwrappedOptional の拡張機能でのメンバー map(_:) をあなたが定義した場合でも正しく、ImplicitlyUnwrappedOptional へのメンバールックアップが期待通りに機能しなかったためでした。


新しい実装では、!? の同義語であるため、コンパイラは Optional<T>map(_:) をここで呼び出しようとします:


let transformed = values.map { $0 as! C } // calls Optional.map; $0 has type [Any]


そして warning: cast from '[Any]' to unrelated type 'C' always fails   (警告:'[Any]'から非関連型 'C' へのキャストは常に失敗します) を生成します。


これは技術的に、型チェックに合格するため、値を強制開封しようとはしません。


optional の連鎖を使用して optional の配列を生成することで、この問題を回避できます。


let transformed = values?.map { $0 as! C } // transformed has type Optional<[C]>


または values を強制開封して配列を生成して回避できます。


let transformed = values!.map { $0 as! C } // transformed has type [C]


多くの場合、動作の変更は表示されません。


let values: [Int]! = [1]
let transformed = values.map { $0 + 1 }


Optionalmap(_:) を呼び出すと、式を正しく型チェックする方法がないため、以前と同じように動作します。代わりに、最終的に values を強制開封し、結果の配列に map(_:) を呼び出します。


型ではない型は推論できません


暗黙に開封された optiolals はもはや optionals とは異なる型ではないので、型として、または何らかの型の一部として推論することはできません。


以下の例では、代入の右辺に暗黙に開封されて宣言された値が含まれていますが、左辺の推論型は値 (または戻り値) が optional であることを示しています。


var x: Int!
let y = x   // y has type Int?

func forcedResult() -> Int! { ... }
let getValue = forcedResult    // getValue has type () -> Int?

func id<T>(_ value: T) -> T { return value }
let z = id(x)   // z has type Int?

func apply<T>(_ fn: () -> T) -> T { return fn() }
let w: Int = apply(forcedResult)    // fails, because apply() returns unforced Int?


この動作の変化にあなたは気付くだろう、特定のいくつかのインスタンスは、AnyObject ルックアップ、try?、および switch にあります。


AnyObject ルックアップ


AnyObject のルックアップの結果は、暗黙開封されている optional として扱われることに注意してください。暗黙開封されていると宣言されているプロパティをルックアップすると、式に暗黙開封の 2 つのレベルが現れます (propertyUILabel として宣言されます)。


func getLabel(object: AnyObject) -> UILabel {
  return object.property // forces both optionals, resulting in a UILabel
}


if letguard let は、単一レベルの optional を開封させるだけです。


以下の例では、以前のバージョンの Swift は label の型は UILabel! であると、if let の optional の 1 つのレベルを開封した後、推測しました。スナップショットビルドで Swift は UILabel であると推測するでしょうか?


// label is inferred to be UILabel?
if let label = object.property { 
   // Error due to passing a UILabel? where a UILabel is expected
  functionTakingLabel(label)
}


これは、明示的な型を使って修正することができます:


// Implicitly unwrap object.property due to explicit type.
if let label: UILabel = object.property {
  functionTakingLabel(label) // okay
}


try?


同様に、try? は optional のレベルを追加するので、try? と結びついた暗黙開封された値を返す関数では、optional の第 2 レベルを明示的に開封するようにコードを変更する必要があることがわかるでしょう。


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

if let x = try? test() {
  let y: Int = x    // error: x is an Int?
}

if let x: Int = try? test() { // explicitly typed as Int
  let y: Int = x    // okay, x is an Int
}

if let x = try? test(), let y = x { // okay, x is Int?, y is Int
 ...
}


switch


Swift 4.1 は、output を暗黙開封されたものとして扱ったので、以下のコードを受け入れました:


func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output:
    return output  // implicitly unwrap the optional, producing a String
  }
}


これがこのように書かれていれば、うまくコンパイルされなかった事に注意しましょう:


func switchExample(input: String!) -> String {
  let output = input  // output is inferred to be String?
  switch input {
  case "okay":
    return "fine"
  default:
    return output  // error: value of optional type 'String?' not unwrapped;
                   // did you mean to use '!' or '?'?
  }
}


新しい実装では、最初の例の output の型が暗黙開封されていない String? でないと推論します。


このコンパイルを再度行う 1 つの方法は、値を強制開封することです。


  case let output:
    return output!


これのもう一つの修正方法は、nil でない物と nil に対して明示的にパターンを一致させることです:


func switchExample(input: String!) -> String {
  switch input {
  case "okay":
    return "fine"
  case let output?: // non-nil case
    return output   // okay; output is a String
  case nil:
    return "<empty>"
  }
}


Optional 対暗黙開封された Optional でのIn-Out パラメータの過負荷


Swift 4.1 では、コードが関数の過負荷を試みる場合、in-out パラメータが単純な optional 対暗黙開封された optional であるという違いがある場合、廃止された警告が導入されました。


  func someKindOfOptional(_: inout Int?) { ... }

  // Warning in Swift 4.1.  Error in new implementation.
  func someKindOfOptional(_: inout Int!) { ... }


Swift 4.1 ではまた、暗黙開封として宣言された値を、in-out パラメータとして関数に渡す機能も単純な optional を期待していると追加されました。これにより、上記の 2 番目の過負荷を削除することが可能になりました (実装が同一であると仮定します)。


  func someKindOfOptional(_: inout Int?) { ... }

  var i: Int! = 1
  someKindOfOptional(&i)   // okay! i has type Optional<Int>


暗黙開封された optional の新しい実装では、optional の過負荷は、Int! の型が Int? の同義語であるという意味はなさない。その結果、上記のような過負荷は今やエラーになり、(Int! で宣言された) 2 番目の過負荷を削除しなければなりません。


ImplicitlyUnwrappedOptional の拡張


ImplicitlyUnwrappedOptional<T> は今や、Optional<T> の、使用できない型エイリアスとなり、型の拡張を作成しようとするコードはコンパイルできません。


// 1:11: error: 'ImplicitlyUnwrappedOptional' has been renamed to 'Optional'
extension ImplicitlyUnwrappedOptional {


Nil のブリッジ


nil 値をブリッジするときに実行時エラーが発生すると言うよりも、nilNSNull にブリッジされます。


import Foundation

class C: NSObject {}

let iuoElement: C! = nil
let array: [Any] = [iuoElement as Any]
let ns = array as NSArray
let element = ns[0] // Swift 4.1: Fatal error: Attempt to bridge
                    // an implicitly unwrapped optional containing nil

if let value = element as? NSNull, value == NSNull() {
  print("pass")     // We reach this statement with the new implementation
} else {
  print("fail")
}


訳注:

上記のコードで iuoElement というのは、ImplicitlyUnwrappedOptionalElement の略と思われる。



結論


暗黙開封された optional は、もはや Optional<T> とは異なる型ではないように再実装されました。結果として、型チェックはより一貫性があり、コンパイラには特殊なケースが少なくなります。これらの特殊なケースを削除すると、これらの宣言の処理におけるバグが少なくなります。


インポートされた Objective-C API とのやりとりの結果として、おそらく暗黙開封にあなたはさらされるでしょう。@IBOutlet プロパティを宣言するとき、または完全に初期化されるまで値にアクセスしないことが わかっている 他の場所で、暗黙開封を使用すると便利な場合があります。しかし、暗黙開封を避ける方が良い場合が多く、if letguard let では明示的に開封する必要があります。それが安全であると確信できるときは、! で明示的な強制開封を使用してください。


質問?コメント?


この記事に関する質問やコメントがある場合は、Swift フォーラムの この関連スレッド にお気軽にお問い合わせください。


<-Swift 4.1 がリリースされた! Swift コミュニティがホストとなった継続的統合->






目次
Xcode 10 の新機能

SwiftLogo
  • Swift 4.2 全メニュー

  • Swift について

  • ブログ
  • 暗黙的に開封された Optionals の再実装
  • 暗黙の開封は宣言の一部です
    ソース互換性
    T! への強制
    宣言よりも型で ! の使用
    暗黙に開封された optional として宣言された値への map の呼び出し
    型ではない型は推論できません
    AnyObject ルックアップ
    try?
    switch
    Optional 対暗黙開封された Optional でのIn-Out パラメータの過負荷
    ImplicitlyUnwrappedOptional の拡張
    Nil のブリッジ
    結論
    質問?コメント?

  • Swift のダウンロード

  • Swift 入門

  • 文書化

  • ソースコード

  • コミュニティガイドライン

  • 投稿

  • Swift の継続的統合

  • Swift ソースの互換性

  • フォーカスエリア

  • ABI の安定性

  • サーバー API プロジェクト

  • コンパイラと標準ライブラリ

  • プロジェクト

  • パッケージマネージャ

  • Swift コアライブラリ

  • REPL とデバッガ、プレイグラウンド













  • トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)












    トップへ(Swift 4.2)