こんにちは。株式会社メルカリでiOSエンジニアをやっているkntkです。
8月22日から8月24日にかけて開催された「iOSDC Japan 2024」にメルカリはプラチナスポンサーとして参加し、会場ブース出展ではiOS開発に関するクイズを3日分(合計18問)出題しました!
本記事ではこのクイズの問題とその解説をお届けします!
前提
- 個別に記載がない限りXcode 15.4(Swift 5.10)・iOS 17.6.1上での実行結果を正解とします。
- 最適化オプションは-Oを正解とします。
Day0 前夜祭 (8/22)
- 回答者数: 26名
- 平均点: 2.04/6点
- 全問正解者: 0名
1. 次のプログラムを実行すると何が出力されるでしょう?
func f(_ a: Int?) { print("Int?") }
func f(_ a: Any) { print("Any") }
let a: Int = 1
f(a)
選択肢
A) Int?
B) Any
答え
B) Any
解説
Swiftにはオーバーロードが存在します。
ある関数呼び出しに当てはまる複数の関数オーバーロードが存在した時、スコア規則というルールで解決する優先順位が決められています。
Intからの変換はInt?よりもAnyが優先です。
参考: Swiftのオーバーロード選択のスコア規則21種類#ランク7: SK_ValueToOptional
2. 以下の数字をカウントするアプリでCounterViewのボタンを5回押した後、RootViewのボタン (“toggle color!”)を1回押すと次のうちどの表示になるでしょうか?
struct RootView: View {
@State var condition = false
var body: some View {
VStack {
CounterView()
.if(condition) {
$0.background(Color.red)
}
Button("toggle color!") { condition.toggle() }
}
}
}
struct CounterView: View {
@State var count = 0
var body: some View {
Button(count.description) { count += 1 }
}
}
extension View {
@ViewBuilder
func `if`<Content: View>(
_ condition: Bool,
@ViewBuilder transform: (Self) -> Content
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
アプリの表示イメージ
選択肢
A)「5, 赤背景」
B)「5, 背景なし」
C)「0, 赤背景」
D)「0, 背景なし」
答え
C) 「0, 赤背景」
解説
Stateの状態はView Identityという識別子によって管理されているため、View Identityが変わるとStateも初期化されます。また、ViewBuilderのif文はそれぞれの分岐のViewで異なるView Identityを持ちます。(Structural Identity)
今回のプログラムでは、”toggle color”ボタンを押すとif文の分岐が変化し、違うView Identityに変わってしまうため、状態が初期化されcountが0に戻ってしまいます。
このコードの様なif modifierは、使用者側からView Identityが変わることが意識しづらいので注意が必要です。
参考: [SwiftUI] ViewのIdentityと再描画を意識しよう
3. Sendability違反 (Swift 6でのエラー) に該当する型を全て選択してください。
※classのinitを省略して記載しています
struct A: Sendable {
let count: Int
}
struct B: Sendable {
var count: Int
}
final class C: Sendable {
let count: Int
}
final class D: Sendable {
var count: Int
}
答え
Dのみ
解説
Sendableは「Isolation Domain(並列にアクセスが行われる単位)を安全に跨ぐことができる」を表すprotocolです。
structのみで構成されるstructはSendable。structは値型であり、スレッドを跨ぐ際に値のコピーが行われるため、(内部の変数がvarであっても)データ競合の危険性がありません。
classはfinalで内部の変数が全て「let」かつ「Sendable」ならSendable。それ以外はnon-Sendable。classは参照型であり、Isolation Domainを跨いで参照が共有可能ですが、変数がletであれば変数への書き込みが不可能であり、さらにSendableであれば安全にアクセスできることが保証されているため、data raceの危険性がありません。
変数がvarまたはnon-Sendableの場合は書き込みが可能でdata raceの危険性があります。
4. nをCollectionの長さとした時、Array.countの計算量は「a」, String.countの計算量は「b」である。
選択肢
A) a: O(n) b: O(n)
B) a: O(1) b: O(n)
C) a: O(n) b: O(1)
D) a: O(1) b: O(1)
答え
B) a: O(1) b: O(n)
解説
Collection.countの計算量はO(n)です。ただしRandomAccessCollectionの場合はO(1)となります。
ArrayはRandomAccessCollectionですが、StringはRandomAccessCollectionではありません。
参考: 公式ドキュメント Collection.count
5. 次のプログラムを実行すると何が出力されるでしょう?
class Counter {
var count = 0
}
var counter = Counter()
let closure = { [counter] in
print(counter.count)
}
counter.count += 1
closure()
counter = Counter()
closure()
選択肢
A) 0 0
B) 0 1
C) 1 0
D) 1 1
答え
D) 1 1
解説
capture listsを用いて変数をキャプチャすると、クロージャ定義時の値でその変数が初期化されるため、変数の変更が共有されません。
参考: Swift 公式ドキュメント Capture Lists
6. 次のプログラムを実行すると何が出力されるでしょう?
func log(info: String = "called: \(#function)") {
print(info)
}
func main() {
log()
log(info: "called: \(#function)")
}
main()
選択肢
A) called: main() called: main()
B) called: main() called: log(info:)
C) called: log(info:) called: main()
D) called: log(info:) called: log(info:)
答え
C) called: log(info:) called: main()
解説
デフォルト引数における#functionは呼び出し元の関数名を生成する特殊マクロですが、特殊マクロはデフォルト引数のsub-expressionで用いる(例: 文字列展開で値を加工する)と呼び出し元の関数名を参照できなくなります。
参考: SE-0422 Expression macro as caller-side default argument
Day1 (8/23)
- 回答者数: 116名
- 平均点: 2.98/6点
- 全問正解者: 4名
1. 次のプログラムを実行すると何が出力されるでしょう?
func f() { print("sync") }
@_disfavoredOverload
func f() async { print("async") }
func g() async {
await f()
}
await g()
選択肢
A) async
B) sync
答え
A) async
解説
@_disfavoredOverload
はオーバーロード解決の優先順位を下げるattributeです。
しかし「async関数内の関数呼び出しではasyncオーバーロードを優先する」という別のルールの方が優先度が高いため、この例では影響しません。
この優先度は「スコア規則」と呼ばれるルールで決められています。
参考: Swiftのオーバーロード選択のスコア規則21種類#ランク17: SK_SyncInAsync
2. 次のプログラムを実行すると何が出力されるでしょう?
struct User: Hashable {
var id: String
var name: String
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
let set: Set<User> = [
User(id: "1", name: "John"),
User(id: "2", name: "John")
]
print(set.count)
選択肢
A) 0
B) 1
C) 2
答え
C) 2
解説
SetやDictionaryにおいて、基本的にはハッシュ値に基づいて保持する要素の管理が行われますが、ハッシュ値が一致した場合はEquatableによる同値比較を行い一意性を担保する仕様になっています。
この例は意図的にハッシュ値を衝突させていますが、ハッシュは仕組み上、元の値が異なってもハッシュ値が一致する可能性があるため、この仕様は重要な普段でも役割を果たしています。
3. 次のプログラムを実行すると何が出力されるでしょう?
func doSomething() async -> Int {
await withCheckedContinuation { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
continuation.resume(returning: 1)
}
}
}
let task = Task {
let result = await doSomething()
print(result)
}
task.cancel()
await task.value
選択肢
A) 1
B) 出力なし
答え
A) 1
解説
Task.cancel()はTask.isCancelledのフラグを立てるだけなので実際に処理を中止する処理は自分で実装しなければいけません。
今回の例は中止する処理が記述されていないのでTask.cancel()は影響せず3秒後に1が出力されます。
参考: [Swift] async関数とAsyncStreamのキャンセル
4. 次のプログラムを実行すると何が出力されるでしょう?
func doSomethingAsyncStream() -> AsyncStream<Int> {
AsyncStream { continuation in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
continuation.yield(1)
continuation.finish()
}
}
}
let task = Task {
for await result in doSomethingAsyncStream() {
print(result)
}
print("finish")
}
task.cancel()
await task.value
選択肢
A) 1 finish
B) finish
C) 1
D) 出力なし
答え
B) finish
解説
Task.cancel()はTask.isCancelledのフラグを立てるだけなので実際に処理を中止する処理は自分で実装しなければいけません。
しかし、AsyncStreamに対するfor await inはTaskのキャンセル直後に処理を中止します。
AsyncSequenceに対するfor await inはAsyncSequence.Iterator.next()のシンタックスシュガーであり、AsyncStream.Iterator.next()は処理を中止する実装がされているからです。
ここで、for await inはTaskのキャンセル直後に処理を中止しますが、Task {}自体は処理を中止する処理が実装されていないので、print("finish")は実行されます。
参考: [Swift] async関数とAsyncStreamのキャンセル
5. Swift6.0から外部パッケージの型にprotocolを準拠させる際に表示される警告を消すattributeの名前は何でしょうか?
extension Date: @attribute名 Identifiable {
public var id: TimeInterval { timeIntervalSince1970 }
}
選択肢
A) @conform
B) @active
C) @retroactive
D) @foreign_conform
答え
C) @retroactive
解説
(外部のパッケージの型に)後からprotocolを準拠させる機能は便利な一方で、フレームワーク提供側が意図しない利用方法であったり、後から提供側が同じprotocolを別の挙動で準拠させる可能性があると言う危険性があります。その危険性を可視化するための警告と、その警告を消すattributeが追加されました。
参考: SE-0364 Warning for Retroactive Conformances of External Types
6. 次のSwiftUIのコードに対応する表示は次のうちどれでしょう?
Text("Hello, mercari!")
.padding()
.border(Color.red, width: 1)
.offset(x: 50, y: 50)
.border(Color.blue, width: 1)
.overlay(Circle().fill(.green))
選択肢
答え
D)
解説
offsetはレシーバーの「表示位置」だけを変えるmodifierであり、周りのレイアウトやその後のmodifierに影響を与えません。
参考: 公式ドキュメント offset
Day2 (8/24)
- 回答者数: 70名
- 平均点: 2.21/6点
- 全問正解者: 2名
1. 次のプログラムの出力が0になるような演算子はどれでしょう?
let a: UInt8 = 255
let b: UInt8 = 1
print(a 演算子 b) // 0
選択肢
A) +
B) ^
C) |
D) &+
答え
D) &+
解説
Swiftでは+, -, を用いた演算でオーバーフローが発生するとランタイムエラーになります。
一方、&+, &-, &はオーバーフロー演算子と呼ばれ、オーバーフローを許容して桁あがりする(.minに戻る)挙動になります。
2. 次のプログラムを実行すると開始から終了までに (約) 何秒かかるでしょう?
func wait() async {
try? await Task.sleep(for: .seconds(1))
}
await (wait(), wait())
選択肢
A) 1
B) 2
答え
B) 2
解説
Swiftではawaitキーワードを一つにまとめたり、位置を変えることができます。
しかし、関数呼び出しの挙動に影響はないため、awaitを一つにまとめても並列に動作はしません。
並列に動作させる場合はそれぞれの呼び出しをasync letで定義する必要があります。
3. 次のプログラムの出力はSwift6未満で「a」Swift6以上で「b」である。
func f(
_ a: (() -> Void)? = nil,
_ b: (() -> Void)? = nil
) {
if a != nil { print("forward") }
if b != nil { print("backward") }
}
f { }
選択肢
A) a: backward b: backward
B) a: forward b: backward
C) a: backward b: forward
D) a: forward b: forward
答え
C) a: backward b: forward
解説
Swift6の破壊的変更の一つです。
Swift5.3未満はTrailingClosureはbackward scan (クロージャーを末尾の引数から当てはめる)のみでした。
Swift5.3からSwift5.10まではbackwardとforward両方のチェックを行います。backwardとforward両方候補になった場合は互換性維持の観点でbackwardを選択します。
しかし、Swift6.0(もしくは-enable-upcoming-feature ForwardTrailingClosures)からはforward scanのみとなります。
参考: SE-0286 Forward-scan matching for trailing closures
4. 次の呼び出しが当てはまる関数定義はどれでしょうか。
getUserInfo([Seller(), Buyer()])
protocol User {
func getInfo()
}
struct Seller: User {
func getInfo() { /* ... */ }
func listItem() { /* ... */ }
}
struct Buyer: User {
func getInfo() { /* ... */ }
func purchaseItem() { /* ... */ }
}
選択肢
A)
func getUserInfo<U: User>(_ users: [U]) { /* ... */ }
B)
func getUserInfo(_ users: some Sequence<User>) { /* ... */ }
C)
func getUserInfo(_ users: [some User]) { /* ... */ }
D)
func getUserInfo(_ users: [any User]) { /* ... */ }
答え
D)
解説
A: Conflicting arguments to generic parameter ‘U’ (‘Buyer’ vs. ‘Seller’)
B: Cannot convert value of type ‘[Any]’ to expected argument type ‘[any User]’
C: Conflicting arguments to generic parameter ‘some User’ (‘Buyer’ vs. ‘Seller’)
ジェネリクスを用いるとBuyerまたはSellerどちらか一方の型の配列として推論されるため、両方の型の値を持つ配列を受け取ることができません。
参考:
A: Generics
C: Opaque Argument Type (Aのシンタックスシュガー)
D: Existential (Argument) Type
5. この表示に対応するプログラムはどれでしょうか?
選択肢
A)
Image(systemName: "paperplane.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .blue)
B)
Image(systemName: "paperplane")
.symbolVariant(.circle)
.symbolVariant(.fill)
.symbolRenderingMode(.multicolor)
C)
Image(systemName: "paperplane.circle.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
D)
Image(systemName: "paperplane.circle.fill")
.symbolRenderingMode(.monochrome)
.foregroundStyle(.blue)
答え
C)
解説
SF SymbolにsymbolRenderingModeを適応することによって、着色方法を変えることができます。(表示結果 左からA,B,C,Dの順)
参考: 公式ドキュメント symbolRenderingMode
6. 次の関数呼び出しが当てはまる定義をすべて答えてください。
class Super {}
class Sub: Super {}
let a: Sub? = Sub()
f(a)
func f(_ a: Any) {} // A
func f(_ a: Any?) {} // B
func f(_ a: Super?) {} // C
答え
A, B, C(全部)
解説
A: Sub?からAnyへの変換。AnyはOptionalも当てはまります。
B: Sub?からAny?への変換。Optionalの要素SubがAnyに変換されています。
C: Sub?からSuper?への変換。Optionalの要素SubがSuperに変換されています。
まとめ
当日はたくさんの方に挑戦いただきありがとうございました!
躓きやすいSwiftやiOSの仕様をピックアップしたクイズになっているので、この記事の解説がSwiftやiOSへの理解を深める助けになれば嬉しいなと思います。#iosdc #iwillblog