書いて覚えるSwift入門

第5回遺産の継承(その2)

前回に引き続き、SwiftからCやObjective-Cの遺産を今回も活用します。前回はおもにC――厳密にはlibc――の機能をSwiftから使うにはどうしたらよいかを見ていきましたが、今回はObjective-C、つまりフレームワークをどう活用していくかを見ていくことにしましょう。

import(Cocoa | UIKit)

Objective-CではCにおけるlibcに相当するのが、OS XではCocoaiOSではUIKitです。ただしlibcよりできることははるかに多彩です。たとえば、あるURLにアクセスしてそのコンテンツを表示するというのは、Webすらなかった時代に作られたlibcでは簡単には書けませんが、CocoaUIKitであればわずかこれだけですリスト1⁠。

リスト1 CocoaやUIKitのサンプル
import Cocoa // OS X の場合。iOS ならUIKit
let url = "http://example.com/"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(
  contentsOfURL: NSURL(string:url)!,
  usedEncoding:&enc,
  error:&err
) {
    println(content)
} else {
    println(err)
}

なんと、NSString()という文字列を初期化するAPIに適切なパラメータを渡すだけで、ソケットを初期化し、HTTPプロトコルでサーバにアクセスし、その内容をGETしてくれるわけです。あまりに楽なので、スクリプト言語でプログラミングしているような感覚です図1⁠。

図1 リスト1の実行結果
図1 リスト1の実行結果

この場合URLのコンテンツはテキストですが、JSONをパースする機能すら基本装備していますリスト2⁠。

リスト2 JSONのパース機能を備えている
import Cocoa
let url = "http://api.dan.co.jp/asin/4534045220.json"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(
  contentsOfURL: NSURL(string:url)!,
  usedEncoding:&enc,
  error:&err
  ) {
    if let json = NSJSONSerialization.JSONObjectWithData(
        content.dataUsingEncoding(enc)!,
        options: nil, error: &err) {
        println(json)
    } else {
        println(err)
    }
} else {
    println(err)
}

しかしパースしたJSONから特定のアイテムを抜き取りたいとなると、かなり面倒なことになりますリスト3⁠。

リスト3 JSONから特定のアイテムを抜き出す
import Cocoa
let url = "http://api.dan.co.jp/asin/4534045220.json"
var enc = NSUTF8StringEncoding
var err:NSError?
if let content = NSString(contentsOfURL: NSURL(string:url)!, usedEncoding:&enc, error:&err) {
    if let json:AnyObject = NSJSONSerialization.JSONObjectWithData(
        content.dataUsingEncoding(enc)!,
        options: nil, error: &err) {
            if let item = json["ItemAttributes"] as? NSDictionary {
                if let author = item["Author"] as? NSString {
                    println(author)
                }
            }
    } else {
        println(err)
    }
} else {
    println(err)
}

JSONをサポートする多くの言語でjson["ItemAttributes"]["Author"]と一度に書けるところを、まずlet item = json["ItemAttributes"] as? NSDictionaryitemを取り出し、さらにlet author =item["Author"] as? NSStringNSStringを取り出しという具合に、型が静的であるというSwiftの特徴がアダとなってしまっています。どうにかしてjson["Item Attributes"]["Author"]と書く方法はないでしょうか? さらに可能ならJavaScriptのようにjson.ItemAttributes.Authorと書けないのでしょうか?

ラッパーのススメ

その試みがSwiftyJSONであり、拙作のSwift-JSONです。たとえばSwift-JSONならリスト3のコードは

let author = JSON(url:"http://api.dan.co.jp/asin/4534045220.json")["ItemAttributes"]["Author"].asString

と1行で済んでしまいます。さらにスキーマをclassとして実装すれば、リスト4のようにすら書けます。

リスト4 ラッパーの使用例
class ASIN : JSON {
    override init(_ obj:AnyObject){ super.init(obj) }
    override init(_ json:JSON) { super.init(json) }
    var ItemAttributes: ASIN { return ASIN(self["ItemAttributes"]) }
    var Author: String { return self["Author"].asString! }
}

let author = ASIN(url:"http://api.dan.co.jp/asin/4534045220.json").ItemAttributes.Author

