본문 바로가기
Swift 공식 가이드/Swift 3

Type Casting

by 토끼찌짐 2017. 3. 23.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Type Casting 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



들어가며

타입 캐스팅Type casting은 인스턴스의 타입을 체크하거나, 인스턴스의 타입을 자기 클래스의 슈퍼클래스 타입이나 서브클래스 타입처럼 다루기 위해 쓰인다.

Swift에서는 isas 연산자를 사용하여 값의 타입을 체크하거나 다른 타입으로 변환하는 방식으로 타입 캐스팅이 이루어진다.

타입 캐스팅은 특정 타입이 프로토콜을 따르고 있는지 체크할 때도 쓰인다. (Checking for Protocol Conformance 에서 자세히 다룹니다)



타입캐스팅을 위한 Class Hierarchy 정의

타입 캐스팅을 통하여 특정 클래스 인스턴스의 타입을 체크하고, 그 타입을 같은 클래스 계층 내의 슈퍼/서브 클래스 타입으로 캐스팅할 수 있다.

예제를 통해 알아보자. MediaItem이라는 베이스 클래스와 그것을 상속받은 서브 클래스 Movie와 Song이 있다.

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

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)
    }
}

그리고 library라는 이름의 배열이 있다. 여기에는 Movie 인스턴스 2개와 Song 인스턴스 3개가 들어있다. 그렇다면 Swift의 타입체커type checker는 이 배열을 어떤 타입으로 추정하게 될까?

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")
]

타입체커는 Movie와 Song의 공통 상위클래스가 MediaItem이라는 것을 통하여 이 배열을 [MediaItem] 타입으로 추정한다. 따라서 배열에 들어있는 아이템 각각은 Movie와 Song 인스턴스임에도 불구하고, 배열을 순회하며 꺼내는 아이템들의 타입은 MediaItem이 될 것이다. 이것들을 그들 원래의 타입native type (ex: Movie, Song 타입)으로서 처리하기 위해서는 타입을 체크하거나 다운캐스팅을 해야한다. 타입 체크는 is로, 타입 캐스팅은 as로 할 수 있다. 아래에서 자세히 살펴보자.



Checking Type

타입 체크 연산자 is 는 인스턴스가 특정 서브클래스 타입인지 체크할 때 사용한다. 

예제를 보자. library 배열에 들어있는 Movie와 Song 인스턴스 각각의 개수를 세는 코드이다.

var movieCount = 0
var songCount = 0

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

// movieCount is 2
// songCount is 3



Downcasting

타입 캐스트 연산자 as? 나 as!는 인스턴스를 특정 서브클래스 타입으로 다운캐스팅할 때 사용한다.

  • as? : conditional form. 다운캐스팅 할 타입의 optional value를 리턴

  • as! : forced form. 다운캐스팅할 타입의 optional value를 강제 언랩핑하여 리턴

  • 다운캐스팅이 항상 성공한다고 확신할 수 있을 때만 as!를 사용할 것 (실패 시 런타임 에러)


예제를 보자. libary 배열을 돌면서 아이템의 타입이 Movie면 감독을, Song이면 아티스트를 출력해주는 코드다. 예제의 경우 아이템의 타입이 Movie인지 Song인지를 미리 확신할 수 없기 때문에 as? 연산자를 사용하고 있다.

for item in library {
    if let movie = item as? Movie {
        // item을 Movie타입으로 접근할 수 있는지 보고, 만약 그렇다면
        // movie라는 임시 상수에
        // 옵셔널 Movie 타입의 Value를 set
        print("Movie : \(movie.name) by \(movie.director)")
    } else if let song = item as? Song {
        print("Song : \(song.name) by \(song.artist)")
    }
}


<note> 캐스팅은 실제로 인스턴스를 변경하거나 값을 바꾸는 것이 아니다. 원래의 인스턴스는 그대로 두고, 단순히 해당 인스턴스를 다른 타입으로 취급하여 접근하는 것 뿐이다.



Type Casting for Any and AnyObject

타입에 상관없이 동작하는 코드를 위한 2가지 특별한 type을 소개한다.

  • AnyObject : 모든 클래스 타입의 인스턴스를 AnyObject로 표현 가능

  • Any : 모든 타입의 인스턴스를 Any로 표현 가능 (fuction 타입 포함)


<note> Any, AnyObject는 필요한 경우에만 사용하라. 특정한 타입을 가지고 동작하는 코드가 언제나 더 낫다.



<AnyObject>

Cocoa API를 사용할 때 가끔 배열을 [AnyObject] 타입으로 받게된다. Objective-C 과거 버전에서 명시적인 특정 타입을 가진 배열을 제공하지 않기 때문이다. (최근 버전에서는 제공한다)

이런 상황에서 만약 배열에 들어있는 타입이 무엇일지 확신할 수 있다면 as! 연산자를 통해 다운캐스팅+강제언랩핑 할 수 있다. 

// 항상 Movie 클래스의 인스턴스만 들어있다고 확신할 수 있는 배열
let someObjects: [AnyObject] = [
    Movie(name: "스페이스 오디세이", director: "감독S"),
    Movie(name: "달", director: "감독D"),
    Movie(name: "에일리언", director: "감독R")
]

for object in someObjects {
    // 다운캐스팅+강제언랩핑
    let movie = object as! Movie
    print("영화 \(movie.name)의 감독은 \(movie.director)")
}

// 배열 자체를 타입캐스팅할 수도 있다
for movie in someObjects as! [Movie] {
    print("영화 \(movie.name)의 감독은 \(movie.director)")
}



<Any>

여러 다른 타입들을 가지고 동작하는 코드에서 Any 타입을 사용할 수 있다. 예제를 통해 살펴보자.

var things = [Any]()

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.insert((3.0, 5.0), atIndex: 5)
things.append(Movie(name: "미드나잇 인 파리", director: "우디 앨런"))
things.append({ (name: String) -> String in "Hello, \(name)" })

// switch문에서 is, as 연산자를 사용하여 다음과 같이 할 수 있다
for thing in things {
    switch thing {
    case 0 as Int:
        print("Int 0")
    case 0 as Double:
        print("Double 0.0")
    case let someInt as Int:
        print("Int \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("0보다 큰 Double \(someDouble)")
    case is Double:
        print("0보다 작은 Double에 해당할 것이다")
    case let someString as String:
        print("String \(someString)")
    case let (x, y) as (Double, Double):
        print("(x, y) 좌표 (\(x), \(y)")
    case let movie as Movie:
        print("영화 \(movie.name)")
    case let stringConverter as String -> String:
        print(stringConverter("철수"))
    default:
        print("그 외")
    }
}


// 출력결과
// Int 0
// Double 0.0
// Int 42
// 0보다 큰 Double 3.14159
// String hello
// (x, y) 좌표 (3.0, 5.0)
// 영화 미드나잇 인 파리
// Hello, 철수

// <note> Any는 optional type도 포함한다.
// 그러나 optional value를 Any value로서 사용할 때는 warning이 난다.
// 이럴 때는 아래처럼 as 연산자를 통해 명시적으로 casting을 하자.
let optionalNumber: Int? = 3
things.append(optionalNumber)        // Warning
things.append(optionalNumber as Any) // No warning


'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Extensions  (1) 2017.03.23
Nested Types  (0) 2017.03.23
Error Handling  (0) 2017.03.18
Optional Chaining  (0) 2017.03.18
Automatic Reference Counting (ARC)  (1) 2017.03.14