【Swift】Tips: あると便利だったextension達(型名編)

はじめに

モバイルアプリサービス部の中安です。

アプリを作ってきた際に、あるとなかなか便利だったextensionをボチボチとご紹介していければと思っています。 アプリの目的に合わせてやりかえる必要があったり、もっとよい実装があるかもしれませんが、何かの役に立てば光栄です。

今回は型名編です。

ある値の型が何であるかを把握するのは当たり前ですが大事です。 しかし、デバッグなどをしていると変数の中身が何かがわかりづらいときもあります。
デバッガを使えばいいという話なのですが、さらっとログに出したいときなどもあり、難なく型名を取得する機能を付け加えてみたら思いのほかに便利でした。

型名を取得する

今回はまず渡した値の型名を返してくれるクラスを定義するところから始めます。クラス名は Reflection としました。

class Reflection {
    
    class func typeName(of target: Any?) -> String {
        guard let target = target else { return "nil" }
        return String(describing: type(of: target))
    }
}

type(of:) は非オプショナルのみしか受け付けるないので、オプショナル対策をラッピングして増強してあげています。 これで引数を Any? にできるので何を渡しても型名を取得することができます。

使い方例

Reflection.typeName(of: 1) // Int
Reflection.typeName(of: "A") // String
Reflection.typeName(of: nil) // nil

実験

実際に型名を取得するとどのように返ってくるのでしょうか。 いろいろなパターンから試してみたいと思います。

print(Reflection.typeName(of: 1)) // Int
print(Reflection.typeName(of: "1")) // String
print(Reflection.typeName(of: 0.1)) // Double
print(Reflection.typeName(of: CGFloat(0.1))) // CGFloat

リテラルな値はこのように返ってきます。

print(Reflection.typeName(of: [1, 2, 3])) // Array<Int>
print(Reflection.typeName(of: ["A": 1, "B": 2, "C": 3])) // Dictionary<String, Int>
print(Reflection.typeName(of: (A: 1, B: 2))) // (A : Int, B : Int)
print(Reflection.typeName(of: "Hello".range(of: "l"))) // Range<Index>
print(Reflection.typeName(of: [0..<1])) // Array<CountableRange<Int>>

配列、辞書、タプル、レンジはこのように返ってきます。

print(Reflection.typeName(of: UITableViewController())) // UITableViewController
print(Reflection.typeName(of: UITableViewController.self)) // UITableViewController.Type

クラスとそのオブジェクトはこのように返ってきます。しかし、注意点として下記のようなパターンが存在します。

print(Reflection.typeName(of: NSDate(timeIntervalSince1970: 200))) // __NSTaggedDate
print(Reflection.typeName(of: NSData())) // _NSZeroData
print(Reflection.typeName(of: UIColor.red.withAlphaComponent(0.2))) // UIDeviceRGBColor

旧来の objective-c から汎用的なクラスに多く見受けられますが、その型で初期化をしても実は返ってきている型は別の継承クラスであったりします。 これらは「クラスクラスタ」と呼ばれるものです。

クラスクラスタについての詳細はここでは割愛しますが、本実装時に型名比較でなにかをするというのは危ういものになってしまいます。 あくまで型名取得はデバッグ時などの限定的な使い方をしたほうがベターではあります。

print(Reflection.typeName(of: {
    return 3
})) // () -> Int

クロージャの場合はこのように返ってきます。

print(Reflection.typeName(of: UITableViewStyle.grouped)) // UITableViewStyle

列挙子の場合はケース名ではなく列挙子名になるようですね。

print(Reflection.typeName(of: CGSize(width: 40, height: 40))) // CGSize

構造体もクラスと同じように返ります。

class A {
    class a {
        
    }
}
print(Reflection.typeName(of: A.a())) // a
print(Reflection.typeName(of: A.a.self)) // a.Type 

クラスの中に定義されたクラスの場合は、そのクラス単体だけで返ってくるようです。

プロトコル化

ここまで書いたように Reflection.typeName(of:) ですべて賄ってもいいのですが、オブジェクトのプロパティとして型名が取得できれば更に楽になります。

protocol Reflectable {}

extension Reflectable {
    
    var typeName: String {
        return Reflection.typeName(of: self)
    }
}

こいつを例えば  NSObject に準拠させてやります。

extension NSObject: Reflectable { }

すると

NSURLRequest(url: URL(string: "http://classmethod.jp")!).typeName // NSURLRequest

多くのオブジェクトに対して typeName を使用することができます。

もちろんカスタムなクラスなどについても下記のようにしてやれば、変数の中身の型名がシンプルに取得できるようになります。

class Job: Reflectable {}

class PoliceMan: Job {}

class FireMan: Job {}

let job: Job = FireMan()
print(job.typeName) // FireMan

ここでは変数jobの型はJobで宣言されていますが、実際には継承クラスのFireManオブジェクトが代入されています。 そんなときであってもtypeName"FireMan"を返してくれます。 何が入ってるかわからないときにはこれで調べることが出来ます。

今回は以上です。