【Swift】Appleの新言語「Swift」のリファレンスを読む(14) - Type Casting、Nested Types

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Type Casting(キャスト)

あんまり面白い話でもないのでささっと終わらせましょう。

Swiftではis演算子を使ってインスタンスの型をチェックしたり、as演算子を使ってキャストすることが出来ます。

Defining a Class Hierarchy for Type Casting

まず基底クラスを作成します。

class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

MediaItemを継承したクラスを二つ作ります。

class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

この二つのクラスをまぜこぜにしたArrayを作ってみます。

let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
    Movie(name: "Citizen Kane", director: "Orson Welles"),
    Song(name: "The One And Only", artist: "Chesney Hawkes"),
    Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// the type of "library" is inferred to be MediaItem[]

当然libraryの型はArray<MediaItem>になりますね、と言うお話。

Checking Type

libraryをfor-in構文でぐるぐる回すとMediaItemのインスタンスがもらえるんですが、それぞれ実体はMovieだったりSongだったりします。型によって処理を変えたい場合はis演算子を使って判断します。

var movieCount = 0
var songCount = 0

for item in library {
    if item is Movie {
        ++movieCount
    } else if item is Song {
        ++songCount
    }
}

println("Media library contains \(movieCount) movies and \(songCount) songs")
// prints "Media library contains 2 movies and 3 songs"

Downcasting

親クラスのインスタンスを派生クラスとして扱うにはas演算子を使用します。

が、クラスのキャストと言うものはいつだって危険に満ち溢れています。かと言っていちいちisで判断するのもだるいので、なるべくas?演算子を使いましょう。

for item in library {
    if let movie = item as? Movie {
        println("Movie: '\(movie.name)', dir. \(movie.director)")
    } else if let song = item as? Song {
        println("Song: '\(song.name)', by \(song.artist)")
    }
}

// Movie: 'Casablanca', dir. Michael Curtiz
// Song: 'Blue Suede Shoes', by Elvis Presley
// Movie: 'Citizen Kane', dir. Orson Welles
// Song: 'The One And Only', by Chesney Hawkes
// Song: 'Never Gonna Give You Up', by Rick Astley

Type Casting for Any and AnyObject

Swiftでは型システムに対し中指を立てるための型が二つあります。以下の二つです。

  • AnyObject
  • Any

AnyObjectは全てのクラスに対する基底クラスのようなものです。Anyは更にその上で、あらゆる型はAnyで表現出来ます。(クロージャやファンクションの型もAnyで表現出来る。)

まずはAnyObjectの説明から。AnyObjectのArrayを作成すると、どんなクラスでも格納可能と言う旧世紀の遺物のようなArrayを作ることが出来ます。

let someObjects: AnyObject[] = [
    Movie(name: "2001: A Space Odyssey", director: "Stanley Kubrick"),
    Movie(name: "Moon", director: "Duncan Jones"),
    Movie(name: "Alien", director: "Ridley Scott")
]


for object in someObjects {
    let movie = object as Movie
    println("Movie: '\(movie.name)', dir. \(movie.director)")
}
// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones
// Movie: 'Alien', dir. Ridley Scott

for-in構文で使用するArrayはas演算子で事前にダウンキャスト可能です。

for movie in someObjects as Movie[] {
    println("Movie: '\(movie.name)', dir. \(movie.director)")
}

// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones
// Movie: 'Alien', dir. Ridley Scott

次はAnyです。これはもう見てもらえばわかると思います。

var things = Any[]()

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))

