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(…)」と出ていたら、この間違いです。

判断の図。出発点は Int(text) が返す Int? のようなオプショナル値。問い1:妥当な既定値があるか。あれば x ?? 既定値 を使う。なければ問い2:nil のとき関数をそこで終えるべきか。終えるなら guard let x else { return }、終えないなら if let x { … } でブロックの中だけで値を使う。オプショナル値から点線の矢印が強制アンラップ x! に伸びており、nil のときは Unexpectedly found nil でプログラムが落ちる最後の手段として示されている。
どのアンラップを使うか。まず既定値で済むかを考え、次に関数をそこで終えるべきかを考える。強制アンラップ(!)は nil になり得ない値だけに使う最後の手段。
状況 使うもの 理由
既定値で十分(ポート、件数、空文字) 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 file.swift で直接実行すると 6,944 ミリ秒、swiftc -Onone のデバッグビルドは 7,299 ミリ秒、swiftc -O の最適化ビルドは 48 ミリ秒で、約145倍速かった。3つとも同じ合計 774944913 を出力した。
同じ 5,000万回ループ(seed 42 固定)を3回測った中央値。Apple Silicon Mac(M1 Pro)、Swift 6.4.2。swift file.swift は 6,944 ms、swiftc -Onone は 7,299 ms、swiftc -O は 48 ms。

限界とバージョンについて

  • 実行したのは 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台の機械で測った値です。大事なのはミリ秒ではなく比率です。

練習問題

  1. 次のコードはコンパイルできません。Total: 7.5 と表示されるように直してください。

    swift let count = 3 let price = 2.5 print("Total: " + count * price)

  2. text が正しいポート番号(1〜65535)ならその数値を、そうでなければ 8080 を返す func parsePort(_ text: String) -> Int を guard let で書いてください。"3000"、"http"、"70000"、" 80"(先頭に空白)で試します。

  3. 実行する前に、出力される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 の 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 でした。

  • #Swift
  • #初級
  • #オプショナル
  • #型
  • #文字列
  • #swiftc

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