来たるSwift5の新機能をまとめました

概要

1/25(金)の仕事終わりにツイッターでこのようなつぶやきがタイムラインで上がってきていた。

Swift 5 Release Notes for Xcode 10.2 beta

とうとう、Swift5がリリースされるそうでした。

ええと、Swift4.2がリリースされてからまだ半年しか経っていないのですが、、、
時代の流れが早いそうです。
(現場によってはまだSwift3の所もあるかと思います。)

Swift4.2でパワーアップしたことについてをまとめました

そんな来たるSwift5のために
Swift5で結局何が変わるの?というのを要点をまとめて整理しようと思います。

因みに元記事はこちらとなります。

What’s new in Swift 5.0

では始めて行きたいと思います。


始めに

Swift 5.0はSwiftの次のメジャーリリースであり、やっとABIの安定性をもたらす予定です。 それだけではありません。Raw strings、将来のenumケース、整数倍数のチェックなど、いくつかの重要な新機能がすでに実装されています。

来年初め(2019/1月)にSwift 5.0をリリースする前に試してみたい場合は、最新のSwift trunk開発スナップショットをダウンロードし、現在のXcodeバージョン内でアクティブ化してから、以下の私の例に従ってください。

Raw strings

SE-0200では、Raw stringsを作成する機能が追加されました。バックスラッシュと引用符(quote marks)は、文字や文字列の終端文字をエスケープするのではなく、それらのリテラル記号として解釈されます。 これにより、多くのユースケースがより簡単になりますが、特に正規表現が役立ちます。

Raw stringsを使用するには、次のように文字列の前に1つ以上の記号を配置します。

Sample.swift
let rain = #"The "rain" in "Spain" falls mainly on the Spaniards."#

文字列の最初と最後の記号は文字列の区切り文字の一部になるため、Swiftは、"rain"と"Spain"を囲むスタンドアロンの引用符は、文字列を終了するのではなくリテラル引用符として扱う必要があると理解します。

Raw stringsでもバックスラッシュを使うことができます。

Sample.swift
let keypaths = #"Swift keypaths such as \Person.name hold uninvoked references to properties."#

これはバックスラッシュを文字列内のエスケープ文字ではなくリテラル文字として扱います。 これは、文字列補間の動作が異なることを意味します。

Sample.swift
let answer = 42
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)."#

私はいかにして文字列補間を使うために\#(answer)を使ったかに注意してください - 通常の\(answer)は文字列内の文字として解釈されるので、Raw stringsで文字列補間を行いたいときはを追加しなければなりません。

SwiftのRaw stringsのおもしろい機能の1つは、始めと終わりにハッシュ記号を使用することです。ここで良い例を出すのは非常に珍しく難しいのですが、この文字列を考慮してください。私の犬は "woof" #gooddogと言いました。ハッシュの前にスペースがないので、Swiftは "#を見てすぐにそれを文字列の終端文字として解釈します。このような状況では、デリミタを#"から##"に変更する必要があります。

Sample.swift
let str = ##"My dog said "woof"#gooddog"##

最後のハッシュの数が最初のハッシュの数とどのように一致しなければならないかに注意してください。
Raw stringsは、Swiftの複数行の文字列システムと完全に互換性があります。開始するには#"""を使用し、次に終了するには"""#を使用するだけです。

