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

まいど、西 @jrsaruo_tech です。こんなにたくさん文章を書いたのは久しぶりです。

iOSDC Japan 2025、アンドパッドのブースでは5回に分けて計20問のSwiftクイズを出題しました。

本記事ではDay2 午後に出題したクイズ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

クイズ Day2 午後

Q1 ★☆☆☆☆

問題

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

func describeNumbers(_ numbers: [Int]) {
    /* ? */ {
        print("not empty")
    } else {
        print("empty")
    }
}
  • A

      if numbers
    
  • B

      if not numbers.isEmpty
    
  • C

      if numbers.!isEmpty
    
  • D

      if !numbers.isEmpty
    

解答と解説

解答

D

解説

配列をif文の条件式として渡すことで空かどうかを判定できるプログラミング言語もよくありますが、Swiftのif文は(if letif caseを除いて)条件式にBool値しか受け取れません。

Bool値を反転させるには式全体の先頭に!を付けるので、このクイズの答えはDです。

Q2 ★★★★☆

問題

次のprintAfterReturn()関数を実行したときの挙動として正しいものを選んでください。

func printAfterReturn() {
    return
    print("After return")
}
  • A: 何も表示されずに正常終了
  • B: "After return"と出力されて正常終了
  • C: 実行時にクラッシュする
  • D: コンパイルエラーとなり実行できない

解答と解説

解答

B

解説

なんでこんな簡単なクイズで難易度★4なんだと思いますよね。答えを聞いてなんでやねんとツッコむ方も多いと思います。僕もツッコみました。

実はreturnと返り値の間には改行を入れることができます。つまり、問題のコードは以下と等価です。

func printAfterReturn() {
    return print("After return")
}

printAfterReturn()print(_:)はいずれも返り値型がVoidです。printAfterReturn()の返り値としてprint("After return")を評価し、その結果"After return"が出力され、そしてprintの返り値()がそのままreturnされるわけですね。よって答えはBです。

なおコンパイラは問題のコードに対してちゃんと以下のような警告を出してくれるのでご安心ください。

Expression following 'return' is treated as an argument of the 'return'

そもそもこんなことしないだろうという方も多いと思いますが、僕の場合はデバッグで長い処理をスキップしたいときに全体をコメントアウトせず処理の手前にreturnと書いて済ませることがあり、それで「return直後のコードだけ実行される」という一見意味不明な挙動にぶち当たり時間を溶かしました。皆さんには同じ轍を踏んでほしくないという意図での出題でした。決してひっかけ問題を出したかったとかではありません*1

Q3 ★★★★☆

問題

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

  • A

      actor FooActor {
          func doSomething() {}
      }
    
      func useFooActor1(_ fooActor: FooActor) async {
          await fooActor.doSomething()
      }
    
      func useFooActor2(_ fooActor: isolated FooActor) {
          /* await */ fooActor.doSomething()
      }
    
  • B

      protocol FooProtocol {
          associatedtype Bar
    
          var bar: Bar { get }
      }
    
      func useFooBar<F>(bar: F.Bar) where F: FooProtocol {
          print(bar)
      }
    
  • C

      func doSomething() throws(Never) {}
    
      func foo() {
          /* try */ doSomething()
      }
    
  • D

      import SwiftUI
    
      func fooView(showsText: Bool) -> some View {
          if showsText {
              Text("Foo")
          } else {
              Image(systemName: "plus")
          }
      }
    

解答と解説

解答

A、C

解説

A:OK

fooActorのメソッドdoSomething()にはasyncが付いていませんが、fooActorのisolation domain外から呼び出す場合は非同期呼び出しが強制されます。したがって、non-isolatedであるuseFooActor1(_:)でのfooActor.doSomething()の呼び出しにはawaitが必要です。

一方、useFooActor2(_:)では引数fooActorの型にisolatedというキーワードがついています。これはuseFooActor2(_:)関数を引数fooActorに隔離することを意味します。つまりfooActorのisolation domain内でfooActor.doSomething()を呼び出すことになるため、await無しで同期的に呼び出すことができます。

以上からこのコードはコンパイルが通ります。

B:NG

これは一見コンパイルが通りそうですが、実際は以下のエラーが出ます。

Generic parameter 'F' is not used in function signature

関数シグネチャにおいて(ここでは引数や返り値において)型パラメータFが使われていないというエラーです。確かにF.Barは引数で使っていますがF自体は使っていませんね。

