Swift

[Swift] is 攻略ガイド(とりわけメタタイプとの絡みについて)

Swiftのis、特にメタタイプが絡んだときの挙動について整理しておきたかったんです。

TL;DR

インスタンス is 型名.Type で、「左辺のインスタンスが右辺のメタタイプとみなせるかどうか」を判定する式になる。

以上のことさえ知れれば、ひとまずは十分だと考えます。
以降、理論的背景の解説など一切なく、個別の現象を書き並べただけの記事のためご注意ください。
パターンマッチ: Type-Casting Patternsにおけるisに関する説明も本記事では割愛いたします。

環境

  • Xcode 9.2
  • Swift 4.0.3

isの基本とその構文

  • インスタンス(左辺)が、特定の型(右辺)、またはそのサブクラスとみなせるかを判定し、 Boolの値を返す。
  • プログラムの静的な解析から、常にtrue、あるいはfalseを返すことが明らかな場合には、コンパイル時にエラーになることがある。1
  • 右辺に 型名.Type と書くと、 「左辺に置いたインスタンスが、右辺の型のメタタイプとみなせるか」を判定する式になる。
構文
   cat                      is Animal
// [インスタンス or メタタイプ]    [型名 or 型名.Type(メタタイプの判定に用いる記法)]

上の解釈に照らし合わせると、isの記法は4つのパターンに区分できます。

左辺 \ 右辺 型名 型名.Type
インスタンス パターン1 パターン2
メタタイプ パターン3 パターン4

以下、上記テーブルの各象限について見ていきます。

インスタンス × 型名 (パターン1)

「左辺のインスタンスが、右辺の型・もしくはそのサブタイプとみなせるかを判定する。」

例えば、以下の実行結果は自然です。

class Animal {}

let animal = Animal()
if animal is Animal {
    print("animalは、Animal型のインスタンスです。")
}

// 結果
// animalは、Animal型のインスタンスです。

インスタンス × 型名.Type (パターン2)

「左辺のインスタンスが、右辺の型のメタタイプとみなせるかを判定する。」

例えば、以下のanimalは当然メタタイプではないので、何も出力されません。

class Animal {}

let animal = Animal()
if animal is Animal.Type {
    print("animalは、Animal型のメタタイプです。")
}

// 結果
// 何も出力されない

メタタイプ × 型名 (パターン3)

「左辺のメタタイプが、右辺の型・もしくはそのサブタイプとみなせるかを判定する。」

例えば、以下を実行すると、何も出力されません。
メタタイプは、その型のインスタンスとはみなされないようです。

class Animal {}

let metaAnimal: Animal.Type = Animal.self  // メタタイプを取得
if metaAnimal is Animal {
    print("metaAnimalは、Animal型のインスタンスです。")
}

// 結果
// 何も出力されない

メタタイプ × 型名.Type (パターン4)

「左辺のメタタイプが、右辺の型のメタタイプとみなせるかを判定する。」
例えば、以下の実行結果も自然だと思います。

class Animal {}

let metaAnimal: Animal.Type = Animal.self  // メタタイプを取得
if metaAnimal is Animal.Type {
    print("metaAnimalは、Animal型のメタタイプです。")
}

// 結果
// metaAnimalは、Animal型のメタタイプです。

特殊なケース・注意点

前章までの内容で、大方のケースにおいて自信を持ってisを使えるようになりました。
以下、isを使用していく中で遭遇した、直感に反するような挙動や注意点について列挙します。

Protocolの場合

isの右辺には、プロトコル名も置くことができます。
その場合、左辺が右辺のプロトコルに適合したインスタンスの場合にtrueが返る、という挙動になります。

protocol Hoge         {}
class HogeClass: Hoge {}

let hogeClass = HogeClass()
print(hogeClass is Hoge)

// 結果
true

さらに、プロトコルにもメタタイプは存在し、
クラスや構造体・列挙体と同様、以下の記法で取得できます。
(その定義にSelf もしくは 連想型(associatedtype)を含まないプロトコルのみ取得が可能です。)

protocol Hoge {}
let metaHoge: Hoge.Protocol = Hoge.self  // プロトコル `Hoge` のメタタイプを取得

