Stringリテラル / String Interpolation "\()" 調査隊
環境
macOS Ventura 13.6.6
Xcode 15.2
Stringリテラル
みんなが息をするように書いているこれ。
let abc = "abc"
これは後ろで
extension String : ExpressibleByStringLiteral {
//コメント省略
@inlinable public init(stringLiteral value: String)
が走っているそうである。このイニシャライザのコメントにそう書いてある。
コメント
与えられた文字列でインスタンスを作ります
直接呼んではいけません。Stringリテラルが与えられたらコンパイラが使います。
たとえば
let nextStop = "Clark & Lake"
このように書くと後ろで(このイニシャライザを)コンパイラが使います。
Stringリテラルという言葉が出てきたが、これは "aaa"
という表記方式のこと。(またはその表記方法で表しているもの)
関係あるところをもう少し出す。このイニシャライザの源の ExpressibleByStringLiteral
プロトコルは次のようになっている。
//抜粋
public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {
init(stringLiteral value: Self.StringLiteralType)
}
このプロトコルの説明の先頭に
Stringリテラルでイニシャライズ出来る型
と書いてあるので、Stringリテラルを投げてインスタンスを作れることを明示するプロトコルと読める。
以上より、「Stringに ExpressibleByStringLiteral
がついているのでStringは String("abc")
のように "abc"
を投げて初期化できる」と言える。
String Interpolation
みんなが大好きなこれ。
let n = 5
print("\(n)") //出力は5
今度は後ろで
@frozen public struct String {
//コメント
@inlinable public init(stringInterpolation: DefaultStringInterpolation)
が走っているそうである。このイニシャライザのコメントにそう書いてある。
コメント
interpolated string literalを使ってインスタンスを作る。
直接呼んではいけません。 あなたがstring interpolationを使ってStringを作る時にコンパイラが使います。
あなたはバックスラッシュと()を使う方式で書いてください。
string interpolationという言葉が出てきたが、これは "aaa\(bbb)"
という表記方式のこと。
関係あるところをもう少し出す。このイニシャライザの源の ExpressibleByStringInterpolation
プロトコルは次のようになっている。
//抜粋
public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
init(stringInterpolation: Self.StringInterpolation)
}
public protocol StringProtocol : /* 省略 */
ExpressibleByStringInterpolation, /* 省略 */ {
}
extension String : StringProtocol {
}
読み方は
init(stringInterpolation:)
は ExpressibleByStringInterpolation
に書かれている。Stringは (間をひとつ経由して) ExpressibleByStringInterpolation
に準拠しているのでStringにも init(stringInterpolation:)
が書かれている(上で見た)。
となる。
関連するものをもう少し出すと
public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
associatedtype StringInterpolation : StringInterpolationProtocol
= DefaultStringInterpolation
where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
init(stringInterpolation: Self.StringInterpolation)
}
public protocol StringProtocol : /* 省略 */
ExpressibleByStringInterpolation, /* 省略 */
where /* 省略 */ Self.StringInterpolation == DefaultStringInterpolation, /* 省略 */ {
}
extension String : StringProtocol {
public typealias StringInterpolation = DefaultStringInterpolation
}
となっていて init(stringInterpolation:)
に渡す StringInterpolation
は ExpressibleByStringInterpolation
の中に書かれている。
ここはあとでやる。
このプロトコルの説明の先頭に
string interpolationを使ってイニシャライズ出来る型
と書いてあるので、string interpolationを投げてインスタンスを作れることを明示するプロトコルと読める。
以上より、「Stringに ExpressibleByStringInterpolation
がついているので、Stringは String("\(n)")
のように "\(n)"
を投げて初期化できる」と言える。
押さえる用語
この先、押さえておきたい用語が3つ。
ExpressibleByStringLiteral
""
方式でインスタンス作成できるタイプを表す(or に付ける)プロトコル。"こんにちは"
を渡して初期化できる。
ExpressibleByStringInterpolation
\()
方式でインスタンス作成できるタイプを表す(or に付ける)プロトコル。"合計は\(sum)円です"
を渡して初期化できる。
StringInterpolation
\()
方式の変換機能の型。\()
の中身を解釈して展開する機能。
例でいうと、sum
が42のときに "\(sum)"
を文字4と文字2に展開する機能。
これ自体は associatedtype StringInterpolation
となっている。
Interpolationは補間という意味らしいが、この記事に限っては展開という言葉で解釈するのもよいでしょう。Intの42を文字の4と文字の2に展開してますし。
ただ、StringInterpolation
と string interpolation が紛らわしい。
-
StringInterpolation
は型 - string interpolation は
\()
という表記方法
StringInterpolation
さっき後でやると言った StringInterpolation
について軽く。
これは string interpolation の各変数(個々の \()
や素の文)の展開を定義するものである。
これは ExpressibleByStringInterpolation
の中で出てくる。
public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
associatedtype StringInterpolation : StringInterpolationProtocol
= DefaultStringInterpolation
where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}
登場人物
StringInterpolation
StringInterpolationProtocol
DefaultStringInterpolation
StringInterpolation
(associatedtypeで宣言された)型の名前
StringInterpolationProtocol
変換機能が備え付ける機能を定義したもの
DefaultStringInterpolation
基本的な変換機能を実装したものの型。StringInterpolationに何も指定しなければこれが使われる。
ややこしい
ExpressibleByStringInterpolation
と StringInterpolation
は、ややこしい書かれ方をしているのでシンプルなものから始めてこのややこしいものにたどり着くことをやる。
その1
まずは一番シンプル。
protocol ExpressibleByStringLiteral {
associatedtype StringLiteralType
}
protocol StringInterpolationProtocol {
associatedtype StringLiteralType
}
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolation : StringInterpolationProtocol
}
struct DummyString : ExpressibleByStringInterpolation {
typealias StringLiteralType = 型A
struct StringInterpolation : StringInterpolationProtocol {
typealias StringLiteralType = 型B
}
}
その2
イコールの関係を付ける。
protocol ExpressibleByStringLiteral //上と同じ
protocol StringInterpolationProtocol //上と同じ
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolation : StringInterpolationProtocol
where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}
struct DummyString : ExpressibleByStringInterpolation {
typealias StringLiteralType = 型A
struct StringInterpolation : StringInterpolationProtocol {
typealias StringLiteralType = 型B
}
}
型A == 型B でないとダメ。
イコールが成り立たなくてコンパイルエラーが出ない組み方はちょっと出来なかった。
その3
DefaultStringInterpolation
を付ける。
実装側で StringInterpolation
を省くことができる。
protocol ExpressibleByStringLiteral //上と同じ
protocol StringInterpolationProtocol //上と同じ
protocol ExpressibleByStringInterpolation: ExpressibleByStringLiteral {
associatedtype StringInterpolation : StringInterpolationProtocol
= DefaultStringInterpolation
where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
}
struct DefaultStringInterpolation : StringInterpolationProtocol {
typealias StringLiteralType = String
}
struct DummyString2 : ExpressibleByStringInterpolation {
typealias StringLiteralType = 型B
}
DummyString2
で StringInterpolation
の実装を省いている。このとき DefaultStringInterpolation
が採用される。
DefaultStringInterpolation
では型が String
で書かれているので 型B == String でないとダメ。
StringInterpolation とは何をするものか
appendInterpolation
を定義して、 \()
で渡されたものを処理する。
引数を持ったものも定義できる。
appendInterpolation(foo: x)
と定義すると \(foo: x)
に対応できる。
Stringリテラル / String Interpolation のまとめ
let s: String = "abc"
let s1 = "abc" // `init(stringLiteral:)` につながる
let s2 = "abc\(s)" // `init(stringInterpolation:)` につながる
コンパイラの仕事
let s: String = "abc"
let s1 = String("abc") // `init(stringLiteral:)`
let s2 = String("abc\(s)") // `init(stringInterpolation:)`
let s3: String = "abc" // `init(stringLiteral:)`
let s4: String = "abc\(s)" // `init(stringInterpolation:)`
let s5 = "abc" // `init(stringLiteral:)`
let s6 = "abc\(s)" // `init(stringInterpolation:)`
s3 s4のようにStringが想定される型ところにStringリテラル/string interpolationが投げられて、想定される型が対応していればそのイニシャライザにつながる。
で、s5やs6はデフォルトの想定がStringになっていてうまくいく。
この
「ある型が想定されるところにStringリテラルや string interpolation が投げられて、その型が対応している場合は対応するイニシャライザにつながる」
という考え方でうまく説明できるところが多くある。
Discussion