初心者必見!Swift Stringの基本の「き」

はい、承知いたしました。SwiftのString(文字列)の基本について、初心者の方にも分かりやすく、詳細に解説した約5000語の記事を作成します。

以下が記事の内容です。


初心者必見!Swift Stringの基本の「き」

プログラミングの世界へようこそ! Swiftは、Appleが開発したパワフルで直感的なプログラミング言語です。iPhone, iPad, Mac, Apple Watch, Apple TV向けのアプリ開発はもちろん、サーバーサイド開発やその他のプラットフォームでも利用されています。

Swiftでプログラミングをする上で、最も基本的な要素の一つが「文字列(String)」です。画面に何かを表示したり、ユーザーからの入力を受け取ったり、ファイル名を扱ったりと、文字列はあらゆる場面で登場します。

しかし、SwiftのStringは他の多くの言語と比べて少しユニークな特徴を持っています。特にUnicodeの扱いに関しては、Swiftの設計思想が強く反映されており、最初は戸惑うかもしれません。

この記事では、SwiftのStringについて、「基本の「き」」から徹底的に解説します。文字の並びとして見えているStringが、内部でどのように扱われているのか、基本的な操作はどのように行うのかを、豊富なコード例とともに学んでいきましょう。

この記事を読み終える頃には、SwiftのStringの基本的な扱いに自信が持てるようになっているはずです。さあ、Stringsの世界へ踏み込みましょう!

はじめに:String(文字列)とは?なぜ重要?

まず、プログラミングにおける「文字列」とは何かを考えてみましょう。

簡単に言うと、「文字が複数並んだもの」です。「Hello, World!」や「ユーザー名」、「メールアドレス」、「ファイルパス」など、私たちが普段使っている自然言語のテキストデータは、ほとんどが文字列として扱われます。

プログラミングでは、これらのテキストデータを扱うための専用の「型」が用意されています。Swiftでは、それが String 型です。

なぜStringが重要なのでしょうか?

  1. 情報の表現: ユーザーへのメッセージ表示、データのラベル付けなど、人間が理解できる形式で情報をやり取りするために文字列は不可欠です。
  2. ユーザーとの対話: アプリやプログラムは、ユーザーからのテキスト入力を受け取ったり、テキストで応答したりします。
  3. データの格納と処理: ファイルの内容、データベースのエントリ、ネットワーク経由で受け取るデータなど、多くの情報がテキスト形式で格納され、処理されます。

このように、Stringはプログラミングにおいて、数値や真偽値(はい/いいえ)と同じくらい、いや、それ以上に頻繁に登場する非常に重要な要素なのです。

Swiftの String 型は、ただ文字を並べるだけでなく、非常に安全で効率的に文字列を扱えるように設計されています。特に、世界中のあらゆる言語や記号を表現できる「Unicode」への対応が強力です。

SwiftにおけるStringの基本

SwiftのStringは、他の多くのプログラミング言語の文字列型と比較していくつかの特徴があります。最も重要なのは以下の2点です。

  1. 値型 (Value Type) であること: Swiftの String は構造体(struct)として実装されており、値型です。これは、文字列を別の変数や定数に代入したり、関数の引数として渡したりする際に、文字列の内容全体がコピーされるということを意味します。元の文字列を変更しても、コピー先には影響しません。これに対し、多くの他の言語では参照型として扱われ、コピーしても同じデータを指しているだけ、という場合があります。値型であることは、コードの予測可能性を高め、意図しない副作用を防ぐ上で重要です。
  2. Unicodeへの強力な対応: SwiftのStringは、Unicodeの「書記素クラスター(Grapheme Cluster)」という単位で文字を扱います。これは、私たちが目で見て「一つの文字」と認識する単位です。例えば、「é」は、基本の「e」とアクサン記号「´」の組み合わせですが、Swiftではこれを一つの文字として扱います。また、絵文字や、複数の国旗記号を組み合わせた絵文字なども、正しく一つの文字として扱われます。この設計により、世界中の多様なテキストを安全かつ正確に扱えます。

この後、これらの特徴が具体的なコードにどう現れるのかを見ていきましょう。

第1章:文字列の生成と宣言

まずは、Swiftで文字列をどのように作るのか、基本的な方法から学びます。

1.1 文字列リテラル (String Literals)