なぜFが関数シグネチャで使われないといけないのでしょうか?理由はFの実際の型を推論できないからです。

試しにuseFooBar(_:)を利用してみましょう。

struct FooA: FooProtocol {
    var bar: Int { 0 }
}
struct FooB: FooProtocol {
    var bar: Int { 0 }
}

let foo = FooA()
let bar: Int = foo.bar
useFooBar(bar)

useFooBar(_:)からすれば、引数で受け取ったのはただのIntです。これだけでは型パラメータFFooAなのかFooBなのか、はたまた他の型なのか判断がつきません。

Fが確定しないとuseFooBar(_:)内でFに依存した処理を実行できないので、引数や返り値からFを推論できるようにする必要があるのです。

C:OK

このコードではtyped throwsを利用しています。throws(Never)の部分です。

あるエラー型ErrorTypeがあるとして、関数シグネチャにthrows(ErrorType)と書くと、その関数はErrorType型のエラーだけ投げることができます。

struct ErrorType: Error {}

func foo() throws(ErrorType) {
    throw ErrorType() // OK
    // throw CancellationError() // NG
}

Never型はErrorプロトコルに適合しているので、throws(Never)と書けます。

問題のdoSomething()関数はthrows(Never)なので、Never型の値しか投げることができません。しかし、Never型はインスタンスを作ることができない(Day0のQ3参照)ので、doSomething()関数はエラーを投げることができません。

このようにthrows(Never)な関数はエラーを投げないことが保証されているため、Swiftではこれをnon-throwing関数として解釈してくれます。つまりtry無しで呼び出すことができるので、このコードはコンパイルが通ります。

// 以下は等価
func doSomething() throws(Never) {}
func doSomething() {}

D:NG

このコードはコンパイルが通りません。

Day0のQ4でも解説した通りsome Viewは何らかの具体的な型を隠しているだけなので、条件によって異なる型の値、つまりTextを返したりImageを返したりすることはできない、というのがざっくりした説明です。

これだけだとやや正確さに欠けるのでもう少し厳密な話をします。

繰り返しになりますが、関数の返り値型に現れるsome Viewは実際の返り値型をただ隠しただけのものです。

func makeView() -> Text {
    Text("...")
}
// ↓ 返り値型Textを隠す
func makeView() -> some View {
    Text("...")
}

そしてsome Viewが隠せるのはViewプロトコルに適合した型のみです。

これらを踏まえて問題のコードを見てみましょう。

func fooView(showsText: Bool) -> some View {
    if showsText {
        Text(...)
    } else {
        Image(...)
    }
}

some ViewViewプロトコルに適合した何かしらの型を隠しています。それはTextかもしれないしImageかもしれないし、はたまた他の型かもしれません。この隠された具体的な型をTとしましょう。

関数の返り値型がTなので、TextImageT型もしくはそのサブタイプでなければなりません。

  • TTextもしくはそのスーパータイプである
  • TImageもしくはそのスーパータイプである
  • TViewプロトコルに適合している

これらの3条件をすべて満たす型Tは存在するでしょうか?

まずT == Textではありません。なぜなら②を満たせないからです。同様にT == Imageでもありません。よって①と②を両方満たし得るTTextImageの共通のスーパータイプです。

TextImageの共通のスーパータイプとは何でしょうか?TextImagestructなので「親クラス」のようなものはありません。考えられるのはany View型(もしくはそのスーパータイプ)です。ところがany View型はViewプロトコルには適合していないため、条件③を満たすことができません。any View型のスーパータイプ(Anyなど)も同様です。

以上から条件①〜③をすべて満たす型Tは存在せず、コンパイルが通らないのです。

ただ、疑問に思った方もいると思います。SwiftUIでvar body: some Viewを実装するとき、分岐ごとに異なるViewを返すようなコードが書けますよね?

struct FooView: View {
    let showsText: Bool
    
    // OK
    var body: some View {
        if showsText {
            Text(...)
        } else {
            Image(...)
        }
    }
}

これはなぜ通るのかというと、ViewプロトコルのbodyプロパティにはViewBuilderというresult builderが適用されており、それを介してこっそり1つの値が組み立てられて返されるからです(仕組みの詳細は割愛します)。

// @ViewBuilder
var body: some View {
    // ViewBuilderによって組み立てられた結果が返される
    _ConditionalContent<Text, Image>(...)
}

問題のコードも@ViewBuilderというアノテーションを付与すればコンパイルが通るようになります。

