Swift入門:let・型・文字列・オプショナルを実行して学ぶ
let と var、暗黙変換のない型、整数オーバーフローのクラッシュ、word[0] が書けない理由、オプショナルと強制アンラップの落とし穴を、Swift 6.4.2 の実際の swiftc 出力と実測で確認します。
シリーズ:Swift 講座 1/6(初級)。 Swift をまだ書いたことがない、または Xcode にサンプルを貼り付けた程度の新人エンジニア向けです。読み終えると、ターミナルで Swift を実行し、let と var を使い分け、最初に出会うコンパイルエラーを落ち着いて読み、アプリを落とさずにオプショナルを扱えるようになります。
iOS チームに配属された新人の最初の1週間は、だいたい次のように進みます。Xcode に意味の分からない赤いエラーが出て、先輩に「とりあえず ! を付けておけば」と言われ、翌週テスト担当の端末でアプリが落ちる。この記事はその新人のためのものです。Xcode はいったん開かず、テキストファイル1つとターミナルだけを使います。下に出てくるエラーは、すべて swiftc が実際に出力した文面そのままです。
環境は Apple Silicon Mac(M1 Pro)の Swift 6.4.2 です。入門記事ではあまり触れられない点を1つ:swift file.swift でファイルを1つ実行すると、このツールチェーンはまだ Swift 5 言語モードでコンパイルします(クラッシュログに Compiling with effective version 5.10 と出ます)。完成したコードはすべて、strict concurrency チェックが有効になる -swift-version 6 でも型チェックし、結果が変わる箇所は明記しました。
5分で最初のプログラム
Mac なら Xcode か Command Line Tools に Swift が入っています。swift --version で確認し、hello.swift を作ります。
let name = "Swift"
print("Hello, \(name)!")
$ swift hello.swift
Hello, Swift!
これで完全な Swift プログラムです。main 関数もクラスも要りません。スクリプトとして実行するファイルでは、一番外側(トップレベル)に書いた文が上から順に実行されます。print は標準出力に1行書き、文字列の中の \(name) は文字列補間(string interpolation)、つまり「ここに name の値を入れる」という意味です。
初日から覚えておきたいコマンドは3つです。
| コマンド | 何をするか | いつ使うか |
|---|---|---|
swift file.swift |
最適化なしでメモリ上にコンパイルし、すぐ実行 | お試し、このシリーズの例題 |
swiftc file.swift -o app |
デバッグ版の実行ファイル app を作る |
何度も実行したいとき |
swiftc -O file.swift -o app |
最適化した(Release 相当)実行ファイル | 速度の計測、配布 |
この表の行同士でどれだけ速度が違うかは、最後に実際に測ります。
let と var:基本は定数
let は定数(一度決めたら変えられない名前)、var は変数(再代入できる名前)を宣言します。定数に代入してみると:
let maxUsers = 10
maxUsers = 20
letvar.swift:2:1: error: cannot assign to value: 'maxUsers' is a 'let' constant
1 | let maxUsers = 10
| `- note: change 'let' to 'var' to make it mutable
2 | maxUsers = 20
| `- error: cannot assign to value: 'maxUsers' is a 'let' constant
Swift のエラーは、1行目の ファイル:行:列 とメッセージ、次に note: の順で読みます。note は直す場所を指しており、この例では直し方まで教えてくれています。
逆の間違いはエラーではなく警告です。関数の中で一度も変更されない var は指摘され、Swift 6.x は角括弧で診断グループ名を付けてくれます。この名前でドキュメントを検索できます。
w2.swift:2:9: warning: variable 'rate' was never mutated; consider changing to 'let' constant [#VariableNeverMutated]
w2.swift:3:9: warning: initialization of immutable value 'unused' was never used; consider replacing with assignment to '_' or removing it [#NoUsage]
Swift のチームで期待される習慣は、まず let で書き、コンパイラに言われたときだけ var にすることです。変わりうるものが少ないほど、後でコードを読むときに覚えておくことが減ります。
型、型推論、そして暗黙変換なし
Swift の値にはすべて型があります。Int(整数。今の Mac や iPhone では64ビット)、Double(小数)、String(文字列)、Bool(true/false)が基本です。型を自分で書くことはあまりありません。コンパイラが値から推論するからです。
let count = 3 // Int
let price = 1.5 // Double
let title = "hi" // String
let ok = true // Bool
print(type(of: count), type(of: price), type(of: title), type(of: ok))
// Int Double String Bool
let count: Int = 3 と明示しても構いません。JavaScript や Python から来た人が驚くのは次のルールです。Swift は数値の型を勝手に変換しません。
let count = 3
let price = 1.5
let total = count * price
conv.swift:3:19: error: binary operator '*' cannot be applied to operands of type 'Int' and 'Double'
| `- note: overloads for '*' exist with these partially matching parameter lists: (Double, Double), (Duration, Double), (Int, Int)
文字列も同じで、"年齢: " + 30 は binary operator '+' cannot be applied to operands of type 'String' and 'Int' になります。直し方は明示的な変換です。
let total = Double(count) * price // 4.5
let label = "年齢: \(30)" // 補間なら文字列にしてくれる
もう1点。let t = 3 * 1.5 はコンパイルが通り、Double の 4.5 になります。リテラルの 3 は使われ方が分かるまで型が決まらないからです。エラーになるのは、値が一度 Int の名前に入った後です。リファクタリングで数値を定数に切り出したら急にエラーになる、という現象の正体はこれです。
整数オーバーフローは「一周」ではなく「停止」
Int8 は -128〜127 を表します。C では 127 + 1 が黙って -128 になりますが、Swift は許しません。定数ならコンパイラが止めます。
ovc.swift:1:19: error: arithmetic operation '127 + 1' (on type 'Int8') results in an overflow
実行時にあふれると、プログラムが止まります。次のファイルは2行を出力した後、ループの中であふれます。
let big = Int8.max
print("Int8.max =", big)
let wrapped = big &+ 1
print("big &+ 1 =", wrapped)
var n: Int8 = 120
for _ in 0..<10 { n += 1 }
print(n)
$ swiftc ovf.swift -o ovfd && ./ovfd
Int8.max = 127
big &+ 1 = -128
$ echo $?
133
親切なメッセージはありません。終了コード 133 は 128 + 5 で、シグナル5は SIGTRAP、つまり Swift がわざとトラップ命令を実行したという意味です。swift ovf.swift で実行すると、代わりにコンパイラプロセスの長い Stack dump: が出ました。-O ビルドも同じく止まりました。オーバーフロー検査は最適化で消えません。一周させたいときに使うのが &+、&-、&* で、ハッシュ関数や乱数生成器はこれを使います。
準備中に実際にはまった罠が1つあります。同じ実行ファイルの出力をパイプに流すと(./ovfd | cat や CI のログ)、2行の print が1行も出ませんでした。標準出力は端末でないときバッファにためられ、書き出す前にトラップがプロセスを終わらせたためです。クラッシュ直前のログが「消える」なら、まずこれを疑ってください。
文字列:補間と、word[0] が書けない理由
補間にはどんな式でも入れられます。
let city = "Tōkyō"
let temp = 21.5
print("\(city): \(temp)°C, tomorrow \(temp + 1.5)°C")
// Tōkyō: 21.5°C, tomorrow 23.0°C
次は誰もが一度は書くコードです。
let word = "café"
let first = word[0]
str.swift:2:13: error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
これは意図的な設計です。Swift の Character は人が1文字と見なす単位で、複数の Unicode スカラーや複数のバイトでできていることがあります。日本語で分かりやすいのは濁点です。1文字の「が」(U+304C)と、「か」に結合用の濁点(U+3099)を付けた「が」はスカラー数が違いますが、Swift は同じ1文字として扱います。
let ga1 = "が"
let ga2 = "か\u{3099}"
print(ga1 == ga2, ga1.count, ga2.count, ga1.unicodeScalars.count, ga2.unicodeScalars.count)
// true 1 1 1 2
let word = "café👍🏽"
print("characters:", word.count, "| utf8 bytes:", word.utf8.count, "| utf16 units:", word.utf16.count)
let first = word[word.startIndex]
let third = word[word.index(word.startIndex, offsetBy: 2)]
print(first, third, word.first!, word.last!)
print(Array(word)[3], word.prefix(3))
characters: 5 | utf8 bytes: 13 | utf16 units: 8
c f c 👍🏽
é caf
結合文字の「が」は、macOS のファイル名や一部の入力経路から実際に入ってきます。バイト単位で比べる言語では「同じなのに一致しない」バグになりますが、Swift の == は true でした。その代わり「3文字目」を探すには先頭から数える必要があり、Swift は word[i] が安い処理に見えることを嫌います。代わりに使う道具は:
- よくある用途:
word.first、word.last、word.prefix(n)、word.dropFirst(n) - 本当に位置が必要なとき:
word.index(word.startIndex, offsetBy: n) - 何度もあちこち参照するなら、一度だけ
Array(word)にしてからIntでアクセス
オプショナル:値があるか、ないか
答えがないかもしれない処理はたくさんあります。代表は文字列から数値への変換です。Int("42") はできても Int("四十二") はできません。Swift は「値があるかもしれない」をオプショナル型で表し、疑問符を付けます。Int? は「Int か nil」で、nil は「値がない」ことです。
let a = Int("42") // Int?
let b = Int("4二")
print(a as Any, b as Any)
// Optional(42) nil
Int? を Int のようには使えません。
opt_err.swift:3:7: error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
| |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
| `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'
値を安全に取り出すことをアンラップ(unwrapping)と呼びます。手に取るべき順に4つです。
// 1. if let:ブロックの中だけで値を使う
if let n = Int("42") {
print("parsed:", n * 2) // parsed: 84
}
// 2. guard let:なければ早めに抜け、関数の残りで値を使う
func describe(_ text: String) -> String {
guard let n = Int(text) else {
return "'\(text)' is not a number"
}
return "\(text) doubled is \(n * 2)"
}
print(describe("21")) // 21 doubled is 42
print(describe("abc")) // 'abc' is not a number
// 3. ?? :既定値で代用する
let port = Int("abc") ?? 8080 // 8080
// 4. オプショナルチェーン ?. :途中が nil なら結果も nil
let names: [String] = []
print(names.first?.uppercased() ?? "(nobody)") // (nobody)
guard にはコンパイラが強制するルールがあります。else ブロックは必ずスコープを抜けなければなりません。return を忘れると error: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope です。
最初の週に必ず見る警告もあります。オプショナルをそのまま print("value: \(maybe)") すると value: Optional(5) と表示され、string interpolation produces a debug description for an optional value と警告されます。アプリのラベルに「Optional(…)」と出ていたら、この間違いです。
| 状況 | 使うもの | 理由 |
|---|---|---|
| 既定値で十分(ポート、件数、空文字) | x ?? 既定値 |
1行で分岐なし |
| 関数の残り全体で値が要る | guard let x else { return } |
正常系がインデントされずに続く |
| 小さなブロックだけで値が要る | if let x { … } |
値がブロックの外に漏れない |
| メソッド呼び出しやプロパティ参照だけ | x?.method() |
結果もオプショナル。最後に ?? |
| nil ならバグという値(アプリ同梱のリソース) | x! |
テストで派手に落とす。ユーザー入力には禁止 |
よくある間違い:「とりあえず ! を付ける」
間違い:ユーザー入力を強制アンラップする。
let userInput = "twelve"
let age = Int(userInput)!
print(age + 1)
デバッグビルドで実際に起きること:
$ swiftc force.swift -o forced && ./forced
forced/force.swift:2: Fatal error: Unexpectedly found nil while unwrapping an Optional value
$ echo $?
133
ユーザーが動かすのと同じ最適化ビルドでは:
$ swiftc -O force.swift -o forceo && ./forceo
$ echo $?
133
何も表示されませんでした。 -O ビルドはクラッシュはそのままに、ファイルと行を含むメッセージだけを省きました。実機ではクラッシュレポートが行を示してくれますが、テスト中に見ていた親切な文はありません。直し方は上の表のアンラップのどれかです。
guard let age = Int(userInput) else {
print("年齢を数字で入力してください")
return // または画面にエラーを表示
}
(関数の中で書くコードです。スクリプトのトップレベルでは if let か ?? を使います。)
実測:swift file.swift と swiftc -O
swift file.swift で時間を測って「Swift は遅い」と結論づける初心者は少なくありません。同じループを3通りで実行しました。固定シード(42)から簡単な乱数生成器を5,000万回回すので、どの実行も同じ合計になるはずです。
// Sums a pseudo-random sequence. Fixed seed, so every run computes the same total.
let n = CommandLine.arguments.count > 1 ? Int(CommandLine.arguments[1])! : 50_000_000
var seed: UInt64 = 42
var total = 0
let clock = ContinuousClock()
let elapsed = clock.measure {
for _ in 0..<n {
seed = seed &* 6364136223846793005 &+ 1442695040888963407 // LCG step
total += Int(seed >> 59) // 0...31
}
}
print("n=\(n) total=\(total) time=\(elapsed)")
ContinuousClock はコンパイル時間を含めず、ループだけを測ります。各3回、Apple Silicon Mac(M1 Pro):
| 実行方法 | 計測値(秒) | 中央値 |
|---|---|---|
swift loop.swift |
6.855、6.944、6.955 | 6,944 ms |
swiftc -Onone の実行ファイル |
7.095、7.299、7.981 | 7,299 ms |
swiftc -O の実行ファイル |
0.0481、0.0479、0.0478 | 48 ms |
9回とも total=774944913 を出力しました。最適化ビルドは約145倍速くなりました。最適化なしでは演算子、クロージャ、グローバル変数を書いたとおりに1つずつ処理し、-O ではコンパイラがすべてインライン化してループが数個の機械語命令になります(+= のオーバーフロー検査は残っています)。教訓:swift file.swift や Xcode の Debug ビルドで速度を判断しないこと。 Xcode なら Scheme の Run を Release にするか、Profile で実行します。
限界とバージョンについて
- 実行したのは Swift 6.4.2 だけです。
swift file.swiftは Swift 5 言語モード(effective version 5.10)で、完成コードはすべて-swift-version 6の型チェックも通りました。Swift 6 モードで変わる点を1つ:トップレベルのvar counterを普通のfuncの中で書き換えると、main actor-isolated var 'counter' can not be mutated from a nonisolated contextになります。並行性のチェックで、第5回で扱います。 [#VariableNeverMutated]、[#NoUsage]は最近のコンパイラが付ける診断グループ名です。古い Xcode は同じ警告をこの表示なしで出します。-Oビルドで「Fatal error」の文が消えるのは、このツールチェーンの macOS で確認した結果です。iOS のクラッシュレポートには行情報が残ります。どちらにしてもコンソールの文面に頼らないでください。- 終了コード 133 はシェルが SIGTRAP を報告する形です。Xcode では同じクラッシュがエディタの赤い行と
Thread 1: Fatal errorやEXC_BREAKPOINTとして表示されますが、ここでは説明だけでスクリーンショットは撮っていません。 - 時間は1台の機械で測った値です。大事なのはミリ秒ではなく比率です。
練習問題
-
次のコードはコンパイルできません。
Total: 7.5と表示されるように直してください。swift let count = 3 let price = 2.5 print("Total: " + count * price) -
textが正しいポート番号(1〜65535)ならその数値を、そうでなければ8080を返すfunc parsePort(_ text: String) -> Intをguard letで書いてください。"3000"、"http"、"70000"、" 80"(先頭に空白)で試します。 -
実行する前に、出力される4列を予想してください。
swift let words = ["naïve", "가나다", "ラーメン"] for w in words { let second = w[w.index(after: w.startIndex)] print(w, w.count, w.utf8.count, second) }
解答
1. コンパイラは '*' cannot be applied to operands of type 'Int' and 'Double' と '+' cannot be applied to operands of type 'String' and 'Int' の2つのエラーを出します。明示的に変換して補間を使います。
print("Total: \(Double(count) * price)") // Total: 7.5
2.
func parsePort(_ text: String) -> Int {
guard let n = Int(text), (1...65535).contains(n) else {
return 8080
}
return n
}
print(parsePort("3000"), parsePort("http"), parsePort("70000"), parsePort(" 80"))
// 3000 8080 8080 8080
" 80" が 8080 になるのは、Int(_:) が空白を取り除かないからです。guard にはアンラップと条件をカンマで並べて書けます。
3.
naïve 5 6 a
가나다 3 9 나
ラーメン 4 12 ー
ï は UTF-8 で2バイト、ハングルの音節と日本語の仮名はそれぞれ3バイトです。文字数は人が見る個数です。長音符「ー」も1文字として数えられています。
次の講座
- 次:Swift 講座 2/6:関数・クロージャ・コレクション。引数ラベル、
inout、変数をつかむクロージャ、コピーオンライトの実測 - その後:構造体・クラス・プロトコル、エラー処理とジェネリクス、async/await とアクター、ARC・メモリ・性能
- 他の言語の第1回:Dart 第1回:null safety と型、Python 第1回:型とミュータビリティ、Java 第1回:型・文字列・等価性
よくある質問
Swift の String を word[0] のように整数で取り出せないのはなぜ?
1文字(Character)が複数のバイトや Unicode スカラーでできていることがあり、n 文字目を探すには先頭から数える必要があるからです。word[0] は "'subscript(_:)' is unavailable: cannot subscript String with an Int" というエラーになります。word.first、word.prefix(n)、word.index(word.startIndex, offsetBy: n) を使います。
「Fatal error: Unexpectedly found nil while unwrapping an Optional value」の意味は?
nil のオプショナルに ! を付けたという意味です。たとえば Int("twelve")! です。プログラムは終了コード 133 で止まります。デバッグビルドではファイルと行つきでこのメッセージが出ましたが、swiftc -O ビルドではメッセージなしで終了しました。if let、guard let、?? に置き換えます。
Swift の整数オーバーフローは C のように値が一周しますか?
しません。Int8 の 127 に 1 を足すと、デバッグでも -O でも終了コード 133(トラップ)で止まりました。let x: Int8 = 127 + 1 のような定数はコンパイルすら通りません。一周させたいときは &+、&-、&* を使います。127 &+ 1 は -128 でした。