본문 바로가기
Swift/Language Guide

Type Casting - #1/1

by diosmio 2021. 9. 10.

서문

Swift에서는 "타입 캐스팅"이라고 표현하는 행위가 다른 언어들과 사뭇 차이가 있음에 유의하자

 

C언어에서 "타입 캐스팅"이라고 하면 String -> Int 처럼 원본을 유지하고 타입만 바꾸는걸 뜻하지만

 

Swift에서 String -> Int를 하면 Int 타입의 새 인스턴스를 생성함으로써 이루어지므로

이 행위를 타입 캐스팅이라고 부르지 않는다

 

여기선 "타입 캐스팅"이라고 하면 상속과 프로토콜 채택을 떠올려야 한다

 

[ Swift에서의 타입 캐스팅 ]

is : 인스턴스의 타입을 확인 (예시: 해당 인스턴스가 Double 타입이 맞나?)

● as : 인스턴스의 타입을 superclass 혹은 subclass 타입으로 변경

● 특정 프로토콜을 따르는지 여부 확인

 

 

 

Defining a Class Hierarchy for Type Casting

타입 캐스팅이 사용되는 예시를 통해 이해해보자

 

상속 관계에 있는 3개의 class가 있고, Movie와 Song은 형제 관계이다

 

서로 다른 타입인 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)
    }
}

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 배열의 타입은 도대체 무엇일까. Moive? / Song?

-> 공동의 부모인 MediaItem 타입의 배열로 추론된다

 

Q. 공동의 부모로 추론한다면, 공동의 부모가 여러개라던가 상속관계가 복잡하면 어떻게 하느냐?

A. 다행(?)인지 Swift에선 1개 class만 상속할 수 있다

 

 

● library의 요소를 루프로 하나씩 꺼내면 각 요소들의 타입은 Movie/Song이 아닌 MediaItem이므로

1. 타입을 확인하고

2. 본래 타입으로 downcast 해야한다

 

 

 

Checking Type

● 상속 관계가 있는 타입의 instance는 부모 타입 중 하나로 upcast될 수 있다.

is 연산자를 사용하면 instance 본질 타입에 대해 알 수 있다

 

var movieCount = 0
var songCount = 0

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

print("Media library contains \(movieCount) movies and \(songCount) songs")
// Prints "Media library contains 2 movies and 3 songs"

 

● 본질이 Movie 타입인 인스턴스가 MediaItem으로 upcast된 상태이지만

is 연산자를 통해 본질이 Movie임을 알 수 있었다

 

그리고, is 연산자는 본질 타입과의 "일치"여부가 아닌 "포함"여부를 확인하는 것이다

만약 본질 타입이 Movie의 자식이어도 Movie를 포함하므로 true를 반환한다

 

 

Downcasting

위에서 언급한 것 처럼 상속관계에 있는 타입의 인스턴스는 부모 타입 중 하나로 upcast될 수 있는데

이를 원래 타입으로 되돌리거나 또 다른 부모 타입 중 하나로 바꾸는 방법이 있다

 

as 연산자를 사용하면 된다.

본질 타입이 바꾸려는 타입의 자식이 아니면, 캐스팅이 실패할 수 있기 때문에

Optional을 반환하는 as? / as! 를 사용한다

(as? 와 as! 의 용도차이는 nil이 아님이 보장될 때만 as! 를 사용해야 한다)

 

for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("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

 

● upcasting이든 downcasting이든 인스턴스에 대한 access를 바꾸는 것이지

인스턴스 본질을 수정하거나 바꾸지 않는다

 

 

 

 

Type Casting for Any and AnyObject

Swift에는 Any라는 타입이 있다

이름 그대로 아무거나 저장할 수 있다

 

● 클래스 인스턴스면 AnyObject / 그 외에는 전부 Any

 

뭔가 Swift스럽지 않고 과하게 편하다는 느낌이 나지 않는가?

맞다. 적절한 타입을 지정하는게 무조건 더 좋고, 꼭 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"))
things.append({ (name: String) -> String in "Hello, \(name)" })

Any를 사용하면 다양한 타입을 하나의 배열로 만들 수도 있다

(심지어 클로저도)

 

 

● Any타입 배열의 요소를 꺼내려면 어떻게 해야 할까?

-> switch문과 is / as 연산자를 사용할 수 있다

 

 

for thing in things {
    switch thing {
    case 0 as Int:
        print("zero as an Int")
    case 0 as Double:
        print("zero as a Double")
    case let someInt as Int:
        print("an integer value of \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("a positive double value of \(someDouble)")
    case is Double:
        print("some other double value that I don't want to print")
    case let someString as String:
        print("a string value of \"\(someString)\"")
    case let (x, y) as (Double, Double):
        print("an (x, y) point at \(x), \(y)")
    case let movie as Movie:
        print("a movie called \(movie.name), dir. \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Michael"))
    default:
        print("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
// Hello, Michael

● 이런 구현이 가능한 이유는 Any/AnyObject는 모든 타입/객체타입의 부모 타입이기 때문이다

 

"case 0 as Int" : 요소가 Int로 downcast 후 값이 0인지

"case let someInt as Int" : Int로 downcast 되는지

 

 

● 반대로, Any/AnyObject로 upcast도 가능하다

 

심지어 Optional도 가능하다.

let optionalNumber: Int? = 3
things.append(optionalNumber)        // Warning
things.append(optionalNumber as Any) // No warning

warning이 뜨는 이유는 아마도...

Any에 Optional을 넣는게 불가능한건 아니지만,

구현자가 생각하기에 Any의 본질적인 의도와는 다르다고 판단한듯

'Swift > Language Guide' 카테고리의 다른 글

Control Flow - #1/2  (0) 2021.09.12
Functions - #1/1  (0) 2021.09.12
Collection Types - #2/2 : Set, Dictionary  (0) 2021.09.10
Collection Types - #1/2 : Array  (0) 2021.09.09
Strings and Characters - #2/2  (0) 2021.09.06

댓글