Swift で自作のオブジェクトを for ... in で使えるようにする

Swift プログラミング

Swift で自作オブジェクトを SequenceType に準拠して、for ... in 構文で使えるようにしてみました。

他にもいろんな、順次取得が可能なオブジェクト向けに用意された機能が使えるようになります。


オブジェクトを for ... in 構文に対応させるには

Swift言語 で独自のクラスや構造体をfor ... in 構文で使いたいときは、オブジェクトをSequenceTypeプロトコル に準拠させます。

SequenceTypeプロトコル

SequenceTypeプロトコル は次の機能を要求します。

protocol SequenceType : _Sequence_Type {

    typealias Generator : GeneratorType

    func generate() -> Generator
}

オブジェクトにこのgenerateメソッド を実装することで、このオブジェクト内の要素をひとつひとつ取り出すのに使うイテレータをGenerator型 として取り出せるようになります。

このGenerator型 は、GeneratorTypeプロトコル に準拠したオブジェクト型が要求されています。

取り出せる値の型はGeneratorTypeプロトコル の実装によって決まります。

GeneratorTypeプロトコル

GeneratorTypeプロトコル は次の機能を要求します。

protocol GeneratorType {

    typealias Element

    mutating func next() -> Element?
}

このプロトコル時準拠したジェネレーターオブジェクトを作成します。

ジェネレーターには、扱うデータと読み進めた位置を記憶しておいて、必要時にnextメソッド を使って次の値を取り出せる機能を実装します。ここでnil を返したとき、最後まで値が読み込まれたと判断されます。

ここのnextメソッド が返す型が、for ... in 構文で取得できる値の型になります。

SequenceTypeプロトコル に準拠したオブジェクトを定義する

それでは、for ... in 構文で使えるオブジェクトを定義してみます。

今回は、たとえば三角関数sin の値を計算するSineClassクラス を作成して、それをfor ... in 構文で使うと『0°から 360°未満まで 1°ずつ順番に取得できる』ようにしてみます。

SineClassクラス の定義

まず、三角関数sin の値をvalue:メソッド で取得できるSineClassクラス を作成します。

for ... in でも使えるようにSequenceTypeプロトコル に準拠させ、ジェネレーターオブジェクトを取得するgenerateメソッド も実装しています。

class SineClass : SequenceType {
	
	/// SequenceType に準拠するための、ジェネレーターを返すメソッドです。
	func generate() -> SineGenerator {

		return SineGenerator(self)
	}
	
	/// 引数で受け取ったラジアンを使って sin を計算します。
	func value(radian:Double) -> Double {
		
		return sin(radian)
	}
}

SineGeneratorクラス の定義

上記のクラスのジェネレーターとして使うSineGeneratorクラス では、今回は、元になる値を受け取ってインスタンスを生成するイニシャライザーと、現在位置を保持するangleプロパティ を用意してみます。

また、GeneratorTypeプロトコル に準拠するために、次の値を取得するnextメソッド も実装しています。

class SineGenerator : GeneratorType {
	
	private unowned var owner:SineClass
	private var angle:Double = 0
	
	/// 扱うデータを持つインスタンスを受け取って初期化しています。
	init(_ owner:SineClass) {
		
		self.owner = owner
	}

	/// 次の値を取得しつつ、その次を取得できるように準備します。	
	func next() -> Double? {
		
		let π:Double = 3.141592653589793
		let getRadianFromAngle = { (angle:Double) in
			
			angle * π / 180.0
		}
		
		let getValue = { (radian:Double) in
			
			self.owner.value(radian)
		}
		
		if self.angle < 360 {
			
			return getValue(getRadianFromAngle(self.angle++))
		}
		else {
			
			return nil
		}
	}
}

今回の例であれば、このジェネレーターが生成されるとき、読み進めた位置を示すangleプロパティ を 0 で初期化しておいてます。

そしてnextメソッド で値を読み進めるたびに、現在位置に該当するsin の値を返しつつ、読み進めた位置を増加させています。

自作オブジェクトを for ... in で使用する

自作オブジェクトをSequenceTypeプロトコル に準拠させたら、あとは普通にfor ... in 構文で使用できます。

let sine = SineClass()

for x in sine {
	
}

このようにすることで、SineClassクラス のインスタンスからジェネレーターが生成され、そこからひとつずつ値が取り出されて処理されます。

SequenceTypeプロトコル に準拠することで得られる機能

自作オブジェクトをSequenceTypeプロトコル に準拠させれば、それ以上のコードを書かなくても、Swift に用意されているSequenceType 用の機能を利用できるようになります。

以下ではそのうちのいくつかの機能を紹介します。

オブジェクトを使って配列を生成する

Swift言語Array型 は、SequenceType型 を初期値に取れるので、自作オブジェクトのインスタンスを使って配列を初期化できます。

let values = [Double](SineClass())

たとえばこうすることで、クラスのジェネレーターが生成する値を順番にセットした配列を生成できます。

オブジェクト内に特定の値が含まれるかを判定する

contains関数 を使って、指定した値がインスタンス内に存在するかを判定できます。

たとえば、上記の例で挙げたSineClassクラス は『0°から 360°未満まで 1°刻みで sin の値を取得できる』設計になっているので、その中に指定した値が含まれるかを判定できます。

if contains(SineClass(), 0.05233595624294383) {
	
}

値を条件でフィルターする

filter関数 を使って、指定した条件に合致するものだけを抽出できます。

let values = filter(SineClass()) { $0 < 0.1 }

各要素を加工した値を取得する

たとえば各要素をそれぞれ 2 倍した配列を取得したいときにmap関数 を支えます。

let values = map(SineClass()) { $0 * 2 }

要素の最大値や最小値を取得する

SequenceTypeプロトコル に準拠したオブジェクトであれば、maxElement関数minElement関数 を使って、内包する値の最大値や最小値を取得できます。

let maxValue = maxElement(SineClass())
let minValue = minElement(SineClass())

各要素を順に処理して値を取得する

reduce関数 を使うと、初期値と最初の値、その二つを使って計算した値と次の値、そうして計算した値と次の値…というように、内包する値を順番に使って、値を計算できます。

たとえば、全ての要素の絶対値を足し合わせたいときは、初期値を 0.0 として、各要素の絶対値を足し合わせるクロージャーを添えて、次のようにreduce関数 を実行します。

let values = reduce(SineClass(), 0) { $0 + abs($1) }

各要素を並び替えた配列を取得

sorted関数 を使うと、内包する要素を並び替えて配列で取得できます。

次の例では、インスタンスが内包する値を降順で取得しています。条件を指定しない場合は昇順で並べられます。

let values = sorted(SineClass()) { $0 > $1 }