개발하는 뚝딱이

[swift] 타입캐스팅 본문

swift

[swift] 타입캐스팅

개발자뚝딱이 2020. 5. 17. 16:20

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 


 

 

타입캐스팅

- 스위프트는 데이터 타입 안전을 위해 다른 타입끼리의 값 교환을 엄격히 제한

- 다른 언어에서 제공하는 암시적 데이터 타입 변환 (Implicit Type Conversion)은 지원하지 않음

 

 

 

1. 스위프트 타입캐스팅

- 타입캐스팅을 이니셜라이저로 단순화함

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

- is와 as 연산자로 구현됨

- is와 as 연산자로 값의 타입을 확인하거나 다른 타입으로 전환 가능

- 타입캐스팅을 통해 프로토콜을 준수하는지 확인 가능

// Coffee 클래스와 Coffee 클래스를 상속받은 Latte와 Americano 클래스
// Latte나 Americano는 Coffee인 척 할 수 있음

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

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

let myCoffee: Americano = Americano(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

 

 

 

2. 데이터 타입 확인

2.0 is 연산자

- 타입 확인 연산자 is를 사용하여 어떤 클래스 (혹은 어떤 클래스의 자식클래스)의 인스턴스인지 타입 확인 할 수 있음

- 타입 확인 연산자는 인스턴스가 해당 클래스의 인스턴스거나 그 자식클래스의 인스턴스라면 true 반환, 아니면 false 반환

- is 연산자는 클래스의 인스턴스뿐만 아니라 모든 데이터 타입에서도 사용 가능

print(coffee is Coffee) // true
print(coffee is Americano) // 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

 

2.1 메타 타입(Meta Type) 타입 

- 메타 타입 타입은 타입의 타입을 뜻함

- 클래스 타입, 구조체 타입, 열거형 타입, 프로토콜 타입 등의 타입의 타입

- 즉, 타입 자체가 하나의 타입으로 표현됨

 

- 클래스, 구조체, 열거형의 이름은 타입의 이름이며, 그 이름 뒤에 .Type을 붙이면 메타 타입을 나타냄

- 프로토콜의 경우 .Protocol를 붙여서 메타 타입을 나타냄

 

- self를 사용해 타입을 값처럼 표현 가능

- 예를 들어, SomeClass.self라고 표현하면 SomeClass의 인스턴스가 아니라 SomeClass 타입의 값으로 표현한 값을 반환

- SomeProtocol.self라고 표현하면 SomeProtocol을 준수하는 타입의 인스턴스가 아니라 SomeProtocol 프로토콜을 값으로 표현한 값을 반환

protocol SomeProtocol {  }
class SomeClass: SomeProtocol {  }

let intType: Int.Type = Int.self
let stringType: String.Type = String.self
let classType: SomeClass.Type = SomeClass.self
let protocolProtocol: SomeProtocol.Protocol = SomeProtocol.self

var someType: Any.Type

someType = intType
print(someType) // Int

someType = stringType
print(someType) // String

someType = classType
print(someType) // SomeClass

someType = protocolProtocol
print(someType) // SomeProtocol

 

2.2 type(of:) 함수

- 프로그램 실행 중에 인스턴스의 타입을 표현한 값을 알아볼 때 사용

- type(of: someInstance).self라고 표현하며 someInstance의 타입을 값으로 표현한 값을 반환

 

* 인스턴스 self와 타입 self의 의미

값.self : 그 값 자신을 의미

타입이름.self : 타입을 표현하는 값을 반환

print(type(of: coffee) == Coffee.self) // true
print(type(of: coffee) == Americano.self) // false
print(type(of: coffee) == Latte.self) // false

print(type(of: coffee) == Americano.self) // false
print(type(of: myCoffee) == Americano.self) // true
print(type(of: yourCoffee) == Americano.self) // false

print(type(of: coffee) == Latte.self) // false
print(type(of: myCoffee) == Latte.self) // false
print(type(of: yourCoffee) == Latte.self) // true

 

 

3. 다운캐스팅

- 어떤 클래스 타입의 변수 또는 상수가 해당 클래스의 인스턴스를 참조하지 않을 수도 있음

- 예를 들어 Latte 클래스의 인스턴스가 Coffee 클래스의 인스턴스인양 Coffee 행세를 할 수 있음

 

- actingConstant 상수는 Coffee  인스턴스를 참조하도록 선언했지만 실제로는 Coffee 타입인 척 하는 Latte 타입의 인스턴스를 참조하고 있음

- acintConstant가 참조하는 인스턴스를 진짜 타입인 Latte 타입으로 사용해야 할 때가 있음

- Latte 타입에 정의되어 있는 메서드를 사용하거나 프로퍼티에 접근해야 할 때 Latte 타입으로 변수의 타입을 변환해줘야 함

- 이것을 다운캐스팅 (Down Casting)이라고 함

- 부모클래스의 타입을 자식클래스의 타입으로 캐스팅

- 다운캐스팅은 클래스 뿐 아니라, Any 타입에서 다른 타입으로 캐스팅할 때도 사용됨

// Latte 타입의 인스턴스를 참조하는 Coffee 타입과 actingConstant 상수
let actingConstant: Coffee = Latte(flavor: "vanila", shot: 2)
print(actingConstant.description)

 

3.1 타입캐스트 연산자

- Type Cast Operator

- as?as! 두 가지가 있음

- 다운캐스팅은 실패의 여지가 충분히 있기 때문에 ?와 ! 두 가지

- 조건부 연산자 as?는 다운캐스팅에 실패했을 때 nil을 반환

- 다운캐스팅을 강제하는 연산자 as!는 실패했을 때 런타임 오류가 발생

if let actingOne: Americano = coffee as? Americano {
    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: Americano = myCoffee as? Americano {
    print("This is Americano")
} else {
    print(myCoffee.description)
}
// This is Americano

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

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

// Success
let castedCoffee: Coffee = yourCoffee as! Coffee

// 런타임 오류, 강제 다운캐스팅 실패!
let castedAmericano: Americano = coffee as! Americano

 

- 항상 성공하는 다운캐스팅

- as!나 as? 대신에 as를 사용할 수 있음

- 항상 성공하는 것을 아는 경우는 캐스팅하려는 타입이 같은 타입이거나 부모클래스 타입이라는 것을 알 때

let castedAmericano: Coffee = yourCoffee as Coffee

 

* 타입 캐스팅의 의미

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

 

 

 

4. Any, AnyObject의 타입캐스팅

- 특정 타입을 지정하지 않고 여러 타입의 값을 할당할 수 있는 Any와 AnyObject라는 특별한 타입이 있음

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

- 어떤 타입의 데이터라도 전달할 수 있지만, 반환되는 타입도 Any나 AnyObject라면 전달받은 데이터가 어떤 타입인지 확인하고 사용해야 함 (스위프트는 타입에 엄격하므로)

 

* Any와 AnyObject를 사용하면 예기치 못한 오류가 발생할 확률이 높으로 사용을 지양할 것

// AnyObject의 타입확인
func checkType(of item: AnyObject) {
    if item is Latte {
        print("item is Latte")
    } else if item is Americano {
        print("item is Americano")
    } else if item is Coffee {
        print("item is Coffee")
    } else {
        print("Unknown Type")
    }
}

checkType(of: coffee) // item is Coffee
checkType(of: myCoffee) // item is Americano
checkType(of: yourCoffee) // item is Latte
checkType(of: actingConstant) // item is Latte
// AnyObject의 타입캐스팅
func castTypeToAppropriate(item: AnyObject) {
    if let castedItem: Latte = item as? Latte {
        print(castedItem.description)
    } else if let castedItem: Americano = item as? Americano {
        print(castedItem.description)
    } else if let castedItem: Coffee = item as? Coffee {
        print(castedItem.description)
    } else {
        print("Unknown 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) vanila latte

 

- AnyObject는 클래스의 인스턴스만 취할 수 있었지만 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("Jason"))
    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, Jason

 

* 옵셔널과 Any

Any 타입은 옵셔널을 포함한 모든 값 타입을 표현한다. 그런데 Any 타입의 값이 들어와야 할 자리에 옵셔널 타입의 값이 위치한다면 스위프트 컴파일러는 경고를 한다. 의도적으로 옵셔널 값을 Any 타입 값으로 사용하고자 한다면 as 연산자를 사용하여 명시적 타입 캐스팅을 해주어 경고 메시지를 받지 않도록 한다.

// print의 정의
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n")

let optionalValue: Int? = 100
print(optionalValue) // 컴파일러 경고 발생
print(optionalValue as Any) // 경고 없음

 

 

'swift' 카테고리의 다른 글

[swift] 서브스크립트  (0) 2020.06.25
[swift] 모나드  (0) 2020.06.23
[swift] 맵, 필터, 리듀스  (0) 2020.05.13
[swift] 옵셔널 체이닝과 빠른 종료  (0) 2020.05.08
[swift] 클로저  (0) 2020.05.07