Index out of range
みんな、こういうやつやるよね?
let arr = [1,2,3,4,5]
arr[5] // fatal error: Index out of range
Null Safety
こういうのが嫌だという人は多くてみんな大体こんなふうにNull安全にして配列の要素にアクセスしているんだと思う。
extension Array {
subscript(i index: Int) -> Element? {
guard indices.contains(index) else { return nil }
return self[index]
}
}
let arr = [1,2,3,4,5]
arr[i:5] // nil
Type Safety
これで十分なんでしょうけど、やっぱりnilになる可能性は排除したいと思うのが人情ですよね?
そういうわけで、配列の要素数とIndexへのアクセスを型にしてやれば解決ですね!
// 数字を型で表現
protocol Number {
static var value: Int { get }
}
protocol _0: Number {}
class N0: _0 {
static let value: Int = 0
}
protocol _1: Number {}
class N1: _1 {
static let value: Int = 1
}
protocol _2: _1 {}
class N2: _2 {
static let value: Int = 2
}
// 要素の他に、配列数をジェネリクスとして受け取る配列のWrapperを作る
struct SafeArray<Element, N: Number> {
var items: [Element]
}
// イニシャライザを拡張
extension SafeArray where N == N0 {
init() {
items = []
}
}
extension SafeArray where N == N1 {
init(_ items: (Element)) {
self.items = [items]
}
}
extension SafeArray where N == N2 {
init(_ items: (Element, Element)) {
self.items = [items.0, items.1]
}
}
// subscriptを拡張
extension SafeArray where N: _1 {
subscript(index: N1.Type) -> Element {
return items[index.value]
}
}
extension SafeArray where N: _2 {
subscript(index: N2.Type) -> Element {
return items[index.value]
}
}
let arr0 = SafeArray<Int, N0>()
arr0[N1.self] // compile error!!
arr0[N2.self] // compile error!!
let arr1 = SafeArray<Int, N1>(0)
arr1[N1.self] // 0
arr1[N2.self] // compile error!!
let arr2 = SafeArray<Int, N2>((0,1))
arr2[N1.self] // 0
arr2[N2.self] // 1
IndexのアクセスにN*.self
とか書かないといけないのはイケていない気がするけど、そこは目を瞑ろう。
こうしておけばコンパイルの段階であり得ないIndexにアクセスすることはなくなったし、初期化の時点で配列にある要素が必ず指定した数になる。
Type Safeは素晴らしい!
Auto Generate Source Code
Type Safeになったのは素晴らしいのだけど、アクセスする数字とそれに対応する型をひたすら定義していくのは人間の成せる業ではありませんよね。
Swift本家では標準ライブラリのコードの幾つかはgybというテンプレートエンジンを利用して生成していますので、これに習ってコードを自動生成してやりましょう。
gybはSwiftプロジェクトに内包されたコードで独立して公開されているわけではなく手元に準備をするのが面倒ですので、元祖テンプレートエンジンであるPHPを利用します。
<?php
const MAX_SIZE = 32;
function makeTupleValueText($property, $times) {
$tmp = []
foreach (range(0, $times-1) as $i) {
$tmp[] = $property . '.' . $i;
}
return join($tmp, ',');
}
function makeRepeatText($text, $times) {
$tmp = []
foreach (range(1, $times) as $i) {
$tmp[] = $text;
}
return join($tmp, ',');
}
?>
protocol Number {
static var value: Int { get }
}
protocol _0: Number {}
class N0: _0 {
static let value: Int = 0
}
protocol _1: Number {}
class N1: _1 {
static let value: Int = 1
}
<?php foreach(range(2, MAX_SIZE) as $i): ?>
protocol _<?= $i; ?>: _<?= $i-1; ?> {}
class N<?= $i; ?>: _<?= $i; ?> {
static let value: Int = <?= $i; ?>
}
<?php endforeach; ?>
// イニシャライザを拡張
extension SafeArray where N == N0 {
init() {
items = []
}
}
extension SafeArray where N == N1 {
init(_ items: (Element)) {
self.items = [items]
}
}
<?php foreach(range(2, MAX_SIZE) as $i): ?>
extension SafeArray where N == N<?= $i; ?> {
init(_ items: (<?= makeRepeatText('Element', $i); ?>)) {
self.items = [<?= makeTupleValueText('items', $i); ?>]
}
}
<?php endforeach; ?>
// subscriptを拡張
<?php foreach(range(1, MAX_SIZE) as $i): ?>
extension SafeArray where N: _<?= $i; ?> {
subscript(index: N<?= $i; ?>.Type) -> Element {
return items[index.value]
}
}
<?php endforeach; ?>
これでいくらでもサイズを増やすことが出来るようになった!
…と思いきや、サイズを増やしていくとコンパイル時にクラッシュしてしまうし、Xcodeは固まるしで限界があるようです。
せいぜい64ぐらいが限度ですが、まぁそれぐらいあれば十分でしょう。
UseCase
さて、この安全な配列は事前に要素数が分かっていないと利用することが出来ないので、よくわからない外部入力に対して定義する用途には使い難い。
事前に要素数がわかっているようなもの、例えば、カレンダーの日付だったり、オセロの枠等に利用すると良いのかもしれません、実際に作ったことはないので知らないけど。
Fin
MetaTypeを描く時に*.self
って書くのどうにかして下さい。