SwiftyJSONやSwift-JSONはこれをどのように実現しているのでしょうか? ソースコード全体を読んでいただければ一目瞭然なのですが、SwifyJSONは1,163行、Swift-JSONは432行で紙幅にとても収まりません(2015年3月当時⁠⁠。ここではSwift-JSONのキモだけ解説します。

Swift-JSONのインスタンス変数は、たった1つです。

public class JSON {
    private let _value:AnyObject
    // ....
}

これに対し、subscriptは2種類定義されていますリスト5、リスト6⁠。

リスト5 subscript(idx:Int)
    public subscript(idx:Int) -> JSON {
        switch _value {
        case let err as NSError:
            return self
        case let ary as NSArray:
            if 0 <= idx && idx < ary.count {
                return JSON(ary[idx])
            }
            return JSON(NSError(
                domain:"JSONErrorDomain", code:404, userInfo:[
                    NSLocalizedDescriptionKey:
                    "[\(idx)] is out of range"
                ]))
        default:
            return JSON(NSError(
                domain:"JSONErrorDomain", code:500, userInfo:[
                    NSLocalizedDescriptionKey: "not an array"
                ]))
            }
    }
リスト6 subscript(key:String)
    public subscript(key:String)->JSON {
        switch _value {
        case let err as NSError:
            return self
        case let dic as NSDictionary:
            if let val:AnyObject = dic[key] { return JSON(val) }
            return JSON(NSError(
                domain:"JSONErrorDomain", code:404, userInfo:[
                    NSLocalizedDescriptionKey:
                    "[\"\(key)\"] not found"
                ]))
        default:
            return JSON(NSError(
                domain:"JSONErrorDomain", code:500, userInfo:[
                    NSLocalizedDescriptionKey: "not an object"
                ]))
            }
    }

つまり、json[0]のように添え字がIntであればインスタンス変数をNSArrayとみなし、json["name"]のように添え字がStringであればNSDictionaryとみなして、その要素から新たなJSONオブジェクトを生成しているわけです。そして要素が存在しない場合は、NSErrorからJSONオブジェクトを生成し、インスタンス変数がNSErrorの場合はそのまま自分自身を返すことで、HaskellのEitherが一度NothingになればずっとNothingであるように、最初に発生したエラーが引き継がれるというわけです。

このようなラッパーは同等の機能をフルスクラッチでSwiftで書くよりずっと簡単に書けますし、書くことによってSwiftとObjective-Cの連携がどのようになされているかを体得することもできます。読者の皆さんも、これぞというものがあったらぜひ書いて、GitHubなどで公開してみてください。

AnyObjectとAnyの違い

SwiftのAnyObjectは、Objective-Cにおけるidに相当します。id同様なんでも入りますが、適切に使うにはisで適切な型を判定したり、asで適切な型に変換したりしなければなりません。

また、CocoaUIKitなど、Objective-C由来のフレームワークをimportしておく必要もあります図2⁠。

import Cocoa
var ao:AnyObject
ao = "assign"
// ao += " any value" // error
ao = (ao as String) + " any value"
ao = 40
// ao += 2            // error
ao = (ao as Int) + 2
図2 AnyObjectの実行例
図2 AnyObjectの実行例

ところがSwiftにはAnyObjectとは別にAnyという型も存在します。前述のAnyObjectAnyに変えてもそのまま動いてしまいますしimport Cocoaをコメントアウトしてもそのまま動いてしまいます図3⁠。

図3 Anyの実行例
図3 Anyの実行例

なぜ、Swiftには「なんでもありな型」が2つも存在するのでしょう?

sizeof()で双方の型を見てみると、面白いことがわかります。64bitプラットフォームではsizeof(AnyObject)は8なのに対し、sizeof(Any)は32。AnyObjectは1ワード、Anyは4ワードです。賢明な読者であれば、この時点で予想がつくでしょう。AnyObjectは参照、つまりclassであるのに対し、Anyは実値、つまりstructなのです。

さらに「禁断の組込み関数⁠⁠、unsafeBitCastを使ってAnyがどうなっているのかを見てみましょうリスト7、図4⁠。

リスト7 unsafeBitCastでAnyの動きを調べる
import Cocoa
let s = "Swift"
var a:Any
a = s
var aq = unsafeBitCast(a, (UInt,UInt,UInt,UInt).self)
var sq = unsafeBitCast(s, (UInt,UInt,UInt).self)
let i = 42
a = i
aq = unsafeBitCast(a, (UInt,UInt,UInt,UInt).self)
var a1 = unsafeBitCast((42,0,0,aq.3), Any.self)
a as Int == a1 as Int
図4 リスト7の実行例
図4 リスト7の実行例

なんのことはない。4ワードのうち頭から本来の値を詰め込んだうえで、最後の1ワードに「型ID」が入っているだけのです。SwiftのStructは、IntDoubleが1ワード、関数が2ワード、StringArrayDictionaryが3ワードなので、Anyの中にすべて納まります。

これに対し、AnyObjectの正体は、Objective-Cで書けばid *オブジェクトへのポインタで、型情報はAnyのように値そのものの一部ではなくその参照先に格納されています。

それではAnyはどこで使われているかというと、Xcodeの内部です。Xcodeは現在書かれているコードにあわせて振る舞いを変えますが、この振る舞いを受け取る関数は当然ありとあらゆる型を受け取れなければなりません。Swiftにはreflect()という関数がありますが、これがAnyを活用している関数の1つで、これを用いると内観(introspect)するためのコードを自作することもできます。

しかしそうでもない限り、Anyを使うケースはほとんどないでしょう。以前紹介したようにSwiftには総称関数とプロトコルがあるので、静的型の特長を活かすためにもAnyの使用は避けるべきです。まとめると次のようになるでしょう。

  • AnyObjectは、Objective-Cで書かれたフレームワークの連携においてのみ使う
  • Anyは使わない(複数の型を受け付けるコードには、総称関数とプロトコルを用いる)

続きは次号

今回はSwiftからObjective-Cのフレームワークを用いる例としてSwift-JSONを紹介し、AnyObjectAnyの違いを垣間見ました。次回はXcodeでCおよびObjective-CのコードとSwiftのコードを同一のプロジェクトで連携する例を見ていくことにします。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