iOSDC Japan 2025 アンドパッドSwiftクイズ解説 – Day1 午後

こんにちは、西 @jrsaruo_tech です。

iOSDC Japan 2025 Day1 後半戦です!Day0から数えればもう折り返しですね。

アンドパッドのブースでは引き続きクイズを出題しています。

  • Day0
  • Day1 午前
  • Day1 午後 ← 本記事
  • Day2 午前
  • Day2 午後

本記事ではDay1 午後に出題したクイズ4題を解説します。

それぞれの解答と解説はセクションを閉じているので、まだ解かれていない方はぜひチャレンジしてみてください。

※ クイズで使用しているSwiftのバージョンは6.1.2、言語モードは6です。

% swift --version   
swift-driver version: 1.120.5 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
Target: arm64-apple-macosx15.0

Q1 ★☆☆☆☆

問題

/* ? */に当てはまるコードとして正しいものを選んでください。

struct Point {
    var x: Int
    var y: Int
}

func move(to destination: Point) {  }

let origin = Point(x: 0, y: 0)

解答と解説

解答

B

解説

Swiftでは関数呼び出しの際に引数名(引数ラベル)を書くことが強制されます。

func move(destination: Point) { ... }


move(destination: origin)

そして、関数の宣言側でその呼び出し側に書かせる引数ラベルをカスタマイズすることができます。

func move(to destination: Point) {
    
    print("Move to:", destination)
}


move(to: origin)

この機能により、SwiftのAPIは”move to origin”のように英語として自然に読むことができ、引数の役割を分かりやすく簡潔に表現できます。関数シグネチャも”move to destination”と読めるのが個人的には好きです。

Q2 ★★☆☆☆

問題

次のコードにおいて、変数fooの型として正しいものを選んでください。

struct Foo {}

struct Bar {
    func makeFoo() throws -> Foo? {
        nil
    }
}

func makeFoo(from bar: Bar?) {
    let foo = try? bar?.makeFoo()
}

解答と解説

解答

A

解説

オプショナルチェーンやtry?演算子がオプショナルをネストさせるかどうか、というクイズです。オプショナルのネストとはつまりT??Optional>)のような状態です。

オプショナルチェーンはオプショナルの値のプロパティアクセスやメソッド呼び出しなどをアンラップなしで行える仕組みです。ドットアクセスの直前に?を付けることで行います。

let property = optionalValue?.property
let result = optionalValue?.method()

オプショナルの値がnilでなければプロパティやメソッドの呼び出しが成功し、その結果を受け取れます。オプショナルの値がnilだった場合は呼び出しが行われず、式全体がnilとして評価されます。

そしてその式全体の型はオプショナルになりますが、プロパティやメソッド自体がオプショナルの値を返すとしてもネストしません。

struct A {
    var value: Int
    var optionalValue: Int?
}

func useOptionalA(_ a: A?) {
    let value: Int? = a?.value
    let optionalValue: Int? = a?.optionalValue 
}

よって問題のbar?.makeFoo()単体で見るとその値はFoo?型となります。

let foo: Foo? = try bar?.makeFoo()

ここからさらにtrytry?に変更します。

try?演算子はthrowsプロパティや関数が投げたエラーをnilに変換してくれます。

func doSomething() throws -> Int { ... }

let result: Int? = try? doSomething()



let result: Int?
do {
    result = try doSomething()
} catch {
    result = nil
}

try?演算子が付くと、式全体の型はオプショナルになります。

そしてこのtry?演算子も先ほどと同様に、元の式がオプショナルの値を返すとしてもネストしません。

したがって、問題のtry? bar?.makeFoo()の型はFoo?となります。

ちなみにtry?演算子は古いバージョンのSwiftではオプショナルがネストする仕様だったのですが、オプショナルチェーンやas?の挙動との一貫性、また利便性の観点からSwift 5で現在の仕様に変更されました。

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0230-flatten-optional-try.md

Q3 ★★★★☆

問題

次のコードのコンパイルが通るようなperform関数の定義をすべて選んでください。

func doSomething() throws {}

func usePerform() throws {
    perform {
        print("Performed")
    }
    try perform {
        try doSomething()
    }
}
  • A

      func perform(_ body: () throws -> Void) throws {
          try body()
      }
    
  • B

      func perform(_ body: () throws -> Void) rethrows {
          try body()
      }
    
  • C

      func perform(_ body: () throws(E) -> Void) throws(E) where E: Error {
          try body()
      }
    
  • D

      func perform(_ body: () throws(E1) -> Void) throws(E2) where E1: Error, E2: Error {
          try body()
      }
    

解答と解説

解答

B、C

解説

まず問題のperform関数の性質を確認します。

perform {
    print("Performed")
}


try perform {
    try doSomething()
}

どちらもperform関数を呼び出す処理で、引数にクロージャを渡しています。

①ではperform関数の引数に渡したクロージャ内でtrythrowもしておらず、performの呼び出し自体にもtryが付いていません。

