結果が自明であるにもかかわらず、数値をキャストしなければいけないのはとても煩わしいです。この講演では、Rich Foxは、型安全性をほんのわずか犠牲にしますが、非常に便利な方法を紹介します。Swift 2.0のプロトコル拡張、パターンマッチ、ジェネリクス、および演算子のオーバーロードを使用し、異なる型同士の数値演算をシンプルにします。
こんにちは、私はRichard Foxといいます。Propeller Labs社で働くiOSデベロッパーです。こちらが私のブログです。Propeller Labsは、MVPクラスのデベロッパーとモバイルに特化した開発会社です(もし興味があるなら、我々は現在、共に働くSwiftに熱心なプログラマーを探しています)。私が今日話すのは、Swiftでキャスト不要な数値演算をどう実現するかについてです。
算術の比較 (01:06)
Objective-Cではこのように書きます。
float w = 5;
double x = 10;
int y = 15;
CGFloat z = w + x + y;
そしてSwiftで同じ処理は以下のように書きます。
let w:Float = 5
let x:Double = 10
let y:Int = 15
let z:CGFloat = CGFloat(w) + CGFloat(x) + CGFloat(y)
強い型付けはすばらしいですが、問題でもあります。もっと簡潔に書きたいです。
Swift 2.0以前 (01:31)
Swift 2.0以前にも私はこの問題を解決しようとしました。これは数値型に対するシンプルなエクステンションで、getterメソッドを追加します。これはうまく動作しました。ただし、同じような5つのgetterメソッドをそれぞれの数値型ごとに書くことになり、重複したコーディングを強いられるのが欠点でした。
extension Double {
var c:CGFloat {
return CGFloat(self)
}
//. . .
}
extension Int {
var c:CGFloat {
return CGFloat(self)
}
//. . .
}
extension Float {
var c:CGFloat {
return CGFloat(self)
}
//. . .
}
//let y:CGFloat = 1 + x.c
私はSwift 2.0のプロトコルエクステンションに触発され、互換性のある数値型のすべてに対して、あるプロトコルを作成しました。そうすると、それぞれの数値型をキャストするときには、パターンマッチを活用して、シンプルなドット記法を使用することができるようになりました。
数値型のキャストの仕組み (02:54)
それぞれの数値型は構造体です。数値型の定義には、それぞれの数値型からそれぞれの他の数値型へキャストするイニシャライザがあります。それぞれのイニシャライザは標準ライブラリで定義されています。
init(_ v: Float ), init(_ v: Double)
let x: Float = 5.0
let y = Float(x) OR let y = Float.init(x)
float.initとすることもキャストすることもできます。コードのどこでもCommand+クリックして、標準ライブラリに存在することを確認できます。
この定義を動作させるため、キャストするために必要とされるイニシャライザを使用し、我々は我々のプロトコルを定義します。我々は我々の数値型を、変換可能なプロトコルの数値型に拡張します。すべての数値型は既にすべてのこれらのinitメソッドを実装済みなので、何も追加せずとも動作するはずです。ただし、CGFloat
を除いては。
CGFloat
は他と少し異なります。この型は標準ライブラリで定義されていません。それはCore Graphicsの一部です。標準ライブラリの数値型と異なり、キャスト用のinitメソッドを持っていませんが、シンプルなエクステンションを用いて簡単に作成することができます。CGFloat
を拡張して、self = value
(自身の型)とすることができます。
protocol NumberConvertible {
init (_ value: Int)
init (_ value: Float)
init (_ value: Double)
init (_ value: CGFloat)
}
extension CGFloat : NumberConvertible {}
extension Double : NumberConvertible {}
extension Float : NumberConvertible {}
extension Int : NumberConvertible {}
extension CGFloat{
public init(_ value: CGFloat){
self = value
}
}
パターンマッチング (05:02)
switch self {
case let x as CGFloat:
print("x is a CGFloat")
case let x as Float:
print("x is a CGFloat")
case let x as Int:
print("x is a CGFloat")
case let x as Double:
print("x is a CGFloat")
default:
print("x is unknown..")
}
selfが何の型なのか把握するために、上記のパターンマッチをプロトコル中で使用します。selfが何の型なのか知ることで、プロトコルで定義したイニシャライザの一つにそれを適合させることができます。そのようなキャストは、Swiftの不具合により、ときどきメモリリークを引き起こしましたが、Swift 2.1で修正されています。
エクステンションの作成 (05:56)
extension NumberConvertible {
private func convert<T: NumberConvertible>() -> T {
switch self {
case let x as CGFloat:
return T(x) //T.init(x)
case let x as Float:
return T(x)
case let x as Double:
return T(x)
case let x as Int:
return T(x)
default:
assert(false, "NumberConvertible convert cast failed!")
return T(0)
}
}
public var c:CGFloat{
return convert()
}
//...
}
エクステンションの中で、private関数としてconvert()
を定義します。convertはジェネリックタイプを戻り値として返却します。イニシャライザを持つ変換可能なすべての数値型に対し、selfが何の型なのか調査するコードを挿入します。selfの型が決定しさえすれば、イニシャライザのT.init(x)
を呼ぶことができます。というのも、TはNumberConvertible
に適合するからです。こうすることでドットプロパティのgetterメソッドを使用し、キャストできます。このエクステンションの中に、まとめてその他すべての数値型のドットプロパティのgetterメソッドを追加できます。
キャスト無しの変換 (07:29)
これで型を宣言することなくキャストできるようになりました。もしconvertメソッドがprivateではなかったら、2つの数値型について、.convert()
メソッドを実行し、それをYに代入します。そして、キャストしなくても実際の型が何かわかります。
let w: Double = 4.4
let x: Int = 5
let y: Float = w.convert() + x.convert()
しかし、もっと簡単にできます。以下はおそらくもっともシンプルなケースです。2つの異なる数値型があり、そこから3番目の異なる数値型を返却するような場合です。
let x: Int = 5
let y: CGFloat = 10
let z: Double = x + y
演算子のオーバーロードを使用します。NumberConvertible
準拠の3つのジェネリクスを使用し、演算子をオーバーロードします。演算子のlhs
とrhs
引数に対して、その両方にconvert()
メソッドを適用することで、引数は同じ数値型となります。標準ライブラリの定義を使用し(もはやそれらは同じ型なのでそれが可能になります)、演算します。ジェネリックな戻り値にキャストするため、もう一度convert()
メソッドを使用します。以下がその例です。
func + <T:NumberConvertible, U:NumberConvertible, V:NumberConvertible>(lhs: T, rhs: U) -> V {
let v: Double = lhs.convert()
let w: Double = rhs.convert()
return (v + w).convert()
}
加算の演算子をオーバーロードして最初のケースを解決します。これに2番目の演算子と3番目の数値型を追加してみましょう。全部で4つの数値型を扱います。しかし、期待と違って、以下のコードはうまく動作しません。
func + <T:NumberConvertible, U:NumberConvertible, V:NumberConvertible>(lhs: T, rhs: U) -> V {
let v: Double = lhs.convert()
let w: Double = rhs.convert()
return (v + w).convert()
}
コンパイラはどの演算子の定義を使用するのか (09:44)
最初の演算子は、新しく定義したカスタム演算子が使用されるように見えます、そして、2番目の演算子は標準ライブラリの定義を使用しています。カスタム演算子の戻り値はジェネリックな型なので、最初のオペレーターは3番目の数値型を推測するための手がかりが必要です。3番目の数値型はfloatなので、2回目の演算のとき、floatとfloatがあるということになります。いくつかの理由により、現在のコンパイラは、floatではない戻り値を探せるほど賢くはありません・・・
コンパイラを導く (10:51)
まず最初に試みたのは、コンパイラと妥協することでした。
public typealias PreferredType = Double
public func + <T:NumberConvertible, U:NumberConvertible>(lhs: T, rhs: U) -> PreferredType
{
let v: PreferredType = lhs.convert()
let w: PreferredType = rhs.convert()
return v+w
}
これで戻り値の型が1つの場合は動作します、しかしそれはdouble型しか返すことができません。コンパイラは満足しますが、それは優れた解決策ではありません。1つの型を返却できるだけです。
コンパイラにさらなる選択肢を与えることができます。先ほどの定義を利用し、それを複製します(lhs
とrhs
が両方とも同じ数値型である2つのジェネリクスのみを使用しましょう)。もちろん、まだ、推測された戻り値を持っています(もともとの定義)。
これらの演算子を一緒に使用することで、目的はほとんど達成されます。しかしながら、ある地点でコンパイラは混乱し、どちらを使用すれば良いのか分からなくなります。
そこで作成した余計なオーバーロードを削除します。ここでゼロを追���することにより、コンパイラはエラーを吐かなくなります。これは、2つのオペレーションを一緒に持っていることが問題のように見えます。
演算子の最適化 (13:39)
extension NumberConvertible {
private typealias CombineType = (Double,Double) -> Double
private func operate<T:NumberConvertible,V:NumberConvertible>(b:T, @noescape combine:CombineType) -> V{
let x:Double = self.convert()
let y:Double = b.convert()
return combine(x,y).convert()
}
}
public func + <T:NumberConvertible, U:NumberConvertible,V:NumberConvertible>(lhs: T, rhs: U) -> V {
return lhs.operate(rhs, combine: + )
}
public func - <T:NumberConvertible, U:NumberConvertible,V:NumberConvertible>(lhs: T, rhs: U) -> V {
return lhs.operate(rhs, combine: - )
}
プロトコルのさらに拡張することによって、改良を加えることができます。数値型の変換に話を戻します。2つのdoubleを引数として持ち、doubleを戻り値として返却する関数をプロトコル拡張に追加しました。そして、この関数を操作する、ジェネリックな型を引数として持つ関数を定義しました。また、四則演算のエイリアスであるCombineType
関数、および異なるジェネリックな型を返却する関数を追加しました。
実装の内側で、私はselfとジェネリックな入力をdouble型へと変換しました。そうすることにより、それらすべてを四則演算関数に適用できます。その結果、それを戻り値の型へと変換できます。そして、以前の演算子の実装を一行で置き換えることができます(lhs.operate、rhs、関数のための単一のたった一つの演算子)。それは見映えも良く、その他のすべての算術演算で簡単に使用可能です。
“Expression Too Complex” (15:21)
“The expression was too complex to be solved in a reasonable time”このエラーを回避するには、式を2つに分割するか、もしくは複数の式(私が複数のオーバーロードされた演算子を使用していた時よりもさらに多くの式)の作成しなければなりません。
もしあなたがこの種のエラーに出くわしたなら、あなたは怒りのツィートをChis Lattner氏に#ExpressionTooComplexというハッシュタグとともに投げかけるかもしれません。それは、実際にはそれほど複雑そうには見えないケースかどうか判断する手助けをするかもしれません。あなたはアンテナを張ることはできますが、やれることはそう多くはありません。
結論 (16:47)
結論として、いくつかの条件付きのキャスト不要な算術演算を作ることができました。ときどき若干の型推論の混乱に見舞われますが、これは0を加算することで解決します。また、Complex Expressionエラーに見舞われる恐れがあります。もともとの実装であるドットプロパティ変換はそんなに悪い実装ではありません。私はそのメソッドを個人プロジェクトでもプロダクション環境でも使用しています。なぜならそれは、型安全性をたくさん取り除くことはないからです。
キャスト不要な演算のすべてのコードはこちらから利用することができです。
Q&A (17:51)
Q: Swiftは算術演算のオーバーフロー時に、エラーを引き起こす代わりに、演算を続行することを明示的に指し示すアンパサンド(&)オペレーターを持っています。このアプローチを採用すると、潜在的にデータを廃棄する可能性があるので、これについて明確にするため、別の演算子を使用すべきでしょうか。
Rich: その考えに同意します。私は単に自分の趣味と知的好奇心から今回の発表を行いましたが、可読性が上がるということで、実行可能な解決方法になっていると思います。
翻訳: Daisuke Adachi
About the content
This content has been published here with the express permission of the author.