Sample.swift
let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
"""#

多くのバックスラッシュを使わずにできるという事は、正規表現で特に役立ちます。たとえば、\Person.nameのようなキーパスを見つけるための単純な正規表現を書くと、これは次のようになります。

Sample.swift
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"

Raw stringsのおかげで、バックスラッシュの半分の数で同じことを書くことができます。

Sample.swift
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#

正規表現でも使用されているので、まだいくつか必要です。

Dynamically callable types

SE-0216ではSwiftに新しい@dynamicCallable属性が追加されました。これにより、型を直接呼び出し可能としてマークすることができます。 これは、コンパイラの魔法のようなものではなく、シンタックスシュガーでありこのコードを効果的に変換します。

Sample.swift
let result = random(numberOfZeroes: 3)

これに:

Sample.swift
let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])

以前、私はSwift 4.2の@dynamicMemberLookupという機能について書きました。 @dynamicCallable@dynamicMemberLookupの自然な拡張であり、同じ目的を果たします。SwiftコードPythonJavaScriptなどの動的言語と連携して動作するのを容易にするためです。

この機能を独自の型に追加するには、@dynamicCallable属性とこれらのメソッドの一方または両方を追加する必要があります。

Sample.swift
func dynamicallyCall(withArguments args: [Int]) -> Double

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double

最初のものは、パラメータラベルなしで型を呼び出すときに使用され(例:a(b、c))、2番目はラベルを提供するときに使用されます(例:a(b:cat、c:dog))。

@dynamicCallableは、そのメソッドがどのデータ型を受け入れて返すかについて非常に柔軟なので、Swiftのすべての型安全性の恩恵を受けることができますが、高度な使用法にはまだ少し余裕があります。そのため、最初のメソッド(パラメータラベルなし)には、array、array slice、setなど、ExpressibleByArrayLiteralに準拠したものを使用でき、2番目のメソッド(パラメータラベル付き)には、dictionaryやkey value pairなどのExpressibleByDictionaryLiteralに準拠したものを使用できます。

注:これまでKeyValuePairsを使用したことがない場合は、@dynamicCallableを使用すると非常に便利なので、それらが何であるかを学ぶのに適した時期です。ここで詳細をご覧ください:KeyValuePairsは何か?

さまざまな入力を受け付けるだけでなく、さまざまな出力に対して複数のオーバーライドを指定することもできます。1つは文字列を返し、もう1つは整数などを返します。Swiftがどちらが使用されているかを解決することができれば、必要なものをすべて組み合わせて組み合わせることができます。

例を見てみましょう。まず、渡された入力に応じて、0から特定の最大値までの数値を生成するRandomNumberGenerator構造体があります。

Sample.swift
struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}

これを@dynamicCallableに切り替えるには、代わりに次のように書きます。

Sample.swift
@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

このメソッドは任意の数のパラメータ、またはおそらくゼロで呼び出すことができます。そのため、最初の値を注意深く読み、適切なデフォルト値があることを確認するためにnil合体を使用します。

これでRandomNumberGeneratorインスタンスを作成し、それを関数のように呼び出すことができます。

Sample.swift
let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)

代わりにdynamicCall(withArguments :)を使用した場合、または同時に両方を単一タイプにすることができるため、次のように記述します。

Sample.swift
@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args[0])
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

let random = RandomNumberGenerator()
let result = random(0)

@dynamicCallableを使用する際に注意が必要ないくつかの重要な規則があります。

  • 構造体、列挙型、クラス、そしてプロトコルに適用することができます。
  • withKeywordArguments:を実装したり、withArguments:を実装しない場合でも、型はパラメータラベルなしで呼び出すことができます - キーには空の文字列を取得するだけです。
  • withKeywordArguments:またはwithArguments:の実装がthrowingとしてマークされている場合は、型の呼び出しもthrowします。
  • @dynamicCallableをextensionに追加することはできません。型のプライマリな定義のみです。
  • まだ他のメソッドやプロパティに型を追加して、通常どおりに使用することができます。

おそらくもっと重要なのは、メソッドの解決がサポートされていないことです。つまり、型の特定のメソッドを呼び出すのではなく (例 random.generate(numberOfZeroes: 5))、型を直接呼び出す必要があります (例 random(numberOfZeroes: 5))。このようなメソッドシグネチャを使用して後者を追加することについてはすでにいくつかの議論があります。

Sample.swift
func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)

それが将来のSwiftのバージョンで可能になった場合、テストモックのためのいくつかの非常に興味深い可能性を開くかもしれません。

それまでの間、@dynamicCallableは広く普及する可能性は低いですが、PythonJavaScript、およびその他の言語との対話を望む少数の人々にとって非常に重要です。

Handling future enum cases

SE-0192には、固定されたenumと将来変更される可能性のあるenumを区別する機能が追加されています。

Swiftのセキュリティ機能の1つは、すべてのswitchステートメントが網羅的である必要があるということです - それらはすべてのケースをカバーしなければならないということです。これは安全性の観点からはうまく機能しますが、将来新しいケースが追加されると互換性の問題が発生します。システムフレームワークがあなたの要求に応えていないものを送信するかもしれません。 あなたのswitchがもはや網羅的ではないので、breakにコンパイルしてください。

@unknown属性を使用すると、2つの微妙に異なるシナリオを区別できます。「このdefaultケースは、個別に処理したくないため、他のすべてのケースに対して実行する必要があります。」および「すべてのケースを個別に処理したいのですが、将来的にエラーが発生するのではなく使用します。」

これがenumの例です。

Sample.swift
enum PasswordError: Error {
    case short
    case obvious
    case simple
}

switchブロックを使用してこれらの各ケースを処理するコードを書くことができます。

Sample.swift
func showOld(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    default:
        print("Your password was too simple.")
    }
}

これは、.shortと.obviousのケースのパスワードに対して2つの明示的なケースを使用しますが、3番目のケースをdefaultブロックにまとめます。

さて将来、oldと呼ばれる新しいケースを以前に使用されていたパスワードに追加すると、そのメッセージが実際には意味をなさない場合でも、自動的にdefaultケースが呼び出されます。パスワードは単純すぎてはいけません。

このコードは技術的に正しい(最良の種類として正しい)ため、Swiftが警告することはできません。したがって、この間違いは簡単に見逃される可能性があります。 幸いなことに、新しい@unknown属性は完全にそれを修正します - それはdefaultケースでのみ使用することができ、新しいケースが将来発生したときに実行されるように設計されています。

例えば:

Sample.swift
func showNew(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    @unknown default:
        print("Your password wasn't suitable.")
    }
}

そのコードはswitchブロックがもう網羅的ではないので警告を出すでしょう。Swiftは、各ケースを明示的に処理するように求めています。これは単なる警告にすぎないので、この属性が非常に便利になります。将来frameworkで新しいケースが追加された場合は、警告が表示されますが、ソースコードが破損される事はありません。

Flattening nested optionals resulting from try?

SE-0230try?の動作を変更して、ネストされたOptionalがフラット化されて通常のOptionalになるようにします。 これはOptionalのチェーニングと条件付きタイプキャストと同じように機能します。どちらも以前のSwiftバージョンではOptionalをフラットにします。

これは、変更を実証する実際的な例です。

Sample.swift
struct User {
    var id: Int

    init?(id: Int) {
        if id < 1 {
            return nil
        }

        self.id = id
    }

    func getMessages() throws -> String {
        // complicated code here
        return "No messages"
    }
}

let user = User(id: 1)
let messages = try? user?.getMessages()

User構造体には無効なイニシャライザがあります。これはfolksが確実に有効なIDを持つuserを作成できるようにするためです。getMessages()メソッドには、理論上userへのすべてのメッセージのリストを取得するためのある種の複雑なコードが含まれているので、throwとしてマークしています。コードがコンパイルされるために、固定の文字列を返すようにしました。

最後の行がキーラインです。userはOptionalなのでオプショナルチェーンを使用し、getMessages()はそれをスローすることができるのでtry?を使用してスローメソッドをオプショナルに変換します。Swift 4.2以前ではmessagesString?? - オプショナルのオプショナルString - にしていましたが、Swift 5.0以降では、try?はすでに値がオプショナルの場合、値をオプショナルで囲むことはできません。
messagesは単にString?になります。

この新しい動作は、オプショナルチェーンと条件付きタイプキャストの既存の動作と一致しています。つまり、必要に応じて1行のコードでオプショナルチェーンを数十回使用することもできますが、12個の入れ子になったオプショナルを使用することはできません。同様に、as?でオプショナルチェーンを使用した場合でも、最終的には1つのレベルの選択性しか得られないでしょう。それは通常あなたが望むものだからです。

Checking for integer multiples

SE-0225では、integerにisMultiple(of :)メソッドが追加され、除算剰余演算を使用するよりもはるかに明確な方法で、ある数が別の数の倍数であるかどうかを確認できます。

具体例

Sample.swift
let rowNumber = 4

if rowNumber.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}

はい、同じチェックをif rowNumber % 2 == 0を使用して書くこともできますが、それほど明確ではないことを認めなければなりません。メソッドとしてisMultiple(of :)を使用すると、Xcodeのコード補完オプションに表示できるため、検索に役立ちます。

Counting matching items in a sequence

SE-0220では、filter()と同等の処理を行い、シングルパスでカウントする新しいcount(where :)メソッドが導入されました。これにより、すぐに破棄される新しい配列の作成が節約され、一般的な問題に対する明確で簡潔な解決策が提供されます。

次の例では、テストの結果の配列を作成し、85以上の数が数えられます。

Sample.swift
let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }

そしてこれは、配列内で“ Terry”で始まる名前の数を数えます。

Sample.swift
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }

このメソッドはSequenceに準拠しているすべての型で利用可能であるため、SetやDictionaryにも使用できます。

Transforming and unwrapping dictionary values with compactMapValues()

SE-0218はDictionaryに新しいcompactMapValues()メソッドを追加され、DictionaryのmapValues()メソッドとArray(「値の変換、結果のアンラップ、そしてnil以外のものの破棄」)からのcompactMap()機能が統合されました。my keyはそのままですが、my valueを変換します。

一例として、これはレースに参加した人々のDictionaryと、彼らが数秒で終了するのにかかった時間です。 "DNF"とマークされた1人の人が終了しませんでした。

Sample.swift
let times = [
    "Hudson": "38",
    "Clarke": "42",
    "Robinson": "35",
    "Hartis": "DNF"
]

nameとtimeはintegerとして、1人のDNFを削除した新しいDictionaryを作成するためにcompactMapValues()を使用することができます。

Sample.swift
let finishers1 = times.compactMapValues { Int($0) }

あるいは、Intのイニシャライザを直接compactMapValues()に直接渡すこともできます。

Sample.swift
let finishers2 = times.compactMapValues(Int.init)

また、、次のようにcompactMapValues()を使用する事で、変換の任意の並べ替えを行うことなく、optionalのラップを解除しnilの値を破棄する事ができます。

Sample.swift
let people = [
    "Paul": 38,
    "Sophie": 8,
    "Charlotte": 5,
    "William": nil
]

let knownAges = people.compactMapValues { $0 }

More still to come!

これらはすでにSwift 5.0ブランチに実装されている機能ですが、Swift 5.0のリリースに至るまでの数カ月のうちに間違いなくもっと多くの機能が追加されるでしょう。

そして、ABIの安定性が5.0の最大の機能の1つであることを忘れないでください。多くの大企業(もちろんApple自身を含みます)がこれが起こるのを待っているので、Swiftへの素晴らしい移行が始まります。

エキサイティングな時代になってきましたね!

追記1

Swift5に関する情報はMatsudateさんのこちらの記事の方が詳しかった(笑!)ので参考資料としてリンク貼ることでご了承ください。

What’s new in Swift 5

追記2

Xcode10.2のβ版がAppleのダウンロードページでダウンロードできるそうです。

Xcode10.2 β

優しい方で良かったです。
(引用ダメそうでしたら削除します。)