書いて覚えるSwift入門

第15回文字列の扱い

文字列

今回はいよいよ文字列を扱います。文字列。最も多用されるデータ型でもあり、Swiftを含め、およそありとあらゆるプログラムも文字列で表記されています[1]⁠。にもかかわらず、筆者は今までSwiftにおける文字列を扱うのにモジモジしてきました。それには深いわけがあります。

> Swift ’s String and Character types provide a fast, Unicode-compliant way to work with text in your code. -- The Swift Programming Language

「SwiftのStringおよびCharacter型は、高速でUnicode準拠したテキスト処理を提供します⁠⁠。⁠高速」はとにかく、⁠Unicode準拠」というのがななかの難物なのです。なぜ難物なのか。それを知るためには、コンピュータにおける文字列処理の歴史を紐とかねばなりません。

A Brief History of Characters

文字列==文字の列。前世紀までは、文字列というものの理解はその程度で間に合っていました表1⁠。

表1 文字列の扱いの歴史
YearEventComment
1963EBCDIC初の文字コード規格
1963ASCII最も普及した文字コード規格
1969JIS X 0201初の日本語文字コード(カタカナのみ)
1978JIS C 6226 (JIS X 0208)かな漢字を含む文字コード
1982Shift_JIS
1985EUC-JP
1991Unicode 1.0現在のデファクトスタンダード
1992UTF-8拡張ASCIIとしてのUnicode
1993ISO-2022-JP電子メールにおける標準日本語文字コード
1995Java 1.016bit Character
1995JavaScript 1.0
1996Unicode 2.0サロゲートペア標準化
2000Python 2.0バイト列 != 文字列
2002Perl 5.8Unicodeを言語としてフルサポート
2007Ruby 1.9
2008Python 3.0UCS2事実上の廃止
2010Unicode 6.0絵文字追加
2014Swift 1.4Version 1.0 からUnicodeをフルサポート

0と1という2種類の数値しか根源的に扱えない電子計算機(あえてこう書く)において文字を扱うにあたり、先人たちがやったのは基本的に次のものでした。

  • 各文字に番号を振り
  • その番号を並べる

往時のプログラマたちにとって幸いなことに、当初扱わなければならない文字列は英語のみ。そして英語というのは必要な文字種がとても少ない言語でした。アルファベット26文字、大文字と小文字を区別しても52文字。アラビア数字を加えても62文字。これにスペースやタブや改行などを加えても、7bits=128種類にらくらく収まったのです。これが、現在でも使われているASCII。量産されているCPUで扱える最も小さなデータ型である1byte=8bits[2]にそのまま入れても1bitあまります。

2byte文字の誕生と混乱

文字とは1byteに収まるものであり、それを並べたものが文字列である。

そのような時代が長いこと続きました。

その原則を破ったのが、日本です。カナだけでも48文字、しかもカタカナとひらがなと2種類。これだけでも8bitに収まらないのに、さらに数千種類の漢字も扱いたいとなると1つの文字を表現するのに12~13bitsは必要。しかも漠然と並べるのではなく、ASCIIとの共存も考えると1つの文字を表記するのに2bytesは必要……こうして登場したのがShift-JISでありEUC-JPでありISO-2022-JPです。ここで注目すべきは、各文字に振られた番号はJIS X0208という単一の規格なのに、それをどう並べるかで複数の規格が乱立したことです。それぞれ一長一短あるのですが、そうなってしまった理由は、⁠最後に正しいものではなく、今すぐ使えるものを」という「電子立国日本の現場圧力」ではなかったかと思われます。インターネットどころかパソコン通信すらまだ一般的ではなく、データ互換性はせいぜいメーカーがそれぞれ自社製品のみ担保されていた時代、まず大事だったのはパソコン、いやワープロ(もはや死語?)で入力できて出力できることだったのです。

Unicodeの誕生

その状況は、ネットの登場で一変します。コンピュータは単独で使用するものから、他のコンピュータ、強いてはそのコンピュータのユーザ同士をつなげるものとなったのです。そんな時代、各国ごとにバラバラの規格を使っていたのではメーカーはたまったものではありません。各国ごとに乱立していた「ASCII+自国語文字コード」から、世界共通の文字コードへの移行の機運は高まっていたのです。⁠世界共通の文字コード⁠⁠、それがUnicodeです。