for thing in things {
    switch thing {
    case 0 as Int:
        println("zero as an Int")
    case 0 as Double:
        println("zero as a Double")
    case let someInt as Int:
        println("an integer value of \(someInt)")
    case let someDouble as Double where someDouble > 0:
        println("a positive double value of \(someDouble)")
    case is Double:
        println("some other double value that I don't want to print")
    case let someString as String:
        println("a string value of \"\(someString)\"")
    case let (x, y) as (Double, Double):
        println("an (x, y) point at \(x), \(y)")
    case let movie as Movie:
        println("a movie called '\(movie.name)', dir. \(movie.director)")
    default:
        println("something else")
    }
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called 'Ghostbusters', dir. Ivan Reitman

Anyの話はどうでもいいです。switchとas演算子を組み合わせることでパターンマッチをキメられることの方が大事です。何でswitchで説明しなかったのか謎です。

ifではas?の方を使いましたが、switchではasでそのまま判定出来ます。

Nested Types(型のネスト)

Swiftではある型の中に別の型を宣言することが出来ます。逆にオブジェクト指向パラダイムを持ちながらその手のことが出来ない言語ってあるんでしょうか。

Nested Types in Action

なんだか若干複雑なサンプルが載っています。

struct BlackjackCard {

    // nested Suit enumeration
    enum Suit: Character {
        case Spades = "?", Hearts = "?", Diamonds = "?", Clubs = "?"
    }

    // nested Rank enumeration
    enum Rank: Int {
        case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
        case Jack, Queen, King, Ace
        
        struct Values {
            let first: Int, second: Int?
        }
        
        var values: Values {
            switch self {
            case .Ace:
                return Values(first: 1, second: 11)
            case .Jack, .Queen, .King:
                return Values(first: 10, second: nil)
            default:
                return Values(first: self.toRaw(), second: nil)
            }
        }
    }
    
    // BlackjackCard properties and methods
    let rank: Rank, suit: Suit
    var description: String {
        var output = "suit is \(suit.toRaw()),"
        output += " value is \(rank.values.first)"
        
        if let second = rank.values.second {
            output += " or \(second)"
        }
        
        return output
    }
}

型のネストにだけ着目すると、こんな構造です。

BlackjackCard
├Suit
└Rank
 └Values

正直「こんなことが出来る」ってだけで十分な気がしますが、それぞれネストしている型が何をやっているのか細かく見てみましょう。

// nested Suit enumeration
enum Suit: Character {
    case Spades = "?", Hearts = "?", Diamonds = "?", Clubs = "?"
}

SuitはCharacterを継承したenumです。メンバとしてトランプの模様を持っています。

// nested Rank enumeration
enum Rank: Int {
    case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
    case Jack, Queen, King, Ace
    
    struct Values {
        let first: Int, second: Int?
    }
    
    var values: Values {
        switch self {
        case .Ace:
            return Values(first: 1, second: 11)
        case .Jack, .Queen, .King:
            return Values(first: 10, second: nil)
        default:
            return Values(first: self.toRaw(), second: nil)
        }
    }
}

Rankはちょっと複雑です。TwoからTenまではそれぞれの単語に対応した2〜10の値が入っています。Intのenumはこう言う書き方が許されています。(参考)

Jack, Queen, King, Aceに関してはちょっと特別なルールがあります。と言ってもブラックジャックのルールなんですがね。それを吸収するためにValuesと言う構造体とvaluesと言うread onlyなプロパティが用意されています。

struct Values {
    let first: Int, second: Int?
}

var values: Values {
    switch self {
    case .Ace:
        return Values(first: 1, second: 11)
    case .Jack, .Queen, .King:
        return Values(first: 10, second: nil)
    default:
        return Values(first: self.toRaw(), second: nil)
    }
}

valuesのgetの中を見ていきましょう。ここでのselfはRankです。自分自身がRankのどのメンバかで処理を分岐させたいのでしょう。

ブラックジャックではAceの値を1か11か選ぶことが出来ます。だから自分自身がAceだったらfirstが1、secondが11のValuesを返します。

また、Jack, Queen, Kingのような絵札は一律10として扱います。それ以外(TwoからTenまで)はそのままの数字です。これらは別の値を選ぶことが出来ないので、secondがnilなValuesを返します。

これらを踏まえた上で、BlackjackCardのプロパティを見てみましょう。そんなに難しくないはずです。

// BlackjackCard properties and methods
let rank: Rank, suit: Suit
var description: String {
    var output = "suit is \(suit.toRaw()),"
    output += " value is \(rank.values.first)"
    
    if let second = rank.values.second {
        output += " or \(second)"
    }
    
    return output
}

イニシャライザでrankとsuitに値を渡し、descriptionを呼び出すとこんな感じになります。

let theAceOfSpades = BlackjackCard(rank: .Ace, suit: .Spades)
println("theAceOfSpades: \(theAceOfSpades.description)")
// prints "theAceOfSpades: suit is ?, value is 1 or 11"

Referring to Nested Types

ネストしている型でも「.」でつなげれば普通に呼び出せます。

let heartsSymbol = BlackjackCard.Suit.Hearts.toRaw()
// heartsSymbol is "?"