しかし、このように取得したメタタイプに対し、前述したテーブルのパターン4にあたる演算を行うと、
直感に反する結果が返ります。

print(metaHoge is Hoge.Type)

// 結果
false

この挙動は非常に不可解な印象を覚えます。

また注意点として、Self もしくは 連想型(associatedtype)を含むプロトコルは、isの右辺になることができません。

protocol Base {
    associatedtype Element
}

print(42 is Base)

// 結果: コンパイルエラー
Protocol 'Base' can only be used as a generic constraint because it has Self or associated type requirements

print(42 is Equatable)

// 結果: コンパイルエラー ∵ `Equatable`は `==`の定義に`Self`を含むので、`is`の右辺で使えない。地味に不便やもしれない
Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements

サブクラスが絡む場合

「あるクラスA(Animal)のサブクラス(Dog)のメタタイプ」は、
「あるクラスA(Animal)のメタタイプ」とみなされます。
これは、通常のスーパークラス-サブクラスの関係を鑑みれば、自然に感じられます。

class Animal      {}
class Dog: Animal {}

let metaDog = Dog.self
print(metaDog is Animal.Type)

// 結果
true  // サブクラスのメタタイプは、スーパークラスのメタタイプとみなされる

同様の理由で、こちらの挙動も、素直なものに感じます。

let metaAnimal = Animal.self
print(metaAnimal is Dog.Type)

// 結果
false  // スーパークラスのメタタイプは、サブクラスのメタタイプとはみなされない

ジェネリクス型をisの右辺に使う場合の注意点

以下のような、型パラメータ<T>を持つジェネリクス型があったとします。

class Container<T> {
    let element: T
    init(element: T) {
        self.element = element
    }
}

この型がクラスの場合、型パラメータ<T>を特殊化することなく、isの右辺に使うことができます。

let container = Container(element: 42)
print(container is Container)

// 結果
true

しかし、この型がクラスではなく構造体(または列挙体)だった場合は、
上記で可能だった記法がなぜかコンパイルエラーになってしまいます。

// クラスから構造体に変更
struct Container<T> {
    let element: T
    // 全項目イニシャライザがあるためこの`init`は本来不要だが、上記のクラス定義と対称性を保つためにあえて明示しています
    init(element: T) {
        self.element = element
    }
}

let container = Container(element: 42)
print(container is Container)

// 結果: コンパイルエラー
Cast from 'Container<Int>' to unrelated type 'Container<_>' always fails

このように、型パラメータ<T>を明示してやれば、Containerが構造体であったとしてもisの右辺で使えるようになります。

let container = Container(element: 42)

print(container is Container<Int>)     // true
print(container is Container<String>)  // false
print(container is Container<Any>)     // false (これは直感に反するけどな)

標準ライブラリにあるArray<Element>が、型パラメータ<Element>を特殊化することなくisの右辺に使えないのも、
Arrayが構造体だからでしょう。
(とはいえ、構造体だとなぜダメなのか、の理由についてまでは理解が進んでいないのですが)

let array = [1,2,3]
print(array is Array)

// 結果: コンパイルエラー
Cast from '[Int]' to unrelated type 'Array<_>' always fails

この場合も、このようにすれば使えます。

let array = [1,2,3]

print(array is Array<Int>)     // true
print(array is Array<String>)  // false
print(array is Array<Any>)     // true(でもってこっちはtrueかよ...`Array`だけの特別措置なのかな)

その他

このような書き方も許容されるようです。

// リテラル`[]`だけでは、本来Arrayの型パラメータ`<Element>`は決定しないはずだが...
print([] is Int)        // false
print([] is [Int])      // true
print([] is Any)        // true
print([] is [Any])      // true

おわりに

型に関する理解が深まり次第、随時追記いたします。
(特にジェネリクス型やオプショナル型が絡んだときの挙動は、今後の研究対象とさせてください)

links

Swift の変数で型を扱う
(型名.Typeの意味がわからず悩んでいたところ、一気に疑問が氷解しました)


  1. 詳解Swift 第3版 P175 より引用。たとえば Int.self is Int? というコードは当該環境でコンパイルエラーになることを確認しました。