最も簡単でよく使われる文字列の作り方は、「文字列リテラル」を使う方法です。文字列リテラルとは、プログラムコード中に直接記述された、固定の文字列のことです。Swiftでは、ダブルクォーテーションマーク(")で囲むことで文字列リテラルを表現します。

“`swift
let greeting = “Hello, Swift!”
let productName = “iPhone 15 Pro”
let emptyStringLiteral = “” // 何も含まない空の文字列もリテラルで表現できます

print(greeting)
print(productName)
print(emptyStringLiteral)
“`

この例では、greetingproductNameemptyStringLiteral という定数(let)を宣言し、それぞれに文字列リテラルで指定した文字列を代入しています。

1.2 複数行文字列リテラル (Multiline String Literals)

長い文章や、改行を含む文字列を記述したい場合、複数行文字列リテラルが便利です。これは、トリプルダブルクォーテーションマーク(""")で囲むことで表現します。

“`swift
let poem = “””
これは
複数行の
文字列です。
新しい行に
テキストを記述できます。
“””

print(poem)

// 出力:
// これは
// 複数行の
// 文字列です。
// 新しい行に
// テキストを記述できます。
“`

複数行文字列リテラルでは、開始の """ と終了の """ の間に書かれた内容がそのまま文字列となります。ただし、終了の """ を記述する行のインデント(字下げ)は重要です。終了の """ と同じレベルのインデント、またはそれより少ないインデントの空白は、文字列の先頭から自動的に取り除かれます。

“`swift
let indentedPoem = “””
ここから始まります。
これはインデントされた行です。
さらにインデントされた行です。
“”” // 終了の”””のインデントレベルに注目

print(indentedPoem)

// 出力:
// ここから始まります。
// これはインデントされた行です。
// さらにインデントされた行です。
“`

この例では、終了の """ が2つの空白でインデントされているため、各行の先頭から最大2つの空白が取り除かれています。最後の行は終了の """ よりインデントが深いため、そのインデントは文字列に残っています。この機能のおかげで、コード全体のインデントを崩さずに、複数行文字列を見やすく記述できます。

1.3 空文字列の生成

何も文字を含まない「空文字列」は、前述の文字列リテラル "" で作る方法の他に、String() イニシャライザを使って生成することもできます。

“`swift
let emptyString1 = “”
let emptyString2 = String() // String型を初期化して空文字列を生成

print(emptyString1)
print(emptyString2)

print(emptyString1 == emptyString2) // true
“`

どちらの方法でも、完全に同じ空文字列が生成されます。どちらを使うかは好みや文脈によりますが、多くの場合、シンプルなので "" が使われます。

1.4 letvar: 文字列の不変性 (Immutable) と可変性 (Mutable)

Swiftでは、変数や定数を宣言する際に let または var を使います。これはStringにも当てはまります。

  • let (定数): let で宣言された文字列は、一度値を設定すると変更できません(不変/Immutable)
  • var (変数): var で宣言された文字列は、後から変更できます(可変/Mutable)

“`swift
let immutableString = “Hello”
var mutableString = “World”

// immutableString = “Goodbye” // エラー: letで宣言されているため変更不可

mutableString = “Swift” // OK: varで宣言されているため変更可能

print(immutableString)
print(mutableString)
“`

Stringが値型であることを考えると、var mutableString = "World" と宣言した後、mutableString = "Swift" と別の文字列を代入すると、元の "World" という文字列のデータは破棄され、新たに "Swift" という文字列のデータが作成されて mutableString に関連付けられます。

もし、文字列の内容を後から変更(例えば、他の文字列を結合したり、一部を置換したり)する可能性がある場合は var で宣言します。そうでない場合は、不変である let を使うのが Swift の推奨されるプラクティスです。let を使うことで、意図しない場所で値が変更されることを防ぎ、コードの安全性が高まります。

第2章:文字列の基本操作

文字列を生成できるようになったら、次はその内容を操作したり、調べたりする方法を学びます。

2.1 文字列の結合 (Concatenation)

複数の文字列を一つに繋げることを「結合」と言います。Swiftでは、プラス演算子(+)を使って文字列を結合できます。

“`swift
let part1 = “Hello”
let part2 = ” ”
let part3 = “Swift!”

let combinedString = part1 + part2 + part3
print(combinedString) // 出力: Hello Swift!

let greeting = “Good” + ” ” + “Morning”
print(greeting) // 出力: Good Morning

var mutableGreeting = “Hello”
mutableGreeting += ” World” // += 演算子で文字列を追記
print(mutableGreeting) // 出力: Hello World
“`

+ 演算子を使うと、新しい文字列が生成されます。+= 演算子を使うと、既存の var で宣言された文字列の末尾に別の文字列を追記できます。これは特に可変な文字列を少しずつ構築していく場合に便利です。

2.2 文字列補間 (String Interpolation)

文字列の中に、変数や定数、式の結果などを埋め込んで新しい文字列を作る機能を「文字列補間」と呼びます。Swiftでは、バックラッシュと丸括弧(\())を使います。

“`swift
let name = “Alice”
let age = 30
let price = 1980

// 変数を埋め込む
let message1 = “こんにちは、(name)さん!”
print(message1) // 出力: こんにちは、Aliceさん!

// 式の結果を埋め込む
let message2 = “あなたの年齢は(age)歳です。”
print(message2) // 出力: あなたの年齢は30歳です。

let total = price * 1.1 // 税込み価格を計算
let message3 = “商品の価格は(price)円(税込み(total)円)です。”
print(message3) // 出力: 商品の価格は1980円(税込み2178.0円)です。

// 文字列リテラル中に改行を埋め込む
let multilineMessage = “””
こんにちは、(name)さん!
あなたの年齢は(age)歳ですね。
“””
print(multilineMessage)
“`

文字列補間は、複数の文字列を結合するよりも、多くの場合コードが読みやすく、簡潔になります。特に、数値や他の型の値を文字列に含めたい場合に非常に便利です。\() の中には、文字列に変換できる値や、評価結果が文字列に変換できる式なら何でも記述できます。

2.3 空文字列かどうかの判定 (isEmpty)

文字列が空(何も文字を含まない)かどうかを調べるには、isEmpty プロパティを使います。これは Bool 型(真偽値)を返します。

“`swift
let string1 = “”
let string2 = “Hello”

if string1.isEmpty {
print(“string1は空です。”) // 出力: string1は空です。
}

if !string2.isEmpty { // isEmptyがfalseなので、!を付けて条件を反転
print(“string2は空ではありません。”) // 出力: string2は空ではありません。
}
“`

2.4 文字数(Count)の取得 (count)

文字列に含まれる文字の数を取得するには、count プロパティを使います。

“`swift
let text = “Hello, Swift!”
print(text.count) // 出力: 13

let greetingInJapanese = “こんにちは”
print(greetingInJapanese.count) // 出力: 5
“`

一見簡単ですが、SwiftのStringの count は、他の言語の文字数カウントと異なる場合があります。これは、前述した「書記素クラスター(Grapheme Cluster)」の概念に基づいているためです。

例えば、アクセント付きの文字や絵文字を見てみましょう。

“`swift
let accentedChar = “é” // ‘e’ + ‘´’
print(accentedChar.count) // 出力: 1 (見た目では1つの文字)

let familyEmoji = “👨‍👩‍👧‍👦” // 複数のUnicodeコードポイントの組み合わせ
print(familyEmoji.count) // 出力: 1 (見た目では1つの文字)

let regionalIndicator = “🇰🇷” // ‘K’ + ‘R’ のようなリージョナルインジケーターの組み合わせ
print(regionalIndicator.count) // 出力: 1 (見た目では1つの文字)
“`

このように、Swiftの count は、ユーザーが目で見て一つの文字と認識する単位(書記素クラスター)の数を返します。これは、Unicodeの内部的な表現(例えば、UTF-8やUTF-16でのバイト数、またはUnicodeスカラー値の数)とは異なる場合があるため、注意が必要です。Stringのcount は、画面に表示される文字の「幅」や「カーソル移動」の単位に近いと考えると理解しやすいかもしれません。

なぜSwiftがこのような設計になっているのか、Unicodeと書記素クラスターについては後ほど詳しく掘り下げます。

2.5 文字列の比較 (==)

二つの文字列が同じ内容であるかを確認するには、等価演算子(==)を使います。等しくないかを確認するには、不等価演算子(!=)を使います。

“`swift
let stringA = “apple”
let stringB = “apple”
let stringC = “orange”

if stringA == stringB {
print(“stringAとstringBは同じです。”) // 出力: stringAとstringBは同じです。
}

if stringA != stringC {
print(“stringAとstringCは異なります。”) // 出力: stringAとstringCは異なります。
}

let caseSensitiveA = “Apple”
let caseSensitiveB = “apple”

if caseSensitiveA == caseSensitiveB {
print(“これは表示されません。”)
} else {
print(“大文字と小文字は区別されます。”) // 出力: 大文字と小文字は区別されます。
}
“`

Swiftの文字列比較は、デフォルトで大文字と小文字を区別します。また、Unicodeの正規化形式(Normalization Form)にも対応しており、見た目は同じだが内部的な表現が異なる文字の並びも等しいと判定します(例: é を「e + ´」と表現した場合と、単一のUnicode文字として表現した場合)。これは、国際化されたアプリケーションで正確な文字列比較を行う上で重要な機能です。

もし大文字・小文字を区別せずに比較したい場合は、文字列を全て大文字または小文字に変換してから比較するという方法があります。

“`swift
let caseInsensitiveA = “Apple”
let caseInsensitiveB = “apple”

if caseInsensitiveA.lowercased() == caseInsensitiveB.lowercased() {
print(“大文字小文字を区別しないと、この2つは同じです。”) // 出力: 大文字小文字を区別しないと、この2つは同じです。
}
“`

大文字・小文字変換については、後述のよく使うメソッドのセクションで詳しく説明します。

第3章:文字列へのアクセス

特定の場所にある文字を取り出したり、文字列の一部を操作したりするには、文字列の個々の要素(文字)にアクセスする必要があります。しかし、SwiftのStringは、他の多くの言語のように myString[0] のように整数インデックスで直接アクセスすることはできません。これも、SwiftのStringがUnicodeの書記素クラスターを単位としていることと関係があります。

3.1 なぜ整数インデックスでアクセスできないのか?

先ほど、.count の説明で触れたように、SwiftのStringは「書記素クラスター」の並びとして扱われます。一つの書記素クラスター(私たちが「文字」と認識するもの)は、内部的には1つ以上のUnicodeスカラー値から構成されます。そして、これらのUnicodeスカラー値は、UTF-8やUTF-16といったエンコーディング方式によって、異なる数のバイトで表現される可能性があります。

例えば、

  • "a" は UTF-8 で 1 バイト
  • "é" は UTF-8 で 2 バイト (e + ´)
  • "😊" (笑顔の絵文字) は UTF-8 で 4 バイト
  • "👨‍👩‍👧‍👦" (家族の絵文字) は UTF-8 でさらに多くのバイト

もし整数インデックス(0, 1, 2, …)で直接アクセスできるようにしてしまうと、例えば myString[3] がどの「書記素クラスター」に対応するのかをすぐに判断できません。なぜなら、最初の3つの書記素クラスターがそれぞれ何バイトを占めるかによって、4番目の書記素クラスターが始まるバイト位置は文字列によって異なるからです。Swiftは安全性を重視しているため、このような曖昧で、かつパフォーマンスの予測が難しい整数インデックスによる直接アクセスを許可していません。

代わりに、Swiftは String.Index という専用の型を使って文字列内の位置を表します。

3.2 String.Index 型について

String.Index は、SwiftのString内の特定の「書記素クラスター」の位置を安全に示すための型です。この型は、文字列の内部構造を考慮しており、異なるエンコーディングや複雑な書記素クラスターの場合でも、常に正しい文字の境界を指し示します。

String.Index は直接整数のように操作するのではなく、String型が提供するメソッドを使って取得・移動させます。

3.3 文字列の開始位置と終了位置

全ての文字列には、最初の文字の位置を示す startIndex と、最後の文字の直後の位置を示す endIndex というプロパティがあります。

“`swift
let greeting = “Hello”

let firstIndex = greeting.startIndex // 最初の文字 ‘H’ の位置
let lastIndex = greeting.endIndex // 最後の文字 ‘o’ の直後の位置

print(“最初の文字の位置: (firstIndex)”) // 出力例: 最初の文字の位置: Index(_offset: 0)
print(“終了位置: (lastIndex)”) // 出力例: 終了位置: Index(_offset: 5)

// endIndexは最後の文字の直後なので、そのままアクセスしようとするとエラーになります
// print(greeting[lastIndex]) // ランタイムエラー
“`

String.Index の具体的な値(例えば _offset: 0_offset: 5)は、内部的な表現に依存するため、直接このオフセット値を使ってインデックスを生成したり操作したりすることは推奨されません。常にStringのメソッドを使ってIndexを取得・操作することが重要です。

3.4 インデックスの移動

startIndexendIndex を基準に、文字列内のインデックスを移動させるには、以下のメソッドを使います。

  • index(after: i): 指定されたインデックス i の次の文字の位置を返します。
  • index(before: i): 指定されたインデックス i の前の文字の位置を返します。
  • index(_:offsetBy: n): 指定されたインデックスから n 個(n が正なら後方、負なら前方)移動した位置を返します。
  • index(_:offsetBy: n, limit: limitIndex): offsetBy と同じですが、limitIndex に到達する前に指定されたオフセットに達した場合のみインデックスを返します。指定されたオフセットが limitIndex を超える場合は limitIndex を返します。これは範囲外アクセスを防ぐために便利です。

これらのメソッドは、移動後のインデックスを返します。

“`swift
let text = “Swift” // S(0) w(1) i(2) f(3) t(4) endIndex(5)

let firstIndex = text.startIndex // S の位置
let secondIndex = text.index(after: firstIndex) // w の位置
let lastCharIndex = text.index(before: text.endIndex) // t の位置

print(“最初の文字の次:”, text[secondIndex]) // 出力: w
print(“最後の文字:”, text[lastCharIndex]) // 出力: t

// startIndexから2つ後ろに移動
let twoStepsForward = text.index(text.startIndex, offsetBy: 2) // i の位置
print(“startIndexから2つ後ろ:”, text[twoStepsForward]) // 出力: i

// endIndexから3つ前に移動
let threeStepsBackward = text.index(text.endIndex, offsetBy: -3) // i の位置
print(“endIndexから3つ前:”, text[threeStepsBackward]) // 出力: i

// 存在しない位置へのアクセスはエラーになります
// let outOfBoundsIndex = text.index(text.startIndex, offsetBy: 10) // ランタイムエラー!
// print(text[outOfBoundsIndex])
“`

index(_:offsetBy:) は、指定されたオフセットが文字列の範囲外になる場合、ランタイムエラー fatal error: String index is out of bounds を発生させます。これを防ぐためには、limit パラメータを指定するか、事前にインデックスが有効な範囲にあるか確認する必要があります。

“`swift
let text = “Swift”
let startIndex = text.startIndex
let endIndex = text.endIndex

// startIndexから10個後ろに移動しようとするが、limitをendIndexに設定
if let potentialIndex = text.index(startIndex, offsetBy: 10, limit: endIndex) {
// potentialIndexはendIndexになります (10個移動する前にendIndexに到達するため)
if potentialIndex == endIndex {
print(“指定されたオフセットは文字列の範囲を超えています(またはちょうどendIndexです)。”) // 出力
} else {
print(“移動後のインデックスは有効です。”)
// print(text[potentialIndex]) // この行は実行されない
}
} else {
// offsetByの移動方向とlimitIndexの方向が逆の場合など、offsetByの方向とは逆向きにしか移動できない場合にnilが返る
// このoffsetByが正、limitがendIndexの場合は通常nilにならない
print(“インデックスの移動に失敗しました。”)
}

// endIndexから10個前に移動しようとするが、limitをstartIndexに設定
if let potentialIndex = text.index(endIndex, offsetBy: -10, limit: startIndex) {
// potentialIndexはstartIndexになります
if potentialIndex == startIndex {
print(“指定されたオフセットは文字列の範囲を超えています(またはちょうどstartIndexです)。”) // 出力
} else {
print(“移動後のインデックスは有効です。”)
// print(text[potentialIndex])
}
} else {
// このoffsetByが負、limitがstartIndexの場合は通常nilにならない
print(“インデックスの移動に失敗しました。”)
}
“`

limit パラメータを使うと、指定されたオフセットに到達する前にリミットに達した場合、移動をそこで停止してリミットのインデックスを返してくれます。これにより、範囲外エラーを防ぎながら安全にインデックスを計算できます。

3.5 文字列のイテレーション(繰り返し処理)

文字列の各文字(書記素クラスター)に対して順番に処理を行いたい場合は、for-in ループを使います。

“`swift
let word = “Hello”

for character in word {
print(character)
}

// 出力:
// H
// e
// l
// l
// o

let emojiWord = “😊World”
for character in emojiWord {
print(character)
}

// 出力:
// 😊
// W
// o
// r
// l
// d
“`

for-in ループを使うと、文字列内の各書記素クラスターが自動的に Character 型の値として取り出され、ループ内で利用できます。これが、Swiftで文字列の各「文字」にアクセスする最も一般的で安全な方法です。

3.6 特定のインデックスにある文字へのアクセス

インデックス型を使って、特定の場所にある文字にアクセスするには、部分文字列にアクセスするのと同様のブラケット構文([])を使いますが、インデックスは必ず String.Index 型でなければなりません。

“`swift
let poem = “Swift is fun!”
let firstCharIndex = poem.startIndex
let thirdCharIndex = poem.index(poem.startIndex, offsetBy: 2) // S(0) w(1) i(2) -> ‘i’
let lastCharIndex = poem.index(before: poem.endIndex)

let firstCharacter = poem[firstCharIndex]
let thirdCharacter = poem[thirdCharIndex]
let lastCharacter = poem[lastCharIndex]

print(“最初の文字: (firstCharacter)”) // 出力: S
print(“3番目の文字: (thirdCharacter)”) // 出力: i
print(“最後の文字: (lastCharacter)”) // 出力: !
“`

この場合も、指定するインデックスが文字列の有効な範囲外であると、ランタイムエラーが発生します。インデックスが有効な範囲にあることを確認するか、安全な方法(例えば limit を使うなど)でインデックスを計算することが重要です。

第4章:部分文字列 (Substrings) とは

元の文字列の一部を取り出して、新しい文字列として扱いたい場合があります。例えば、「Hello, World!」から「World」の部分だけを取り出すなどです。Swiftでは、このような部分を取り出す操作は Substring という型を返します。

4.1 Substringの生成

文字列から部分文字列を取り出すには、通常、範囲(Range)とインデックスを使います。よく使われるメソッドには以下のようなものがあります。

  • prefix(n): 先頭から n 個の文字を含む部分文字列を返します。
  • suffix(n): 末尾から n 個の文字を含む部分文字列を返します。
  • ブラケット構文 [range]: 指定された範囲のインデックスに含まれる部分文字列を返します。範囲は、startIndex..<endIndex のような Range<String.Index> 型で指定します。

“`swift
let originalString = “Hello, Swift World!”

// 先頭から5文字
let prefixSubstring = originalString.prefix(5) // “Hello”
print(prefixSubstring)
print(type(of: prefixSubstring)) // 出力: Substring.Type

// 末尾から6文字
let suffixSubstring = originalString.suffix(6) // “World!”
print(suffixSubstring)
print(type(of: suffixSubstring)) // 出力: Substring.Type

// 特定の範囲(インデックスを使って範囲を作成)
let startIndex = originalString.index(originalString.startIndex, offsetBy: 7) // S の位置
let endIndex = originalString.index(startIndex, offsetBy: 5) // S..t の範囲の直後 (World の前のスペースから t の直後まで)

// 特定の範囲(インデックスを使って範囲を作成)
// ここで “Swift” の部分を取りたい
let swiftStartIndex = originalString.index(originalString.startIndex, offsetBy: 7) // ‘S’ の位置
let swiftEndIndex = originalString.index(swiftStartIndex, offsetBy: 5) // ‘t’ の直後の位置

let rangeSubstring = originalString[swiftStartIndex..<swiftEndIndex] // “Swift”
print(rangeSubstring)
print(type(of: rangeSubstring)) // 出力: Substring.Type

// もう少し簡単な範囲の作り方例
let commaIndex = originalString.firstIndex(of: “,”)! // “,” の位置 (後述のcontainsメソッドで詳しく)
let spaceAfterSwiftIndex = originalString.index(originalString.firstIndex(of: “S”)!, offsetBy: 5) // “t” の直後の位置

let partAfterComma = originalString[originalString.index(after: commaIndex)…] // “, ” 以降全て -> ” Swift World!”
let partBeforeComma = originalString[.. “Hello”
let swiftWord = originalString[originalString.index(commaIndex, offsetBy: 2)..<originalString.index(commaIndex, offsetBy: 7)] // “Swift”

print(partAfterComma)
print(partBeforeComma)
print(swiftWord)
“`

これらのメソッドが返す型は Substring です。SubstringString と非常によく似たインターフェース(プロパティやメソッド)を持っているので、ほとんどのString操作は Substring に対しても行えます。

4.2 SubstringとStringの違い(メモリ管理)

SubstringString と異なる最も重要な点は、そのメモリ管理です。

Substring は、元の String が使用しているメモリ領域を共有します。つまり、Substring を作っても、新しい大きな文字列データは作成されません。これは、大きな文字列から小さな部分文字列を頻繁に取り出すような場合に、非常に効率的です。

“`swift
let largeString = “非常に長い文字列… (仮に数MBのデータとします)”
let smallSubstring = largeString.prefix(10) // largeStringのメモリを共有

// この時点では、largeString全体のメモリが解放されません。
// smallSubstringがlargeStringのメモリの一部を参照し続けているからです。
“`

しかし、このメモリ共有には注意が必要です。もし元の文字列が非常に大きいのに、それから取り出した Substring が非常に小さい場合、その小さな Substring が元の大きな文字列全体のメモリを保持し続けることになります。これは、意図しないメモリ消費につながる可能性があります。

例えば、数MBのテキストファイルの内容を一つの String に読み込み、その中からエラーメッセージだけを取り出してログに記録したいとします。

“`swift
func findErrorMessage(logContent: String) -> Substring? {
if let range = logContent.range(of: “ERROR:”) {
let errorSubstring = logContent[range.lowerBound…]
return errorSubstring // Substringを返す
}
return nil
}

let fileContent = “ログファイルの非常に長い内容…\nERROR: Something went wrong\n…\n” // 仮に巨大な文字列

let errorMessage = findErrorMessage(logContent: fileContent) // Substringが返される

// ここでfileContentはもう使わないとする
// しかし、errorMessageがfileContentのメモリを共有しているため、
// errorMessageが存在する限り、fileContentのメモリは解放されない可能性がある。
“`

このような場合に、取り出した部分文字列を別の String として完全に独立させたい場合は、SubstringString() イニシャライザを使って String 型に変換します。

4.3 SubstringからStringへの変換

Substring を新しい、独立した String オブジェクトに変換するには、String() イニシャライザを使います。

“`swift
let originalString = “Hello, Swift World!”
let swiftSubstring = originalString[originalString.index(originalString.startIndex, offsetBy: 7)..<originalString.index(originalString.startIndex, offsetBy: 12)] // “Swift” (Substring)

let independentString = String(swiftSubstring) // “Swift” (String)
print(type(of: independentString)) // 出力: String.Type

// independentStringは元のoriginalStringとは独立したメモリを持つ
“`

String() イニシャライザで SubstringString に変換すると、Substring が参照していた内容が新しいメモリ領域にコピーされます。これにより、新しい String は元の文字列とは無関係になり、元の大きな文字列が不要になった場合はメモリが解放されるようになります。

一般的に、文字列操作メソッドが Substring を返すのはパフォーマンス上の理由(不要なコピーを防ぐため)ですが、その Substring を長期間保持する場合や、元の文字列が非常に大きい場合は、明示的に String() で変換することを検討すると良いでしょう。

第5章:よく使う文字列メソッド

SwiftのString型には、様々な便利な操作を行うためのメソッドやプロパティが豊富に用意されています。ここでは、特によく使う基本的なものをいくつか紹介します。

5.1 大文字・小文字変換

文字列全体を全て大文字、または全て小文字に変換した新しい文字列を取得できます。

  • uppercased(): 全て大文字に変換したStringを返します。
  • lowercased(): 全て小文字に変換したStringを返します。

“`swift
let mixedCase = “SwIfT ProGrAmMiNg”

let upper = mixedCase.uppercased()
let lower = mixedCase.lowercased()

print(upper) // 出力: SWIFT PROGRAMMING
print(lower) // 出力: swift programming
“`

これらのメソッドは元の文字列を変更するのではなく、新しい文字列を返します。

5.2 空白文字の除去 (trimmingCharacters(in:))

文字列の先頭や末尾にある空白文字や改行などの不要な文字を取り除いた新しい文字列を取得できます。

  • trimmingCharacters(in: characterSet): 指定された CharacterSet に含まれる文字を、文字列の先頭と末尾から取り除いたStringを返します。CharacterSet.whitespacesAndNewlines がよく使われます。

“`swift
let messyString = ” Hello, World! \n “

let trimmedString = messyString.trimmingCharacters(in: .whitespacesAndNewlines)

print(“元の文字列: ‘(messyString)'”)
print(“整形後の文字列: ‘(trimmedString)'”)

// 出力:
// 元の文字列: ‘ Hello, World!
// ‘
// 整形後の文字列: ‘Hello, World!’
“`

5.3 含まれているかの判定

ある部分文字列や文字が文字列の中に含まれているか、または特定の文字で始まるか終わるかなどを判定できます。

  • contains(substring): 指定された部分文字列が含まれているかを Bool で返します。
  • hasPrefix(prefix): 指定された接頭辞(先頭部分)で始まるかを Bool で返します。
  • hasSuffix(suffix): 指定された接尾辞(末尾部分)で終わるかを Bool で返します。

“`swift
let sentence = “Swift is a powerful language.”

print(sentence.contains(“Swift”)) // 出力: true
print(sentence.contains(“java”)) // 出力: false
print(sentence.contains(“power”)) // 出力: true

print(sentence.hasPrefix(“Swift”)) // 出力: true
print(sentence.hasPrefix(“swift”)) // 出力: false (大文字小文字を区別)
print(sentence.hasPrefix(“Language”))// 出力: false

print(sentence.hasSuffix(“language.”))// 出力: true
print(sentence.hasSuffix(“language”)) // 出力: false (ピリオドがないため)
“`

これらのメソッドも、デフォルトでは大文字小文字を区別します。区別したくない場合は、両方の文字列を lowercased()uppercased() で変換してから比較することを検討します。

5.4 置換 (replacingOccurrences(of:with:))

文字列中のある部分文字列を、別の部分文字列に置き換えた新しい文字列を取得できます。

  • replacingOccurrences(of: target, with: replacement): 文字列中にある target を全て replacement で置き換えたStringを返します。

“`swift
let original = “Hello, World! Hello, Swift!”

let newString = original.replacingOccurrences(of: “Hello”, with: “Hi”)
print(newString) // 出力: Hi, World! Hi, Swift!

let anotherReplacement = original.replacingOccurrences(of: “!”, with: “?”)
print(anotherReplacement) // 出力: Hello, World? Hello, Swift?
“`

5.5 分割 (split(separator:))

特定の区切り文字(Separator)を使って文字列を分割し、部分文字列の配列を取得できます。

  • split(separator: separatorChar): 指定された文字 separatorChar で文字列を分割し、[Substring] 型の配列を返します。

“`swift
let csvLine = “Apple,Banana,Orange”

let fruits = csvLine.split(separator: “,”) // [Substring] 型の配列
print(fruits) // 出力: [“Apple”, “Banana”, “Orange”]
print(type(of: fruits)) // 出力: Array.Type

// 必要に応じてString型に変換する
let fruitsAsStrings = fruits.map { String($0) }
print(fruitsAsStrings) // 出力: [“Apple”, “Banana”, “Orange”]
print(type(of: fruitsAsStrings)) // 出力: Array.Type

let sentence = “Swift is fun.”
let words = sentence.split(separator: ” “) // 半角スペースで分割
print(words) // 出力: [“Swift”, “is”, “fun.”]
“`

split(separator:) は、デフォルトでは区切り文字自身は結果に含まれません。また、複数の区切り文字が連続している場合や、文字列の先頭・末尾が区切り文字の場合は、空の Substring が含まれる可能性があります。挙動を細かく制御したい場合は、より詳細なオプションを持つオーバーロード版メソッドや、他の文字列操作メソッドを組み合わせる必要があります。

第6章:UnicodeとSwift Stringのより深い理解

SwiftのStringを本当に理解するためには、Unicodeと「書記素クラスター」の概念が不可欠です。これらは、SwiftのStringがなぜ他の言語と異なる振る舞いをするのか(特にインデックスやカウントに関して)を説明します。

6.1 Unicodeとは?

Unicodeは、世界中の文字、記号、絵文字などを一つ一つに固有の「コードポイント」と呼ばれる番号を割り当てた国際的な文字集合の規格です。これにより、異なる言語やシステム間でも文字化けすることなくテキストを交換できるようになりました。

例えば:
* ‘A’ のコードポイントは U+0041
* ‘あ’ のコードポイントは U+3042
* ‘😊’ のコードポイントは U+1F60A

6.2 エンコーディング (UTF-8, UTF-16など)

Unicodeで定義されたコードポイントを、コンピューターのメモリやファイルに保存するために、バイト列に変換する方式を「エンコーディング」と呼びます。代表的なエンコーディング方式には、UTF-8、UTF-16、UTF-32などがあります。

  • UTF-8: 1バイトから4バイトまでの可変長でコードポイントを表現します。ASCII文字は1バイトで表現できるため、英語圏で広く使われています。日本語なども表現可能です。インターネット上で最も広く使われているエンコーディングです。
  • UTF-16: 16ビット(2バイト)または32ビット(4バイト)の可変長でコードポイントを表現します。古いWindows内部などで使われていましたが、最近はUTF-8が主流です。Swiftの内部では、StringはUTF-16で効率的に操作されるように設計されています。
  • UTF-32: 常に32ビット(4バイト)固定長でコードポイントを表現します。単純ですがメモリ効率が悪いため、あまり広く使われません。

SwiftのStringは、これらのエンコーディングされた表現(utf8, utf16, unicodeScalars というプロパティでアクセス可能)を内部に持っていますが、通常はこれらを意識することなく「書記素クラスター」として文字列を扱います。

6.3 書記素クラスター (Grapheme Cluster)

これがSwiftのStringの肝となる概念です。書記素クラスターとは、「ユーザーが目で見て一つの文字として認識する単位」です。

例えば:
* "e" (U+0065) は単一の書記素クラスター
* "é" は、基本文字 'e' (U+0065) と結合文字 '´' (U+0301) の組み合わせで、見た目は1文字ですが、内部的には2つのUnicodeスカラー値で構成されます。Swiftはこれを一つの書記素クラスターとして扱います。
* "👍🏽" (親指を立てる絵文字 + 肌の色調修飾子) も、複数のUnicodeスカラー値の組み合わせですが、Swiftはこれを一つの書記素クラスターとして扱います。
* "🇰🇷" (韓国国旗絵文字) は、リージョナルインジケーターという特殊な文字2つの組み合わせですが、これも一つの書記素クラスターとして扱われます。
* "👨‍👩‍👧‍👦" (家族の絵文字) は、さらに多くの文字とゼロ幅結合子という特殊な文字が組み合わさってできていますが、これも一つの書記素クラスターです。

SwiftのStringの count プロパティは、この書記素クラスターの数を返します。だからこそ、"é".count"👨‍👩‍👧‍👦".count1 になるのです。これは、文字列を画面に表示したり、ユーザーに文字数制限を伝えたりする際に、直感的で正しい「文字数」を提供します。

6.4 なぜ整数インデックスが危険なのか?

これで、なぜSwiftのStringが整数インデックスによる直接アクセスを許可しないのかが明確になります。

もし myString[n] のように整数インデックスでアクセスできたとしたら、その n が指す位置は、UTF-8エンコーディングにおけるバイトオフセットかもしれませんし、UTF-16におけるコードユニットのオフセットかもしれませんし、Unicodeスカラー値のオフセットかもしれません。そして、どれであっても、それは必ずしも「書記素クラスター」の境界と一致しません。

例えば、文字列 "Hé" を考えます。
* 見た目の文字数(書記素クラスター):2 (H, é)
* Unicodeスカラー値の数:3 (H, e, ´)
* UTF-8でのバイト数:3 (H 1バイト, é 2バイト)

もし myString[1] が整数インデックスでのアクセスを許可していたら、それは何を指すべきでしょうか?
* 2番目の書記素クラスター 'é' の開始位置?
* 2番目のUnicodeスカラー値 'e' の位置?
* UTF-8エンコーディングにおける2番目のバイト位置?

これらのどれを返すにしても、他の表現では不正な位置を指す可能性があります。例えば、UTF-8の2番目のバイトは 'é' を構成する途中のバイトかもしれません。そこから文字列を読み始めようとすると、無効な文字シーケンスとしてエラーになったり、文字化けが発生したりします。

Swiftは、このような曖昧さや危険性を排除するために、インデックスを String.Index という抽象的な型にし、常に書記素クラスターの境界を安全に指し示すように設計されています。インデックスの移動は、書記素クラスター単位で行う必要があります。

6.5 各エンコーディングビューへのアクセス

必要に応じて、文字列を構成するUnicodeスカラー値やUTF-8/UTF-16コードユニットのシーケンスにアクセスしたい場合もあります。Swiftは、Stringの以下のプロパティを通して、これらの「ビュー」を提供します。

  • unicodeScalars: UnicodeScalar 型のシーケンス。これは、結合文字なども分解せず、各Unicodeスカラー値を列挙します。
  • utf8: UInt8 型のシーケンス。UTF-8エンコーディングでのバイト列です。
  • utf16: UInt16 型のシーケンス。UTF-16エンコーディングでのコードユニット列です。

これらのビューは、低レベルな文字列処理や、特定のエンコーディング形式との間でデータをやり取りする必要がある場合に利用します。ただし、ほとんどの日常的な文字列操作では、書記素クラスター単位で扱うStringのデフォルトの挙動で十分です。

“`swift
let complicated = ” café☕️” // スペース + c + a + ´ + f + é + ☕️

print(“String count:”, complicated.count) // 出力: 5 (スペース, c, aé, f, ☕️)

print(“\n— Unicode Scalars —“)
for scalar in complicated.unicodeScalars {
// scalar.value は Unicode コードポイントの数値 (UInt32)
// scalar.description は スカラー値の文字列表現
print(“(scalar.value): ‘(scalar.description)'”)
}
/ 出力:
— Unicode Scalars —
32: ‘ ‘
99: ‘c’
97: ‘a’
769: ‘´’
102: ‘f’
233: ‘é’
9749: ‘☕’ <- ‘☕’ (U+2615) が単一のスカラー値として扱われる例
/

print(“\n— UTF8 Code Units —“)
for byte in complicated.utf8 {
// 各バイトを16進数で表示
print(“(byte) ((String(byte, radix: 16))) “, terminator: “”)
}
print()
/ 出力例:
— UTF8 Code Units —
32 (20) 99 (63) 97 (61) 196 (c4) 129 (81) 102 (66) 195 (c3) 169 (a9) 226 (e2) 152 (98) 149 (95)
/

print(“\n— UTF16 Code Units —“)
for unit in complicated.utf16 {
// 各コードユニットを16進数で表示
print(“(unit) ((String(unit, radix: 16))) “, terminator: “”)
}
print()
/ 出力例:
— UTF16 Code Units —
32 (20) 99 (63) 97 (61) 769 (301) 102 (66) 233 (e9) 9749 (2615)
/
“`

この例からも分かるように、.count はユーザーが認識する「文字」の数であり、unicodeScalars, utf8, utf16 はそれぞれ異なる粒度やエンコーディングでの内部表現を反映しています。初心者の方にとっては、まずはデフォルトのString(書記素クラスター)の扱いに慣れることが最も重要です。

第7章:よくある落とし穴とTips

SwiftのStringの基本を学んできましたが、初心者の方がつまづきやすいポイントや、知っておくと便利なTipsを紹介します。

7.1 IndexOutOfBoundエラー

前述のように、Stringのインデックスは String.Index 型であり、安易な整数オフセットでのアクセスは危険です。index(_:offsetBy:) などを使ってインデックスを計算する際は、そのインデックスが文字列の有効な範囲内にあるか常に注意が必要です。

“`swift
let text = “abc” // startIndex, index(after: startIndex), index(startIndex, offsetBy: 2), endIndex

// 有効なアクセス
print(text[text.startIndex]) // a
print(text[text.index(before: text.endIndex)]) // c

// 無効なアクセス(存在しないオフセットを指定)
// print(text[text.index(before: text.startIndex)]) // Fatal error: String index is out of bounds
// print(text[text.index(after: text.endIndex)]) // Fatal error: String index is out of bounds
// print(text[text.index(text.startIndex, offsetBy: 3)]) // Fatal error: String index is out of bounds
“`

インデックス計算の結果が範囲外になる可能性がある場合は、以下のいずれかの方法で安全性を確保します。

  1. index(_:offsetBy:limit:) を使う: 範囲外になる前に計算を停止させます。結果が limit と一致するか確認することで、有効なインデックスが得られたか判定できます。
  2. 事前に範囲チェックを行う: 計算したいインデックスが startIndex...index(before: endIndex) の範囲に含まれるか、または startIndex...endIndex の範囲に含まれるかなどを事前に確認してからアクセスします。

“`swift
let text = “abc”

// 安全なアクセス例 (offsetBy:limit: を使う)
if let i = text.index(text.startIndex, offsetBy: 3, limit: text.endIndex) {
if i != text.endIndex {
print(“有効なインデックス: (text[i])”)
} else {
print(“指定されたオフセットは範囲外です(またはendIndexです)”) // 出力
}
}

// 安全なアクセス例 (事前に範囲チェック)
let offset = 3
if offset >= 0 && offset < text.count {
let i = text.index(text.startIndex, offsetBy: offset)
print(“有効なインデックス: (text[i])”) // これは実行されない (offset 3 は無効)
} else {
print(“指定されたオフセット((offset))は範囲外です”) // 出力
}

let validOffset = 1
if validOffset >= 0 && validOffset < text.count {
let i = text.index(text.startIndex, offsetBy: validOffset)
print(“有効なインデックス: (text[i])”) // 出力: 有効なインデックス: b
}
“`

特に offsetBy で負の値を指定する場合や、ループでインデックスを移動させる場合は、終端条件に十分注意が必要です。

7.2 Mutable/Immutableの使い分け

let で宣言されたStringは不変であり、var で宣言されたStringは可変です。

  • let を使うべき場合: 値が一度設定されたら変わらない場合。例えば、定数として扱うメッセージ、ファイルパス、ユーザー名(変更されないと仮定する場合)など。安全性のため、可能な限り let を使うのがSwiftの一般的なスタイルです。
  • var を使うべき場合: 文字列の内容を後から変更したり、結合したり、置換したりする場合。例えば、ユーザーからの入力に合わせて動的に変化する文字列、ループ内で少しずつ構築される文字列など。

不変なStringに対して変更操作(例えば uppercased()replacingOccurrences())を行うと、新しいStringオブジェクトが返されます。これは元のStringを変更しません。可変なString(var)に対して += のような操作を行うと、同じ変数名が新しい文字列データを指すように変更されます。

“`swift
let immutable = “Start”
// immutable += ” End” // エラー

var mutable = “Start”
mutable += ” End” // OK
print(mutable) // 出力: Start End

let upper = immutable.uppercased() // 新しいStringが生成される
print(upper) // 出力: START
print(immutable) // 出力: Start (元のimmutableは変更されない)
“`

7.3 パフォーマンスに関する考慮事項

SwiftのString操作は、Unicodeや書記素クラスターを安全に扱うために設計されており、多くの場合非常に効率的です。しかし、大規模な文字列に対して特定の操作を行う場合は、パフォーマンスを考慮する必要があります。

  • 文字列結合 (+ または +=): 短い文字列を繰り返し結合して長い文字列を構築する場合、+ 演算子は結合のたびに新しい文字列オブジェクトを生成し、内容をコピーするため、非効率になることがあります。特にループ内で多数回結合する場合は、より効率的な方法(例: 部分文字列の集合を最後に一度結合するなど)を検討する必要があるかもしれません。ただし、Swiftの標準ライブラリは内部でいくつかの最適化を行っているため、通常の利用では大きな問題にならないことが多いです。
  • インデックスアクセス: index(_:offsetBy:) のようなインデックス計算は、文字列の先頭から(あるいは終端から)目的の位置まで内部的にスキャンする必要があるため、文字列の長さに比例した時間がかかる場合があります(O(n))。他の言語のようにO(1)でランダムアクセスできるわけではありません。頻繁に文字列中の特定の位置にアクセスする必要がある場合は、そのデータ構造の見直し(例えば、配列など)を検討する必要があるかもしれません。
  • Substring: 前述のように、Substringはメモリ共有により効率的ですが、元の大きなStringの参照を持ち続けることで、意図しないメモリ消費につながる可能性があります。必要に応じて String() で明示的にコピーすることがパフォーマンスやメモリ管理の点で重要になることがあります。

これらのパフォーマンスに関する考慮事項は、非常に大きなテキストデータを扱う場合や、パフォーマンスが厳しく要求される場面で重要になります。日常的なアプリケーション開発では、まずはコードの読みやすさや安全性を優先して、必要に応じて最適化を検討するのが良いでしょう。

7.4 Character 型について

Stringを for-in ループでイテレーションすると、各要素は Character 型になります。Character 型は、単一の書記素クラスターを表します。

“`swift
let character: Character = “😊”
let anotherCharacter: Character = “é”
let simpleChar: Character = “a”

print(character)
print(anotherCharacter)
print(simpleChar)

// Character型は1つの書記素クラスターのみを含む
// Character(“Hello”) // エラー: 複数の書記素クラスターを含む文字列はCharacterにできない
“`

Character 型も、String 型と同様に、Unicodeに対応しており、単一の絵文字やアクセント付き文字などを正しく表現できます。Character も値型です。

終わりに

この記事では、SwiftのStringの基本中の基本から、少し踏み込んだUnicodeやインデックスの概念までを詳しく解説しました。

  • StringはSwiftでテキストを扱うための主要な型であり、値型として安全に扱えます。
  • 文字列はリテラル ("", """ """) や String() で生成できます。
  • let で不変な文字列、var で可変な文字列を宣言します。
  • ++= で結合、\() で補間ができます。
  • isEmpty で空か判定、count で文字数(書記素クラスター数)を取得できます。
  • == で比較できます(デフォルトで大文字小文字区別)。
  • 文字列内の位置は String.Index 型で表され、startIndex, endIndex, index(after:), index(before:), index(_:offsetBy:) などのメソッドで安全に操作します。
  • for-in ループで各 Character に安全にアクセスできます。
  • 部分文字列は Substring 型で返され、メモリを共有しますが、必要に応じて String() で独立させます。
  • uppercased(), lowercased(), trimmingCharacters(in:), contains(), hasPrefix(), hasSuffix(), replacingOccurrences(of:with:), split(separator:) など、便利なメソッドがあります。
  • SwiftのStringはUnicodeの書記素クラスターを基本単位としており、これがインデックスやカウントの挙動に影響を与えています。

SwiftのStringは、他の言語に比べて最初は少し複雑に感じるかもしれませんが、これは世界中の多様なテキストを安全かつ正確に扱うためのSwiftの設計思想の現れです。一度この基本的な概念を理解すれば、自信を持ってStringを使いこなせるようになるはずです。

ぜひ、この記事で学んだことを元に、実際にSwiftのコードを書いてStringを操作してみてください。Playgroundで様々な文字列操作を試してみるのがおすすめです。

これで、SwiftのStringの基本の「き」はマスターです!さらに進んだStringの機能(正規表現、ロケールに応じた比較など)については、必要に応じて公式ドキュメントなどを参照してみてください。

Happy Coding!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール