타입캐스팅

wimes·2020년 3월 26일
0

Swift tutorial

목록 보기
17/17

이 글은 Swift 프로그래밍책을 읽고 요약한 내용입니다.

스위프트는 다른 프로그래밍 언어에서 대부분 지원하는 암시적 데이터 타입 변환은 지원하지 않습니다.

아래의 스위프트 코드를 보면 Int(value)라는 형태로 데이터 타입의 형태로 변경해주는데, 이니셜라이저입니다.
즉, 기존 값을 전달인자로 받는 이니셜라이저를 통해 새로운 Int 구조체의 인스턴스를 생성합니다.
스위프트에서는 이를 타입 캐스팅이라고 하지않습니다. 이니셜라이저를 통해 새로운 인스턴스를 생성하는 과정입니다.

var value: Double = 3.3
var convertedValue: Int = Int(value)
convertedValue = 5.5 // error!

스위프트 타입캐스팅

스위프트에서는 다른 언어의 타입캐스팅을 이니셜라이저로 단순화 했습니다. 그렇다면 스위프트에는 타입캐스팅이 없나요?
스위프트에도 타입캐스팅은 존재하며, 대신 조금 다른의미로 사용됩니다.

스위프트의 타입캐스팅은 인스턴스의 타입을 확인하거나 자신을 다른 타입의 인스턴스인양 행세할 수 있는 방법으로 사용할 수 있습니다.

스위프트의 타입캐스팅은 isas 로 구현했습니다.
이 두 연산자로 값의 타입을 확인하거나 다른 타입으로 전환할 수 있습니다.
또한 타입캐스팅을 통해 프로토콜을 준수하는지도 확인해볼 수 있습니다.

///Coffee 클래스와 Coffee클래스를 상속받은 Latte와 Americano  클래스
class Coffee{
    let name: String
    let shot: Int
    
    var description: String{
        return "\(shot) shot(s) \(name)"
    }
    
    init(shot: Int) {
        self.shot = shot
        self.name = "coffee"
    }
}

class Latte: Coffee{
    var flavor: String
    
    override var description: String{
        return "\(shot) shot(s) \(flavor) latte"
    }
    
    init(flavor: String, shot: Int) {
        self.flavor = flavor
        super.init(shot: shot)
    }
}

class Americcano: Coffee{
    let iced: Bool
    
    override var description: String{
        return "\(shot) shot(s) \(iced ? "iced" : "hot") americano"
    }
    
    init(shot: Int, iced: Bool) {
        self.iced = iced
        super.init(shot: shot)
    }
    
}

위의 코드를 보면 Latte와 Americano 클래스는 Coffee 클래스를 상속받았습니다.
이는 Coffee 클래스가 갖는 특성들을 Latte나 Americano가 모두 포함한다는 의미입니다.

다시말하면 Coffee는 Latte나 Americano 인 척할 수 없지만, Latte나 Americano는 Coffee인척 할 수 있다는 뜻입니다.

데이터 타입 확인

is 를 사용해서 인스턴스가 어떤 클래스(혹은 어떤 클래스의 자식클래스)의 인스턴스인지 타입을 확인해볼 수 있습니다.

인스턴스가 해당 클래스의 인스턴스거나 그 자식클래스의 인스턴스라면 true를 반환하고, 그렇지 않다면 false를 반환합니다.

let coffee: Coffee = Coffee(shot: 1)
print(coffee.description) // 1 shot(s) coffee

let myCoffee: Americcano = Americcano(shot: 2, iced: false)
print(myCoffee.description) // 2 shot(s) hot americano

let yourCoffee: Latte = Latte(flavor: "green tea", shot: 3)
print(yourCoffee.description) // 3 shot(s) green tea latte

print(coffee is Coffee) // true
print(coffee is Americcano) // false
print(coffee is Latte) // false

print(myCoffee is Coffee) // true
print(yourCoffee is Coffee) // true

print(myCoffee is Latte) // false
print(yourCoffee is Latte) // true

다운캐스팅

클래스 타입이 변수 또는 상수가 정말로 해당 클래스의 인스턴스를 참조하지 않을 수도 있습니다.
예를 들어 Latte 클래스의 인스턴스가 Coffee 클래스의 인스턴스인양 Coffee 행세를 할 수 있습니다.

let actingConstant: Coffee = Latte(flavor: "vanilla", shot: 2)
print(actingConstant.description) // 2 shot(s) vanilla latte

위의 코드를 보면 actingConstant는 Coffee 타입이지만 Coffee타입인 척하는 Latte 타입의 인스턴스를 참조하고 있습니다.
이런 상황에서 actingConstant가 참조하는 인스턴스를 진짜 타입인 Latte타입으로 사용해야 할 때가 있습니다.

가령 Latte 타입에 정의되어 있는 메서드를 사용하거나 프로퍼티에 접근해야 할 때, Latte 타입으로 변수의 타입을 변환해줘야 합니다. 이를 스위프트에서는 다운캐스팅이라고 합니다.

부모클래스의 타입을 자식클래스의 타입으로 캐스팅한다고 해서 다운캐스팅이라고 부르는 것입니다.

타입캐스트 연산자에는 as? as! 두가지가 있습니다. 타입캐스트 연산자를 사용하여 자식클래스 타입으로 다운캐스팅할 수 있습니다.
다운캐스팅을 시도해보는 조건부 연산자 as? 는 다운캐스팅이 실패했을 경우 nil 을 반환합니다.
다운캐스팅을 강제하는 연산자 as! 는 다운캐스팅에 실패할 경우 런타임 오류가 발생합니다.