②ではperform関数の引数に渡したクロージャ内でcatch無しにtryしており、performの呼び出し自体にもtryが付いています。

つまり、perform関数は「引数に渡したクロージャがthrowsでなければ自身もthrowsにはならず、引数に渡したクロージャがthrowsであれば自身もthrowsとなる」という性質を持っています(厳密にはnon-throwingな関数にもtryはつけられるので②のperformthrowsであるとは限りませんが、選択肢にそのような定義はないため無視します)。

この性質を実現するには2種類の方法があります。

  • rethrowsを利用する方法
  • Typed throwsを利用する方法

では、選択肢を1つずつ見ていきましょう。

A:NG

perform関数自体がthrowsになっているため、performの呼び出しには必ずtryが必要です。
したがって①のコンパイルが通りません。

B:OK

perform関数がrethrowsになっています。これはまさに今回のような振る舞いを実現するために用意された機能で、「引数で受け取ったクロージャがthrowsでなければperform自身もthrowsにはならず、引数で受け取ったクロージャがthrowsであればperform自身もthrowsとなる」という性質を与えます。

したがってこのperform関数ならコンパイルが通ります。

C:OK

Swift 6で導入されたtyped throwsを利用した関数定義です。関数の返り値型を宣言するのと同じように、typed throwsでは関数が投げるエラーの型を宣言できます。

func foo() throws(CancellationError) {
    
    throw CancellationError()
}

try foo()

エラーの型としてNeverを指定することもできます。Never型はインスタンスを作ることができないので、throws(Never)な関数は例外を投げることができません。そのため throws(Never)な関数はnon-throwingな関数として扱えるようになっており、呼び出し時にtryをつける必要がありません。

func foo() throws(Never) {
    
    
    print("cannot throw")
}

foo() 

さらにエラー型はジェネリック関数の型パラメータとして宣言することもできます。
それを利用したのが選択肢Cのperform関数です。以下に再掲します。

func perform<E>(_ body: () throws(E) -> Void) throws(E) where E: Error {
    try body()
}

このperform関数では引数で受け取ったbodyの型によって型パラメータEが決まり、それによってperform関数自身の投げ得るエラー型が決まります。

実際に型パラメータEがどのように推論されるか見てみましょう。まずは①の例です。

perform {
    print("Performed")
}

この例では引数bodyに渡すクロージャはエラーを投げません。このとき型パラメータENeverと推論され、同時にperform関数もthrows(Never)となります。つまり例外が投げられることはないので、perform関数をtry無しで呼び出すことができます。

実際、E == Neverのときのperform関数は以下の通りどう頑張ってもエラーを投げることができません。型に守られている安心感がありますね。

func perform<E>(_ body: () throws(E) -> Void) throws(E) where E: Error {
    try body() 
    throw SomeError() 
    throw SomeError() as! E 
}

次に②の例を見てみます。

try perform {
    try doSomething()
}

この例では引数bodyに渡すクロージャ内でcatchせずにtryしています。このとき型パラメータEany Errorと推論され、同時にperform関数もthrows(any Error)となります。つまりperform関数はエラーを投げ得るので呼び出し時にtryが必要です。

以上から選択肢Cの定義もperform関数の性質を満たし、コンパイルが通ります。

従来rethrowsという専用の機能で実現されていた性質が、新たに導入されたtyped throwsでも自然に表現できるというのが面白いですね。Never型もうまくはまっていて設計の妙を感じます。

D:NG

performthrows(E2)なので、try body()で投げられ得るE1型のエラーを捕捉せずそのまま伝播させることはできません!という話をするつもりで作問したのですが、実際に試してみると想定と異なるコンパイルエラーが出ました。

Generic parameter ‘E2’ is not used in function signature

型パラメータE2が関数シグネチャで使われていないというエラーです。throws(E2)も関数シグネチャの一部という理解でいましたが(もし詳しくご存知の方いらっしゃればコメントください)、要はE2を推論できないことが問題だと思われます。

話を簡単にするためにミニマルな例を考えてみましょう。

func foo<E>() throws(E) {}

foo()
try foo()

このとき、型パラメータEをどのように推論すれば良いでしょうか。

呼び出し側でtryが付いていなければE == Nevertryが付いていればE == any Errorと考えても良いかもしれませんが、それだとfoo()と書いても良いしtry foo()と書いても良いということになり、従来の「throws関数の呼び出しに必ずtryが強制されることでエラーが投げられるかどうかを認知・意識しやすい」というメリットが薄まってしまいます。

逆に常にE == any Errorと推論してtryなしのfoo()をコンパイルエラーにすることもできるかもしれませんが、それだと型パラメータを導入する意味がないですよね。

こうした事情から型パラメータEthrows(E)の部分にしか現れないような関数定義は許されていないのだと思います。