最初に登場した段階におけるUnicodeは、過去との互換性はほとんど気にかけていませんでした。Shift-JISやEUC-JPといったASCII互換の日本語文字コードが可変長だったのに対し、1.0段階のUnicodeは16-bit固定長。ここにカナもハングルも日中韓の漢字も全部収める予定だったのです。そのためには、各国で用いられている文字コードをまとめて割り振るのではなく、並べ替えが必要となりました。それが(悪名高き)Han Unificationです。それの何が問題だったかといえば、文字コード変換。Unicode以前の日本語文字コードの相互変換は、元になる「背番号」が共通だったこともあり単純計算でOKだったのが、変換表が必要になったのです。

Unicodeの不幸

その一方で、Unicode陣営も1.0の「ゼロベースで文字列を再定義する」やり方がそのままではうまく行かないことに気づいてきました。まず、⁠同じ文字に同じ番号」という原則が破られます。Unicodeコンソーシアムのベンダ自身が、それまでの文字コードからUnicodeに変換したあとの逆変換がきちんと成立することを求めたからです。かくして半角カナも生き残りました。次に、16bitではとても足りないことに気がつきました。それを解決すべく、Surrogate Pairというものが登場しました。16bitで1文字から、⁠文字によっては16bit、2つで1文字」というわけで、固定長という原則がここに崩れたのです。その代償として、Unicodeでは最大(16 + 1) *2**16 == 1,114,112文字まで扱えるようになりました。そしてASCII互換性も、UTF-8で解決されました。1文字の長さは1~4bytesの可変長になる代わりに、ASCIIはこれまでどおり1byte。ASCIIを前提としていた数多のソフトウェア、とくにCコンパイラでもそのまま扱えます。

我々にとって不幸だったのは、⁠使えるUnicode」である2.0が登場する直前に、⁠インターネット爆発」が起こってしまったこと。とくにJavaとJavaScriptが1文字16bitとしてしまったのは今もなお尾を引いているのは本誌の読者であればご存じでしょう。おかげで絵文字の長さはそのままでは2文字になっちゃうとか...

文字コード統一の奇跡

Han Unificationのような「理念の押し付け」のあとに、Surrogate PairsやUTF-8のような「日和見(ひよりみ⁠⁠。それだけの犠牲を払って、世界をUnicode化するだけの価値はあったのでしょうか?

私自身、Perl 5.8のUnicode化という形でその片棒を担いだ以上、中立の立場とはとても言えないということをあらかじめお断りしたうえで言えば、その価値は確かにあったと断言します。Unicode以前の世界、小はワンライナーから大はOSに至るまで、ソフトウェアには各国語版がつきものでした。EmacsにはNemacs、PerlにはJPerl、Mac OSには漢字Talk……。しかし今や、世界中で使用されるソフトウェアは真の意味で世界的です。EmacsもPerlもOS Xも日本語版はもはや不要。iOSやAndroidにいたっては、その誕生時点からUnicodeを前提にできています。インターネットの普及によりTCP/IP以外の通信プロトコルは事実上滅亡しましたが、それによって世界が画一化したようにはとても思えません。我々の遺伝子だって、RNA-DNA-タンパク質というシングルアーキテクチャであることを考えれば、文字コードのように基礎的なデータ構造が(およそHan Unificationの混乱を除けば)統一できたのは奇跡なのではないでしょうか。

Swiftにおける文字[列]?

ずいぶんと前置きが長くなりました。それではSwiftの文字を実際に見てみましょう。次のコードをplaygroundかREPLで実行してみてください図1⁠。

var str = "Swift"
str += "スウィフト"
str = "\(str)"
let = str
print()
図1 Swiftにおける文字コードの出力結果
図1 Swiftにおける文字コードの出力結果

Swiftスウィフトとprintされたはずです。たった5行のコードでも、これだけのことがわかります。

  • 型宣言は不要。リテラルから適切に推論される
  • ちなみに型の名前はString。Xcodeなら識別子をoption+clickすれば確認できる
  • 文字列リテラルは""で囲まれた内部
  • \()で変数展開(interpolation)
  • 識別子(identifier)にASCII以外のUnicodeを用いることもできる

もっともこのレベルのサポートは、Perl 5.8がもう12年以上前に実現していました。Swiftがすごい――熟練プログラマの多くがやりすぎると感じるかも――のはここからです。次のコードをご覧ください。

let me = "だん"
let me2 = "\u{3060}\u{3093}"
let nfd = "\u{305f}\u{3099}\u{3093}"
me == me2
me == nfd

me == me2trueなのは当然としても、me ==nfdまでもtrueになるのです。なぜ等しいか?

ひらがなの「だ」一文字と「た」+濁点が等しいとSwift(正確にはfunc ==(_:String,_:String)->Boolがみなしているからです。

文字とは何か? 等しい文字とは何か? 文字列を文字にバラして見てみましょう。

for c in "\u{305f}\u{3099}\u{3093}".characters {
    print(c, String(c).unicodeScalars.count, String(c).utf8.count)
}

結果は次のとおりになるはずです。

だ 2 6
ん 1 3

Swiftにおける「文字」=Characterは、1byteではもちろんなく、1 code pointですらなく、1 graphemeなのです。graphemeというのは難しい言葉ですが、OS Xの辞書によると「書記素(書き言葉の最小単位⁠⁠」だそうです。⁠文字列の比較はgraphemeをもってせよ」というのは確かにUnicode Consortiumの文書[tr15]にあるのですが、これをきちんと言語レベルで実装しているのは筆者の知る限りSwiftだけです。

これはある意味、1.0時点でのUnicodeの理念を実現した格好にもなっているのですが、その後Unicodeが現実に対してずいぶん妥協したのは前述のとおりで、実際"ダン" == "ダン"半角カナの" ダン"と全角カナの"ダン" を==で比較してもfalseとなります。

「Unicode潔癖症」?

2.2におけるSwiftの「Unicode潔癖症」は、添字(Subscript)にも見られます。たとえばJavaScriptでは(ES5以降は正式に⁠⁠、

"JavaScript"[4] // "S"

という具合に文字列を文字の配列とみなして文字を取り出すことができますが、

"Swift"[4]

は"t" ではなく(そのままでは)エラーです。だからと言って添字を使えないわけではなく、次のようにすれば似たようなことはできます。

let str = "Swift"
let idx = str.startIndex
str[idx.advancedBy(4)]

これを利用すれば、extensionを使ってIntによる添字を後付けすることは一応できます。

extension String {
    subscript(idx:Int)->Character {
        return self[self.startIndex.advancedBy(idx)]
    }
}
"Swift"[4] // "t"

しかし、デフォルトでそうしようとすればできるのに、今のところはそうなっていません。文字列を文字に分解する際も、わざわざ.charactersというメソッドを経由しています。Swiftに限らずありとあらゆるソフトウェアは理念と現実の狭間にありますが、Swiftの組込み型の中で、文字と文字列の扱いは突出して理念が先行しているように筆者は感じています。

String... the final frontier?

前回「Swiftの'ミステリー'」として、一重引用符''がSwiftではまだ使われていないことを指摘しました。このことはSwiftの現在の文字列の振る舞いに対し、中の人がまだ試行錯誤している証なのかもしれません。実際Swiftの文字列処理は、C/C++/Objective-CよりずっとマシとはいえPerlやRubyはおろか、JavaScriptにも劣るというのが実感です。Swiftの当初の「檜舞台」がモバイルアプリケーション開発であることを考えれば、それでもObjective-Cしかなかったころに比べればずっとマシではあるでしょうし、最悪ややこしい処理はサーバに丸投げしちゃっても良いとはいえ。

しかしオープンソース化され、Linuxにも移植され、サーバサイドでも使われるようになるごく近い将来、現状のSwiftの文字列処理の貧弱さが気がかりです。

裏を返せば、そこが宝の山という見方もできなくもありません。Swiftライブラリで一旗上げるなら、文字列処理を狙うのもよさそうです。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