Swiftでのジェネリック(テンプレート)について
Swiftでのジェネリック(テンプレート)について
これどうしたら良いのか
struct Sample<T> {
var data : [T]
func takeValuesAsInt() -> [Int] {
var values : [Int] = [Int](repeating: 0, count: data.count)
for i in (0 ..< data.count) {
values[i] = Int(data[i]) // ここでエラー
}
return values
}
}
var sample = Sample(data:[Int8](repeating: 1, count: 3))
内部ストレージの型に自由度を持たせるとして、でも何かのタイミングではInt
で受け取ろうとするとしたとき、Int
に変換しなければならないですね。
これが普通のクラス・構造体あるいはプロトコルなら、T
が何かを示せば良いし、そもそもむやみに型変換しないのだけれども、数値はなかなかそうは言ってられないことがあります。
Int
にキャスト・型変換が可能というようなプロトコルが定義されていればできそうなんですが、ExpressibleByIntegerLiteral
とか名前は一瞬それっぽい?ものもあったりするものの、整数リテラルで記述可能、ということで微妙にこの目的には使えません。
ちょっと逃げるパターン
struct Sample<T> where T : BinaryInteger {
var data : [T]
func takeValuesAsInt() -> [Int] {
var values : [Int] = [Int](repeating: 0, count: data.count)
for i in (0 ..< data.count) {
values[i] = Int(data[i]) // OKになる!
}
return values
}
}
var sample = Sample(data:[Int8](repeating: 1, count: 3))
where T: BinaryInteger
を追加したことで、Int
への変換が通るようになります。
ただし今度は、
var sample = Sample(data:[Float](repearing: 1, count: 3))
これがアウトになっちゃう。Float
はBinaryInteger
ではないからですね。
Swiftの標準ライブラリにおいて、算術演算子の多重定義が異常に多くされていることを考えると、この問題はテンプレート/ジェネリックではなくて、何かの多重定義で逃げるしかないのかも知れません。
メソッドの引数・返値なら多重定義で回避しやすいのだけれども、ストレージの型を可変にする場合、どうしてもクラス・構造体のテンプレートになってしまうので、クラス・構造体ごと分けるとかいうことになってしまいそう。
それでは書く手間がかえって増えてしまいます。そう考えると、こういう設計(ストレージの数値型をテンプレート引数にとる)はそもそも良くないね、という結論になりそうです。
C++だとこんなことにはならない気がします。
(最近いじってないから正確には分かりませんが)
class Sample<T> {
var data : [T]
init(data: [T]) {
self.data = data
}
func takeValuesAsInt() -> [Int] {
var values : [Int] = [Int](repeating: 0, count: data.count)
for i in (0 ..< data.count) {
values[i] = takeValueAsInt(i)
}
return values
}
func takeValueAsInt(_ index: Int) -> Int {
assert(false, "takeValueAsInt is not implemented")
return 0
}
}
class SampleInteger<T> : Sample<T> where T : BinaryInteger {
override func takeValueAsInt(_ index: Int) -> Int {
return Int(self.data[index])
}
}
class SampleFloatingPoint<T> : Sample<T> where T : BinaryFloatingPoint {
override func takeValueAsInt(_ index: Int) -> Int {
return Int(self.data[index])
}
}
var sample1 = Sample(data:[Int8](repeating: 1, count: 3))
var sample2 = SampleInteger(data:[Int8](repeating: 1, count: 3))
//var sample3 = SampleInteger(data:[Float](repeating: 1, count: 3)) // これはコンパイルできない
var sample4 = SampleFloatingPoint(data:[Float](repeating: 1, count: 3))
var values : [Int]
values = sample2.takeValuesAsInt()
print(values)
values = sample4.takeValuesAsInt()
print(values)
values = sample1.takeValuesAsInt()
print(values)
やるとするとこんな感じですかね。struct
からclass
に変更しています。
[1, 1, 1]
[1, 1, 1]
__lldb_expr_89/MyPlayground.playground:125: Assertion failed: takeValueAsInt is not implemented
構造体は継承できないのでクラスにするしかないというのと、抽象メソッド・抽象クラスがないので本来インスタンス化されるべきでない基底クラスがインスタンス化されていてもコンパイル時には引っかけられない、というのがちょっと嫌な感じですかね。
構造体を継承させないメリットというのは何となく分かるような気はしますが、抽象メソッドが用意されない理由というのは調べてみたいところです。
(全然違う話になった)
再び話はちょっとずれる
SwiftではObjective-Cのような
- (void)doSomething:(id <MyProtocol>)arg {}
という感じの書き方、例えば
func doSomething(arg: any MyProtocol) {}
のようなことができなくて、C++のように
func doSmothing<T>(arg: T) where T : MyProtocol {}
のように書く、ということのようですね。
Array
とArraySlice
のどちらも受け付けられる関数を作ろうとしてはまりました。
Objective-Cから持ち込まれた用語や概念も多いのに、かなりC++に近い言語という気がしますね。
(何を今更、という気はしますが)
それよりも言語の仕様がまだちょくちょく変わっていっているところが、気にはなりますが。