解答者の皆さんのなかでこのコンパイルエラーを予想できた方はいたでしょうか。私にとっては想定外の結果だったのでヒヤッとしましたが、いずれにせよコンパイルは通らないのでクイズの答えに影響はありません。よかった。

Q4 ★★★★★

問題

次のなかでコンパイルが通るものをすべて選んでください。

  • A

      let makeArray: () -> [Int] = { [0, 1, 2] }
      let makeSequence: () -> any Sequence = makeArray
    
  • B

      let makeSequence: () -> any Sequence = { [0, 1, 2] }
      let makeArray: () -> [Int] = makeSequence
    
  • C

      let useArray: ([Int]) -> Void = { _ in }
      let useSequence: (any Sequence) -> Void = useArray
    
  • D

      let useSequence: (any Sequence) -> Void = { _ in }
      let useArray: ([Int]) -> Void = useSequence
    

解答と解説

解答

A、D

解説

Swiftのクイズというよりはもう少し一般性の高い、型に関するクイズです。たまにはこういうのも良いですね。

A:OK

let makeArray: () -> [Int] = { [0, 1, 2] }
let makeSequence: () -> any Sequence = makeArray

これはコンパイルが通ります。順を追って見ていきましょう。

まず、[Int]Sequenceプロトコルに適合しているので、any Sequenceのサブタイプです。以下のコンパイルが通ります。

let elements: any Sequence = [0, 1, 2] 


elements.forEach { element in
    print(element)
}

[0, 1, 2]makeArray()に変えてみます。これも当然コンパイルが通りますね。

let elements: any Sequence = makeArray() 

つまり、makeArrayは「any Sequenceを返す関数」と捉えても破綻しないわけです。したがって() -> any Sequence型のクロージャとして扱うことができます。

let makeSequence: () -> any Sequence = makeArray 
let elements: any Sequence = makeSequence()

B:NG

let makeSequence: () -> any Sequence = { [0, 1, 2] }
let makeArray: () -> [Int] = makeSequence

一見問題なさそうに見えますが、makeSequence[0, 1, 2]を返すとは限りません。例えば["a", "b", "c"]を返すとしたらどうでしょうか?

let makeSequence: () -> any Sequence = { ["a", "b", "c"] }
let makeArray: () -> [Int] = makeSequence
let numbers: [Int] = makeArray() 

[Int]型の変数に["a", "b", "c"]が代入されることになりますね。これでは型が破綻してしまうので、コンパイルエラーになります。

C:NG

let useArray: ([Int]) -> Void = { _ in }
let useSequence: (any Sequence) -> Void = useArray

Aが通るならなんとなくこれも通りそうに見えますが、以下のコードを見てみてください。

let useArray: ([Int]) -> Void = { numbers in
    print(numbers.map { $0 * 2 })
}
let useSequence: (any Sequence) -> Void = useArray
useSequence(["a", "b", "c"])

useSequenceの引数はany Sequence型なので、当然["a", "b", "c"]のような値も受け取れます。ところが実際にはuseArrayという[Int]しか受け付けない関数が入っているわけです。これも型が破綻してしまっていますね。numbers["a", "b", "c"]が入った状態でnumbers.map { $0 * 2 }が実行されてしまうことを考えると恐怖で夜も眠れません。

というわけでこれもコンパイルエラーが出るようになっています。安心して眠ってください。

D:OK

let useSequence: (any Sequence) -> Void = { _ in }
let useArray: ([Int]) -> Void = useSequence

useArray[Int]を受け取って何かをします。実際にはuseSequence[Int]を受け取るわけですが、useSequenceany Sequenceを受け取れるので、型が破綻しません。

let useSequence: (any Sequence) -> Void = { elements in
    elements.forEach { element in
        print(element)
    }
}
let useArray: ([Int]) -> Void = useSequence
useArray([0, 1, 2]) 

したがってDはコンパイルが通ります。

[Int]any Sequenceのサブタイプである一方、([Int]) -> Void(any Sequence) -> Voidではサブタイプ関係が逆転するのは面白いですね。ここらへんの話は共変性(covariant)と反変性(contravariant)について調べると色々出てくるので興味のある方は調べてみてください。

Day1 午後のクイズ解説でした。あなたは何問正解できましたか?

明日は早くも最終日ですね。明日も午前、午後と新しいクイズを出題するのでぜひまたアンドパッドブースまで遊びにきてください。

明日はTrack Bにて11:25より『ネイティブ製ガントチャートUIを作って学ぶUICollectionViewLayoutの威力』というタイトルで登壇しますので、そちらもぜひお越しください。ブースではANDPADアプリも展示しているので、ネイティブのガントチャートUIを一足早く触ってみることもできますよ。

fortee.jp

さらにLT大会の後半戦には弊社やまひろ @yamahiro248 が登壇します。こちらもお楽しみに!

fortee.jp

以上、お読みいただきありがとうございました!




Source link

関連記事

コメント

この記事へのコメントはありません。