if let actingOne: Americcano = coffee as? Americcano{
    print("This is Americano")
}else{
    print(coffee.description)
}
// 1 shot(s) coffee

if let actingOne: Latte = coffee as? Latte{
    print("This is Latte")
}else{
    print(coffee.description)
}
// 1 shot(s) coffee

if let actingOne: Coffee = coffee as? Coffee{
    print("This is just Coffee")
}else{
    print(coffee.description)
}
// This is just Coffee

if let actingOne: Americcano = myCoffee as? Americcano{
    print("This is Americano")
}else{
    print(coffee.description)
}
// This is Americano

if let actingOne: Latte = myCoffee as? Latte{
    print("This is Latte")
}else{
    print(coffee.description)
}
// 1 shot(s) coffee

if let actingOne: Coffee = myCoffee as? Coffee{
    print("This is just coffee")
}else{
    print(coffee.description)
}
// This is just coffee

let castedCoffee: Coffee = yourCoffee as! Coffee
let castedAmericano: Americcano = coffee as! Americcano // error!

위의 코드를 통해 as?as! 의 사용방법을 살펴볼 수 있습니다.
if let actingOne: Americano = coffee as? Americano 만 놓고 보면 "만약 coffee가 참조하는 인스턴스가 Americano타입의 인스턴스라면 actingOne이라는 임시 상수에 할당하라" 라고 해석할 수 있습니다.

let castedAmericano: Americano = coffee as! Americano 만 놓고 보면 "coffee가 참조하는 인스턴스를 Americano타입으로 강제 변환하여 castedAmericano상수에 할당하라 뒷일은 책임지지 않는다."로 해석할 수 있습니다.

컴파일러가 다운캐스팅을 확신할 수 있는 경우에는 as?as! 대신 as 를 사용할 수도 있습니다. 항상 성공하는 것을 아는 경우는 캐스팅하려는 타입이 같은 타입이거나 부모클래스 타입이라는 것을 알 때 입니다.

/// 항상 성공하는 다운캐스팅
let castedCoffee: Coffee = yourCoffee as Coffee

타입캐스팅의 의미

캐스팅은 실제로 인스턴스를 수정하거나 값을 변경하는 작업이 아닙니다. 인스턴스는 메모리에는 똑같이 남아있을 뿐입니다.
다만 인스턴스를 사용할 때 어떤 타입으로 다루고 어떤 타입으로 접근해야 할지 판단할 수 있도록 컴퓨터에 힌트를 주는 것입니다.

Any, AnyObject의 타입캐스팅

Any 는 함수타입을 포함한 모든타입을 뜻하고, AnyObject 는 클래스 타입만을 뜻합니다.

반환되는 타입이 AnyAnyObject 라면 전달받는 데이터가 어떤 타입인지 확인하고 사용해야 합니다.
스위프트는 암시적 타입 변환을 허용하지 않으며, 타입에 굉장히 엄격하기 때문입니다.

아래의 코드는 item이 어떤 타입인지 판단하는 동시에 실질적으로 해당 타입의 인스턴스로 사용할 수 있도록 캐스팅하는 코드입니다.

func castTypeToAppropriate(item: AnyObject){
    if let castedItem: Latte = item as? Latte{
        print(castedItem.description)
    }else if let castedItem: Americcano = item as? Americcano{
        print(castedItem.description)
    }else if let castedItem: Coffee = item as? Coffee{
        print(castedItem.description)
    }else{
        print("Inknown Type")
    }
}

castTypeToAppropriate(item: coffee) // 1 shot(s) coffee
castTypeToAppropriate(item: myCoffee) // 2 shot(s) hot americano
castTypeToAppropriate(item: yourCoffee) // 3 shot(s) green tea latte
castTypeToAppropriate(item: actingConstant) // 2 shot(s) vanilla latte

Any 는 함수, 구조체, 클래스, 열거형 등 모든 타입의 인스턴스를 의미할 수 있습니다.

func checkAnyType(of item: Any){
    switch item {
    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 latte as Latte:
        print(latte.description)
    case let stringConverter as (String) -> String:
        print(stringConverter("wimes"))
    default:
        print("something else: \(type(of: item))")
    }
}

checkAnyType(of: 0) // zero as an Int
checkAnyType(of: 0.0) // zero as a Double
checkAnyType(of: 42) // an integer value of 42
checkAnyType(of: 3.14159) // a positive double value of 3.14159
checkAnyType(of: -0.25) // some other double value that I don't want to print
checkAnyType(of: "hello") // a string value of "hello"
checkAnyType(of: (3.0, 5.0)) // an (x, y) point at 3.0, 5.0
checkAnyType(of: yourCoffee) // 3 shot(s) green tea latte
checkAnyType(of: coffee) // something else: Coffee
checkAnyType(of: { (name: String) -> String in
    "Hello, \(name)"
}) // Hello, wimes

옵셔널과 Any

Any타입은 모든 값 타입을 표현합니다. 더불어 옵셔널 타입도 표현할 수 있습니다. 그런데도 Any타입의 값이 들어와야 할 자리에 옵셔널 타입의 값이 위치한다면 스위프트 컴파일러는 경고를 합니다.

의도적으로 옵셔널 값을 Any타입의 값으로 사용하고자 한다면 as 연산자를 사용하여 명시적 타입 캐스팅을 해주면 경고메시지를 받지 않습니다.

let optionalValue: Int? = 100
print(optionalValue) // warning
print(optionalValue as Any) // 경고 없음
profile
iOS/Swift & Node.js Dev.

0개의 댓글