SwiftでNSMethodSignatureはどうなったか?

LINEで送る
Pocket

動的なメソッド呼び出しにはNSMethodSignatureを使用しました。
なかなか便利な機能なのですが、Swiftではなくなっています。

このことについてSwiftブログで解説されています。

Bringing the Cocoa frameworks to Swift gave us a unique opportunity to look at our APIs with a fresh perspective. We found classes that we didn’t feel fit with the goals of Swift, most often due to the priority we give to safety. For instance, some classes related to dynamic method invocation are not exposed in Swift, namely NSInvocation and NSMethodSignature.Swift Blog

NSMethodSignatureはどうなったか?

SwiftにCocoaフレームワークを用いることで、APIを見るときに新鮮な視点を持つことができました。安全性に重点を置いてみると、Swiftの目的とは合致しないクラスを見つけました。例えば、動的なメソッド呼び出し、すなわちNSInvocationNSMethodSignatureはSwiftでは公開されていません。

これらが存在しないことに気づいた会社から最近バグレポートがあがりました。この開発者はObjective-Cでメソッドの引数の型をチェックするのにNSMethodSignatureを使用していて、Swiftにこのコードを移植するときにNSMethodSignatureがないことに気づきました。移植しようとしたコードは以下のようにさまざまな用法のHTTPハンドラーを受け入れることができました。

func handleRequest(request: HTTPRequest, queryStringArguments: [String: String]) { }
func handleRequest(request: HTTPRequest, jsonBody: JSON) { }

Objective-CではNSMethodSignatureは最初のメソッドは引数には[String: String]を要求し、2番めのメソッドはJSONを要求することを決定するために使用することができます。しかしSwiftは強力な言語でありNSMethodSignatureを使わずに簡単に、コンパイラが型やメモリの安全性のために支援することを損なうことなしに、このシナリオを扱うことができます。

Swiftで同じ問題を解決する別の方法があります。

struct HTTPRequest {
	// ...
}

protocol HTTPHandlerType {
	typealias Data

	/// :returns: true if the request was handled; false otherwise
	func handle(request: HTTPRequest, data: Data) -> Bool
}

まず、このインターフェースを通してHTTPRequestが扱うことができることが何かを定義するプロトコルを用意します。このプロトコルは単純で一つのメソッドだけを持ちます。

なぜ、HTTPHandlerクラスのサブクラスでなく、プロトコルを使用するのでしょうか。プロトコルにはこのコードのクライアントまでの実装を詳細にする必要がない柔軟性があるからです。もしHTTPHandlerクラスを作ったら、クライアントにも参照する型のセマンティクスを強制されるため、クラスを使用しないといけなくなくなります。しかしプロトコルを用いることによって、クライアントは自分のコードに適切な型を選ぶことができます。クラスでも構造体でも列挙型でも使用できます。

class HTTPServer {
	func addHandler(handler: T) {
		handlers.append { (request: HTTPRequest, args: Any) -> Bool in
			if let typedArgs = args as? T.Data {
				return handler.handle(request, data: typedArgs)
			}
			return false
		}
	}

	// ...
}

つぎに、HTTPServerクラスはパラメータとしてHTTPHandlerTypeを受け入れるようなジェネリックメソッドを持ちます。handlerの関連する型を使用することで、このhandlerがリクエストを扱うかどうかを決定するためにargsパラメータを条件付きで実行します。ここにHTTPHandlerTypeをプロトコルとして定義した利点があるのです。HTTPServerはhandlerがリクエストに反応しているかどうかを知る必要がなく、handlerそのものの性質を気にする必要がないのです。与えられた値によってリクエストの扱いを決めることができるということだけ知っておけばいいのです。

class HTTPServer {
	// ...

	private var handlers: [(HTTPRequest, Any) -> Bool] = []

	func dispatch(req: HTTPRequest, args: Any) -> Bool {
		for handler in handlers {
			if handler(req, args) {
				return true
			}
		}
		return false
	}
}

HTTPServerがリクエストを受け取ると、handlersを順にそのリクエストを扱えるかどうかをチェックします。

これで簡単にさまざまな型をもつ独自HTTPHandlerTypeを作成し、HTTPServerに登録していくことができます。

class MyHandler : HTTPHandlerType {
	func handle(request: HTTPRequest, data: Int) -> Bool {
		return data > 5
	}
}

let server = HTTPServer()
server.addHandler(MyHandler())
server.dispatch(HTTPRequest(...), args: "x") // returns false
server.dispatch(HTTPRequest(...), args: 5)   // returns false
server.dispatch(HTTPRequest(...), args: 10)  // returns true

プロトコルとジェネリクスを組み合わせることで、さまざまな型を扱うHTTPハンドラーを作り登録するSwiftコードをエレガントに書いてみました。この方法は型安全であることをコンパイラが保証することができ、優れたランタイム性能を確保します。

この記事に関するplaygroundはこちらからダウンロードすることができます。
Request.playground

LINEで送る
Pocket

Tags:

コメントを残す