// OK
@ViewBuilder
func fooView(showsText: Bool) -> some View {
    if showsText {
        Text(...)
    } else {
        Image(...)
    }
}

余談ですが、ViewBuilderを使わない代わりにTextImageAnyViewで包むという手段でコンパイルを通すことも可能です。条件によらず返り値がAnyView型となるからです。AnyView型はViewプロトコルに適合しています。

// OK
func fooView(showsText: Bool) -> some View { // some Viewの具体的な型がAnyViewに定まる
    if showsText {
        AnyView(Text(...))
    } else {
        AnyView(Image(...))
    }
}

AnyViewAnyHashableAnySequenceなどの型は、具体的な型情報を削除することから「型消去(type eraser)」と呼ばれます。

型情報は最適化などの観点から残すに越したことはないので、ViewBuilderで済むならそちらを使ったほうが無難かと思います*2

Q4 ★★★★★

問題

以下のコードを実行したときの挙動として正しいものを選んでください。

func printMessage(_ message: String, after delay: Duration) async {
    do {
        try await Task.sleep(for: delay)
        print(message, "Success")
    } catch {
        print(message, error)
    }
}

func doSomething() async throws {
    async let _ = printMessage("a", after: .seconds(3))
    async let _ = printMessage("b", after: .seconds(1))
    try await Task.sleep(for: .seconds(2))
}

try await doSomething()
  • A: 0秒後に"a CancellationError()"と"b CancellationError"が順序不定で出力された後、2秒後に終了
  • B: 1秒後に"b Success"、さらに2秒後に"a Success"が出力されて終了
  • C: 1秒後に"b Success"、さらに1秒後に"a CancellationError()"が出力されて終了
  • D: 3秒後に"a Success"、さらに1秒後に"b Success"が出力され、さらに2秒後に終了

解答と解説

解答

C

解説

最終問題はasync letの仕様を問うクイズです。使い方を誤って意図しない挙動を招かないよう、しっかり抑えておきましょう。

重要なポイントを3点挙げます。

  • async letは子タスクを並列で立ち上げる
  • async let _ = ...で返り値を捨てたとしても子タスクの実行はキャンセルされない
  • ③ 親のスコープを抜けるとasync letで作成された子タスクはキャンセルされる

では、問題のコードをタイムラインに沿って見ていきましょう。

t = 0s時点

  1. try await doSomething()が呼び出され、doSomething()の処理が始まる
  2. async let _ = printMessage("a", after: .seconds(3))が呼ばれる
    • 子タスクAが立ち上がる
    • ②により子タスクAはキャンセルされない
  3. ①により子タスクAの完了を待つことなくasync let _ = printMessage("b", after: .seconds(1))が呼ばれる
    • 子タスクBが立ち上がる
    • ②により子タスクBはキャンセルされない
  4. try await Task.sleep(for: .seconds(2))が呼ばれる

t = 1s時点

  1. 子タスクBのtry await Task.sleep(for: delay)が完了する
  2. 子タスクBのprint(message, "Success")が呼ばれて"b Success"が出力される

t = 2s時点

  1. try await Task.sleep(for: .seconds(2))が完了する
  2. doSomething()のスコープを抜ける
  3. async letで作成された子タスクAが残っているため、③により子タスクAがキャンセルされる
  4. 子タスクAで実行中のtry await Task.sleep(for: delay)CancellationErrorを投げ、catch節に入り"a CancellationError()"が出力される
  5. 処理が終了する

以上から、クイズの答えはCです。

おわりに

Swiftクイズ最終4題でした!皆さん解けましたか?

僕はクイズを20問作ったら解説も20問分書かないといけないという貴重な学びを得ました。5万字書きました。

難しいクイズもあったと思いますが、Swiftの面白さや奥深さを少しでも共有できたなら幸いです。今後コンパイルエラーに遭遇したとき「アンドパッドのSwiftクイズでやったやつだ!」と解決の糸口を掴んでもらえることを願っています。
20問すべて正解できたあなたはSwiftが大好きに違いありません。ぜひ僕 @jrsaruo_tech とSwift愛を語り尽くしましょう。

それでは、5記事にわたってお読みいただきありがとうございました!

*1:Q2の正解率が楽しみだなんて言っていた出題者がいたそうですが事実無根です。そんなまさか。

*2:実際にパフォーマンス差を検証したわけではないためあくまで参考まで。