はじめに
Swiftはジェネリクスをサポートしており、配列や辞書を作る際に型を指定できるようになりました。
こういう有り物を使う側としてはとっても便利というイメージがあるジェネリクスですが、いざ自分でこういったモノを作ろうと思ったらいろいろハマってしまったので、実装中に出くわしたエラーと、それをどうやって乗り切ったかを記録として残しておきます。今後ジェネリクスを使う方のヒントにでもなれば幸いです。
- Xcode 6.0.1
- Playground
- 私はジェネリクス初心者です
動機
CGPoint
型のx,y要素はCGFloat
型となっており、CGFloat
型は32ビットCPU環境では32ビット長(Float相当)、64ビットCPU環境では64ビット長(Double相当)という仕様になっています。
で、これがちょっと困る場合があるため、
「CPU依存しないPoint型(2D座標型)が作りたい」
「ついでに整数型にも対応したい」
...と思ったのです(本当に使うかは知らない)。
つまり、PointI
型(Int32
型)、PointF
型(Float
型)、PointD
型(Double
型) 等々を作りたいのですが、それなら
**ジェネリクス使えば一気にできるんじゃね?**と。
とりあえずベースとなる部分を作ってみた
PointT
というジェネリクスを使った構造体を作り、(構造体の派生は出来ないので)typealias
でPointI
などを定義します。
struct PointT<T> {
var x: T
var y: T
init(_ x: T, _ y: T) {
self.x = x
self.y = y
}
}
// 各種型を定義
typealias PointI = PointT<Int32>
typealias PointF = PointT<Float>
typealias PointD = PointT<Double>
// テスト
var i = PointI(0, 0) // {x 0, y 0}
var f = PointF(1.5, 1.5) // {x 1.5, y 1.5}
var d = PointD(2.2, 2.2) // {x 2.2, y 2.2}
たぶんここまでは誰でも書けると思われます。
offset関数を実装してみる
2D座標クラスにはありがちのoffset
関数(座標を指定した分移動させる関数)を実装してみます。
で、よくわからないエラーが出て怒られます。
(どこからUInt8
が出てきたのでしょう??)
たぶん、「'T'
型が足し算できるなんてこっちは知らんから」という理由でエラーが出ていると予想されます。
こうなった場合は、'T'型に制約を付けて、足し算(+=
)できる型のみ受け付けるようにすれば良いんですよね。
たぶん、四則演算用のプロトコルがすでに定義されてるはず...
...無いし?
というわけで、自分でPointTElement
というプロトコルを定義し、それを制約にしました。
protocol PointTElement {
func +=(inout lhs: Self, rhs: Self)
}
struct PointT<T: PointTElement> {
var x: T
var y: T
init(_ x: T, _ y: T) {
self.x = x
self.y = y
}
mutating func offset(cx: T, _ cy: T) {
x += cx
y += cy
}
}
// 各種型を定義
typealias PointI = PointT<Int32>
extension Int32: PointTElement {
}
typealias PointF = PointT<Float>
extension Float: PointTElement {
}
typealias PointD = PointT<Double>
extension Double: PointTElement {
}
// テスト
var i = PointI(0, 0)
i.offset(5, 5)
i // {x 5, y 5}
var f = PointF(0, 0)
f.offset(5, 5)
f // {x 5.0, y 5.0}
var d = PointD(0, 0)
d.offset(5, 5)
d // {x 5.0, y 5.0}
extension
で各型をPointTElement
に準拠させるのがポイントでしょうか。
+=
演算子自体は各型ですでに実装されているので、他にやることはありません。
ちなみにfunc +=(inout lhs: Self, rhs: Self)
←こういった書き方はSwiftの定義ソースから適当に拝借してきて型だけSelf
に置き換えています。
2点間の直線距離を求める関数を実装してみる(T型からDouble型へ変換)
こちらも2D座標クラスにはありがちのやつです。ピタゴラスの定理でしたっけ。
冗長かもしれませんが、一旦Double
型にしてから計算するという方針を採ります。
とりあえず実装してみますが、真っ赤っかになります。
たぶん、Double
型のイニシャライザにT
型なんていう謎の型が来られても知らないし!って事でエラーが出てるんだと思います。
ここはどう回避してよいのかわからなかったので、仕方なくPointTElement
プロトコルにDouble
型変換用のプロパティを定義し、それを使うようにしました。
import Darwin
protocol PointTElement {
func +=(inout lhs: Self, rhs: Self)
var doubleValue: Double { get } // これ
}
struct PointT<T: PointTElement> {
var x: T
var y: T
init(_ x: T, _ y: T) {
self.x = x
self.y = y
}
mutating func offset(cx: T, _ cy: T) {
x += cx
y += cy
}
func distance(pt2: PointT<T>) -> Double {
let cx = x.doubleValue - pt2.x.doubleValue
let cy = y.doubleValue - pt2.y.doubleValue
return sqrt(cx * cx + cy * cy)
}
}
// 各種型を定義
typealias PointI = PointT<Int32>
extension Int32: PointTElement {
var doubleValue: Double {
return Double(self)
}
}
typealias PointF = PointT<Float>
extension Float: PointTElement {
var doubleValue: Double {
return Double(self)
}
}
typealias PointD = PointT<Double>
extension Double: PointTElement {
var doubleValue: Double {
return self
}
}
// テスト
var i = PointI(0, 0).distance(PointI(5, 5)) // 7.07106781186548
var f = PointF(0, 0).distance(PointF(5, 5)) // 7.07106781186548
var d = PointD(0, 0).distance(PointD(5, 5)) // 7.07106781186548
逆にDouble型からT型へ変換する場合
ちょっと良い題材(関数)が思いつかなかったので急にこんな感じになっちゃいましたが、
Double
型で計算した結果をT
型(またはPointT
型)へ戻すような事をしたいケースもあるかと思います。
こういった場合は、プロトコルをいじり...
protocol PointTElement {
func +=(inout lhs: Self, rhs: Self)
var doubleValue: Double { get }
init(_ v: Double) // これ
}
...のようにDouble
型を引数に持つイニシャライザを定義してあげれば良さそうです。
大抵の型は、すでにこのイニシャライザが実装されているので、他にやる事はありません。
struct PointT<T: PointTElement> {
(中略)
func hoge() -> PointT {
let d: Double = 0
return PointT(T(d), T(d)) // これ
}
}
CGPoint型へ変換する方法を用意してみる
Double
型への変換と同じようにCGFloat
型への変換をプロトコルで定義するしかないのかなと。
protocol PointTElement {
func +=(inout lhs: Self, rhs: Self)
var doubleValue: Double { get }
var CGFloatValue: CGFloat { get } // これ
init(_ v: Double)
}
(プロパティ名の頭文字が大文字になるのがちょっと気持ちよくないのが失敗した感あります)
あとは各型で定義し、CGPoint
の変換用イニシャライザを用意してあげれば動きます。
// 各種型を定義
typealias PointI = PointT<Int32>
extension Int32: PointTElement {
var doubleValue: Double {
return Double(self)
}
var CGFloatValue: CGFloat {
return CGFloat(self)
}
}
typealias PointF = PointT<Float>
extension Float: PointTElement {
var doubleValue: Double {
return Double(self)
}
var CGFloatValue: CGFloat {
return CGFloat(self)
}
}
typealias PointD = PointT<Double>
extension Double: PointTElement {
var doubleValue: Double {
return self
}
var CGFloatValue: CGFloat {
return CGFloat(self)
}
}
extension CGPoint {
init<T>(_ pt: PointT<T>) {
x = pt.x.CGFloatValue
y = pt.y.CGFloatValue
}
}
// テスト
var i = PointI(15, 20)
var j = CGPoint(i) // {x 15 y 20}
終わりに
いろいろな型をジェネリクスで吸収するという目的はそこそこ達成できたのではないかと思われます。
もしジェネリクスが得意な方で他に良い実装方法などをご存知の方がいらっしゃいまいしたら、教えていただければ幸いです。