Swiftのタプルには謎が多い
具体的に7つの不思議があるわけではありませんが、コード例を交えながらタプルの不思議(魅力)をお伝えできればと思います。
特に断りがない場合は、執筆時点の最新であるSwift 5.9.2を前提とします。
まず、はじめは、
いきなりですが、
`Tuple型`
は存在しません
タプルに`型`
は存在しますが、Tuple
といった基底型 やプロトコル は定義されていない
、ということです。
例えば、x
がInt
型かを判定する場合に`x is Int`
と書きますが、
任意のタプルxを`x is Tuple`
では判定はできません。なぜなら、共通的な(汎用的な)Tuple型は存在しないからです。
print(10 is Int) //true
print((10, 20) is Tuple) //error: cannot find type 'Tuple' in scope
- タプルはそれぞれが
`型`
typealias Tuple1 = (Int, Int)
typealias Tuple2 = (Int, String)
let tuple1 = (10, 20)
print((10, 20) is Tuple1) //true
print(tuple1 is (Int, Int)) //true
print((10, 20) is Tuple2) //false
print(type(of: (10, 20))) //(Int, Int)
print(type(of: (a: 10, b: 20))) //(a: Int, b: Int)
print(type(of: (10, s: "A"))) //(Int, s: String)
強いてタプル型を判定するなら、『(
(左カッコ)で始まる』型、でしょうか。
タプルの「名前」はオプション
タプルの各項目には項目名を付けることができますが、オプションのため、項目名のある/なしの混在が可能です。
- 名前付きタプルでも、名前による区別がされない場合がある
//typealias Tuple1 = (Int, Int)
print((a: 10, b: 20) is Tuple1) //true
typealias Tuple3 = (a: Int, b: Int)
typealias Tuple4 = (c: Int, d: Int)
print((10, 20) is Tuple3) //true
print((10, 20) is Tuple4) //true
print((a: 10, 20) is Tuple3) //true
print((10, d: 20) is Tuple4) //true
print((b: 10, a: 20) is Tuple3) //false ∴名前(の順序)が異なる
print((c: 10, 20) is Tuple3) //false ∴名前が異なる
タプルの「名前」はオプション(その2)
- 「比較」において、名前は区別されない
要素(値)の並びだけで比較される。
(名前を無視して、要素の型の並びが同じであれば比較できる)
var tuple3 = (a: 10, b: 20)
var tuple4 = (c: 10, d: 20)
print(tuple3 == tuple4) //true
print(tuple3 < (x: 10, y: 21)) //true
print(tuple3 == (10, "A")) // error: tuple type '(Int, String)' is not convertible to tuple type '(Int, Int)'
代入時、名前付きタプルの扱いは要注意
- 名前付きタプルの変数に、名前なしタプルの代入が可能
- 名前付きタプルの変数に、異なる名前付きタプルの代入は不可(型が異なる)
//var tuple3 = (a: 10, b: 20)
tuple3 = (20, 30) //OK
tuple3 = (x: 10, y: 20) //error: cannot assign value of type '(x: Int, y: Int)' to type '(a: Int, b: Int)'
ここまでは、まだ理解できるが、次が問題。
- 名前が同じであれば、名前の順序が異なっていても「代入」可能
//var tuple3 = (a: 10, b: 20)
let tuple5 = (b: 10, a: 20)
tuple3 = tuple5 //OK
print(tuple3 == tuple5) //false ⭐️
// マジで代入できちゃう、しかも代入後はイコールじゃない??
⭐️ 普通に考えると、代入すれば両者は同じになるはず。しかし、false とは何故?
中身を見ると、その理由が分かります。
print(tuple5) //(b: 10, a: 20)
print(tuple3) //(a: 20, b: 10))
名前の並び(10と20の並び)が異なっています。だから、同値では有りません。
つまり、右辺が名前付きタプルの場合の代入においては、名前の順序は無視されて、対応する名前の要素に代入される、ということです。
//let tuple1 = (10, 20)
//右辺が名前なしタプル`tuple1`の代入は、(位置(順序)で代入)
(tuple3.0, tuple3.1) = (tuple1.0, tuple1.1) //と等価に対して、
tuple3 = tuple5 //この代入は、(名前で代入)
(tuple3.a, tuple3.b) = (tuple5.a, tuple5.b) //と等価となる
//こう書くと、なぜか納得感がある
「型の判定」、「比較」、「代入」では、名前の扱いが違う
このことによって、アプリ開発で混乱することはあまり無いとは思いますが、
名前あり/なし混在のタプルを使う場合は、注意が必要です。
それにしても、どうしてこのような言語仕様になったのでしょうか??
話は変わって、
タプルの要素をKeyPathでアクセスできるか?
普通にKeyPathでアクセスできます。
let tuple4KeyPath_c = \Tuple4.c
print(type(of: tuple4KeyPath_c)) //WritableKeyPath<(c: Int, d: Int), Int>
print(tuple4[keyPath: tuple4KeyPath_c]) //10
print(tuple4[keyPath: \Tuple4.d]) //20
print(tuple4[keyPath: \Tuple4.0]) //10
print(tuple4[keyPath: \Tuple4.1]) //20
print(tuple4[keyPath: \Tuple4.2]) //error: value of tuple type 'Tuple4' (aka '(c: Int, d: Int)') has no member '2'
tuple4.0
をtuple4[0]
のようにsubscriptでアクセスできないか考えましたが、簡単にはできそうにありません。
もし、できるならば、tuple4[n]
のように動的に要素にアクセスできるのだが・・・
タプルの比較
初期のSwiftでは、タプル同士の比較ができませんでしたが、
どこかのバージョンでタプル同士の比較ができるようになり、すごく便利になりました。
タプルの要素を左から順に比較していきます。
let compareTuple = (1, 10, 10.0, "Z") < (1, 11, 1.1, "A") //true
これを応用すると、複数のソート項目によるソートの比較関数が簡単に定義できます。ソート対象がタプル配列である必要はありません。structやclassの配列にも使えます。
typealias Point = (x: Int, y: Int)
let points: [Point] = [(8, 2), (1, 9), (7, 5), (5, 4), (5, 0)]
let sotredPoints = points.sorted(by: <) // $0 < $1
print(sotredPoints)
//[(x: 1, y: 9), (x: 5, y: 0), (x: 5, y: 4), (x: 7, y: 5), (x: 8, y: 2)]
//(タプル以外の配列でも可)X小さい、Y大きい順
let sotredPoints2 = points.sorted { ($0.x, $1.y) < ($1.x, $0.y) }
print(sotredPoints2)
//[(x: 1, y: 9), (x: 5, y: 4), (x: 5, y: 0), (x: 7, y: 5), (x: 8, y: 2)]
タプルを使わずに「X小さい、Y大きい順」の比較関数を定義すると、下記のように書く必要がありました。
//let sotredPoints2 = points.sorted { ($0.x, $1.y) < ($1.x, $0.y) }
let sotredPoints2 = points.sorted {
($0.x == $1.x) ? ($0.y > $1.y) : ($0.x < $1.x)
}
ソート項目が3つ4つと増えると、ネストが増えて 可読性が下がります。タプルで書く方が断然分かり易いです。
タプルはEquatable
やComparable
ではない
タプルは、単独では、==
で同値判定したり、<
や>
などで大小比較できるのですが、SwiftプロトコルのEquatable
やComparable
には準拠していません。
このため、タプル配列に対してcontainsメソッドは使えないし、sortも比較関数を書かないと使えません。
var points = [(8, 2), (1, 9), (7, 5), (5, 4), (5, 0)]
if points.contains((8, 2)) { /*・・・*/ } //error: cannot convert value of type '(Int, Int)' to expected argument type '((Int, Int)) throws -> Bool'
points.sort() //error: type '(Int, Int)' cannot conform to 'Comparable'
タプルはHashable
でもない
さらに、Hashable
にも準拠していないため、Set型の要素に使えないし、Dictonary型のkeyとしても使えないです。
let pointSet = Set(points) //error: type '(Int, Int)' cannot conform to 'Hashable'
var pointDict: [(Int, Int): Int] = [:] //error: type '(Int, Int)' does not conform to protocol 'Hashable'
これらを不便と感じる方も多いと思います。自分もその一人です。
タプルをEquatable
、Comparable
やHashable
に準拠させる
しかし、タプルはextension
が使えません💢。
typealias Point = (x: Int, y: Int)
extension Point { } // error: non-nominal type 'Points' (aka '(x: Int, y: Int)') cannot be extended
↓構造体を定義するしかないのです。
(Genericには出来ないが、マクロ化はできるかも?)
(2024.7.16追加)マクロ化しました → [Swift] HashableTuple を生成する Swift Macro を作ってみた
struct Tuple {
typealias Element = (x: Int, y: Int) //カスタマイズする
let tuple: Element
init(_ _tuple: Element) { self.tuple = _tuple }
func callAsFunction() -> Element { self.tuple }
}
extension Tuple: Equatable {
static func == (lhs: Tuple, rhs: Tuple) -> Bool {
lhs.tuple == rhs.tuple
}
}
extension Tuple: Comparable {
static func < (lhs: Tuple, rhs: Tuple) -> Bool {
lhs.tuple < rhs.tuple
}
}
extension Tuple: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(tuple.x)
hasher.combine(tuple.y)
}
}
extension Tuple: CustomStringConvertible {
//おまけで`CustomStringConvertible`にも準拠しておく
var description: String {
"\(type(of: self))(\(tuple.x), \(tuple.y))"
}
}
( ↑準拠するプロトコルと必要メソッドの対応を分かり易くするため、敢えてそれぞれをextensionしています。)
let tupple = Tuple((10, 20))
let (x, y) = tupple()
print(x, y) //10 20
var array = [Tuple]()
if array.contains(tupple) { /*・・・*/ } //ok
array.sort() //ok
しかし、上記の例Tuple
の(x: Int, y: Int)
ように、同じ型の要素だけで 要素数も 2つ3つ程度なら、わざわざEquatable
、Comparable
やHashable
のためのコードを書かずに、単に配列にして対応する場合が多いです。
//本来の タプル (x, y) を、配列 [x, y] で代用。
let set = Set<[Int]>()
let dict = [[Int]: Int]()
要素の型が異なる場合にはArray<Any>
にすれば可能ですが、参照するときの型変換が面倒かも知れません。
タプルは、Codable
でもない
タプルは一切のどのプロトコルにも準拠していません。Equatable
、Comparable
、Hashable
だけの話では無いのです。
当然、Codable
にも準拠していません。これによる影響は、例えば、タプルをjsonに変換したり、jsonからタプルに変換することができません。
独自の型を定義して、Codable
に準拠させる必要が有ります。ここまで来れば当然のことですね。
くどくなってきた感がありますので、プロトコルの話はここで終わりにします。
話は変わって、
タプルの要素をイテレートできるか?
extension
が使えないため、IteratorProtocol
に準拠することは難しいですが、一旦、配列に変換することで代用できます。
let tuple8 = (a: 10, c: "A", 1.0, e: (x: 10, y: 20), [10, 20])
let array8 = Mirror(reflecting: tuple8).children.map { ($0.label ?? "unlabeled", $0.value) }
print(type(of: array8)) //Array<(String, Any)>
print(array8)
//[("a", 10), ("c", "A"), (".2", 1.0), ("e", (x: 10, y: 20)), (".4", [10, 20])]
let type8 = array8.map { "\(type(of: $0.1))" }
print(type8)
//["Int", "String", "Double", "(x: Int, y: Int)", "Array<Int>"]
タプルがネストしている場合は、これを再帰的に変換する必要がありますが、名前、値、要素の型が取得できます。
for (type, (name, value)) in zip(type8, array8) {
print((name, type, value))
}
//("a", "Int", 10)
//("c", "String", "A")
//(".2", "Double", 1.0)
//("e", "(x: Int, y: Int)", (x: 10, y: 20))
//(".4", "Array<Int>", [10, 20])
配列化出来たので、各要素の動的アクセスやランダムアクセスが可能になりますが、実用的かと問われると、?です。
これまでは、タプル型、タプル式についての内容でした。
次はパターンとしてのタプルについて、です。
パターンとしてのタプル
switchやif文のcaseラベルに書くパターンのことです。
if文(guard文)でのパターン
(x, y)で示す座標があったとして、その座標が範囲を超えているかどうかを、簡単にチェックできます。
let (Width, Height) = (20, 10)
let x = 10, y = 20
if case (0 ..< Width, 0 ..< Height) = (x, y) {
//範囲内
} else {
//範囲外
}
//guard case (0 ..< Width, 0 ..< Height) = (x, y) else { return }
=
演算子が代入に見えてしまうのが残念ですが・・・。
switch文のパターン
switch文のcaseパターンには、ほんと多彩な書き方ができます。
(例を作るのが面倒だったので、公式ドキュメントからのコピペで失礼します)
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box"
- Value Bindings
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// Prints "on the x-axis with an x value of 2"
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// Prints "On an axis, 9 from the origin"
- where (おまけ)
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// Prints "(1, -1) is on the line x == -y"
where句はホント便利ですね。for文で使うことが度々有ります。
enumにおけるタプル
Swiftのenumは、単に「場合分け」を表すだけではなく、それに応じた値(Associated Values)を保持できるところが特徴です。
enum Barcode {
case upc(system: Int, Int, Int, Int)
case qrCode(String)
}
var productBarcode = Barcode.upc(system: 8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
このenum変数をswitch文で判定するときに、前項のcaseパターンが活躍します。
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
enumのAssociated Valuesは、if文やswitch文のcaseパターンでしか、読み取る(受け取る)ことができません。
話が逸れますが、
case let
のところは、case var
とも書けることをご存知でしょうか?
var
で受け取ると、それは変数となり「値の更新」ができます。
if let
やguard let
でも同じです。
次は、またタプル型に戻りまして、
要素数0のタプル
Swiftでは、要素を一つも持たない要素数0のタプル(空のタプル)が認められています。
let emptyTuple = ()
実はこれは、Void
の実体です。Swiftの言語仕様でVoid
は「空のタプル」と定義されています。
typealias Void = ()
Void
は、関数定義で戻り値を定義しなかった場合に仮定される型です。
//どちらも戻り値はVoid
func noResultFounction() /* -> Void を省略 */ { /*・・・*/ }
func voidResultFounction() -> Void { /*・・・*/ }
Void
を返す関数は、return
でVoid
の関数を呼び出すことができます。
(こういった使い方はあまり無いと思いますが。。。)
func noResultFounction() {
return //普通は書かないが、
return Void() // とも書ける。さらに、
return print(0) // Voidを返す関数も書ける
}
あえて、付け加えておきますが、
Void
を引数にとる関数を定義した場合は、これを呼び出すときは、引数としてVoid
を渡す必要があります。戻り値Void
のように、省略することはできません。
func funcVoid(_ arg: Void) { }
funcVoid(Void()) //ok
funcVoid(()) //ok
funcVoid() //error: missing argument for parameter #1 in call
C / C++ のvoid func(void);
では、呼び出す時に引数は書きませんが、それとは違うということです。念の為。
要素数1のタプル
残念なことに、Swiftでは、要素数1のタプルは定義できません。
var t1 = (1) //と書いても、単にInt型の変数
print(type(of: t1)) //Int
var t2 = ("A") //と書いても、単にString型の変数
print(type(of: t2)) //String
また、Pythonのように (1,)
とも書けません。
var t3 = (1, ) //error: unexpected ',' separator
ちなみに、
どこかのバージョンで、Arrayの最終要素の後ろの,
(カンマ)の記述を認めるようになりましたが、 タプルでは許していないようです。
(2個目以降のカンマならば認めて欲しいところですが、(1,)
を認めないことに起因しているのかも知れません。)
let a4 = [1, 2, 3, 4, ] //OK
let t4 = (1, 2, 3, 4, ) //error: unexpected ',' separator
以上で、タプルを語り尽くせたと思いますが、もし抜け漏れや間違いなどがあれば、ご指摘ください。
この記事が何かの参考になれば幸いです。
最後に、 Swift言語文法サマリーに登場する「Tuple」の箇所を抜き出して終わりにします。