Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Swiftでジェネリクスを使った構造体/クラスを実装してみた

More than 5 years have passed since last update.

はじめに

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というジェネリクスを使った構造体を作り、(構造体の派生は出来ないので)typealiasPointIなどを定義します。

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が出てきたのでしょう??)

スクリーンショット 2014-10-01 10.50.41.png

たぶん、「'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型にしてから計算するという方針を採ります。

とりあえず実装してみますが、真っ赤っかになります。

スクリーンショット 2014-10-01 11.17.55.png

たぶん、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}

終わりに

いろいろな型をジェネリクスで吸収するという目的はそこそこ達成できたのではないかと思われます。
もしジェネリクスが得意な方で他に良い実装方法などをご存知の方がいらっしゃいまいしたら、教えていただければ幸いです。

takabosoft
仕事でWinかiOS向けのアプリを作っています。 「Dominoの作者」とか「EDGEの作者」とか言われることが多いです。
http://takabosoft.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away