Swift 21. 타입캐스팅

 Swift 21. 타입캐스팅




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

다른 프로그래밍 언어에서 대부분 지원하는 암시적 데이터 타입 변환도 지원하지 않음




1. Swift의 타입 변환


[데이터 타입]( [변환할 값] )

형식으로 값을 변환함

=> 해당 타입의 이니셜라이저를 이용하는 것임

기존 값을 전달인자로 받는 이니셜라이저를 통해 해당 타입의 새로운 인스턴스를 생성

Swift에서는 이를 타입 캐스팅이라고 칭하지 않음


* 예시

var a: Double = 3.14


var b: Int = Int(a)

print(b)

<실행결과>

3




각 타입들은 구조체로 정의되어 있는데 각 구조체의 정의를 보면 다양한 이니셜라이저가 구현되어 있음을 확인할 수 있음

대부분 실패하지 않는 이니셜라이저로 구현되어 있지만 실패 가능 이니셜라이저로 구현되어 있는 것도 있음 

예) Int 구조체에서 올바르지 않은 형식의 String 타입의 값이 들어오면 인스턴스 생성에 실패하고 nil 반환






2. Swift의 타입캐스팅


Swift의 타입캐스팅을 타입을 변경한다는 의미가 아닌 조금 다른 의미로 사용

인스턴스의 타입을 확인하거나 자신을 다른 타입의 인스턴스인척 사용할 수 있는 방법으로 사용


as 와 is 연산자로 구현

값의 타입을 확인하거나 다른 타입으로 전환할 수 있음

프로토콜을 준수하는 지도 확인해 볼 수 있음

참조 타입에서 주로 사용됨




* 예시 - Coffee 클래스를 상속받는 Americano 클래스와 Latte 클래스

// 부모 클래스

class Coffe {

    var name: String

    

    init() {

        name = "커피"

    }

}


// 자식 클래스

class Americano: Coffe {

    override init() {

        super.init()

        self.name = "아메리카노"

    }

}


// 자식 클래스

class Latte: Coffe {

    override init() {

        super.init()

        self.name = "라떼"

    }

}



자식 클래스는 부모 클래스의 모든 특성을 갖고 있음

-> 자식 클래스는 부모 클래스인 척 할 수 있지만 부모 클래스는 자식 클래스인 척 할 수 없음 






3. 데이터 타입 확인


타입 확인 연산자 is를 이용해서 인스턴스가 어떤 클래스의 인스턴스 인지, 혹은 어떤 클래스의 자식 클래스인지 확인할 수 있음


[인스턴스] is [클래스]


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

클래스의 인스턴스 뿐만 아니라 모든 데이터 타입에 사용할 수 있음



* 예시

let coffee: Coffee = Coffee()

let myCoffee: Americano = Americano()

let yourCoffee: Latte = Latte()



print(coffee is Coffee)

print(coffee is Americano)

print(coffee is Latte)

print(myCoffee is Coffee)

print(yourCoffee is Coffee)

print(myCoffee is Latte)

print(yourCoffee is Latte)

<실행결과>

true

false

false

true

true

false

true

-> 인스턴스가 해당 클래스의 인스턴스 이거나 해당 클래스의 자식 클래스의 인스턴스일 때만 true




- 메타 타입


타입의 타입

타입 자체가 하나의 타입으로 또 표현될 수 있음

클래스, 구조체, 열거형의 이름은 타입의 이름


타입 이름 뒤에 .Type을 붙이면 메타 타입을 나타냄

프로토콜 타입의 뒤에 .Protocol을 붙이면 프로토콜 타입의 메타 타입을 나타냄

타입 뒤에 self 키워드를 붙여주면 해당 타입을 표현하는 값 반환



* 예시

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


// Any는 모든 타입 적용 가능

var someType: Any.Type


// 메타 타입을 하나의 값으로 취급해서 변수에 할당

someType = intType

print(someType)


someType = stringType

print(someType)


someType = classType

print(someType)


someType = protocolprotocol

print(someType)

<실행결과>

Int

String

SomeClass

SomeProtocol

-> 메타 타입이 하나의 값으로 취급되어 변수에 할당됨



- 인스턴스의 타입을 표현한 값


type(of: ) 함수를 사용하면 해당 인스턴의 타입을 표현한 값 반환



* 예시

let coffee: Coffee = Coffee()

let myCoffee: Americano = Americano()

let yourCoffee: Latte = Latte()


var a: Int = 0

var b: Int.Type = Int.self


print(type(of: a)) // Int형 변수의 타입

print(type(of: b)) // Int의 메타 타입 변수의 타입

print()


print(type(of: coffee) == Coffee.self)

print(type(of: coffee) == Americano.self)

print(type(of: coffee) == Latte.self)

print()


print(type(of: coffee) == Americano.self)

print(type(of: myCoffee) == Americano.self)

print(type(of: yourCoffee) == Americano.self)

print()


print(type(of: coffee) == Latte.self)

print(type(of: myCoffee) == Latte.self)

print(type(of: yourCoffee) == Latte.self)

<실행결과>

Int

Int.Type


true

false

false


false

true

false


false

false

true






4. 다운캐스팅


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

자식 클래스의 인스턴스가 부모 클래스의 인스턴스인척 할 수 있음


* 예시

// Coffee인척 하는 Latte -> Coffee 타입 변수에 Latte 인스턴스 할당

let fake: Coffee = Latte()

print(fake.name)

<실행결과>

라떼

-> fake 상수는 Coffee 인스턴스를 참조하도록 선언했지만 Coffee 타입인척 하는 Latte 인스턴스를 참조

이때 fake가 참조하는 인스턴스를 원래 타입으로 사용해야할 때가 있음 (해당 타입에 정의되어 있는 프로퍼티나 메소드를 사용해야 할 때)

-> fake의 타입을 Latte 타입으로 변환해야 함 

=> 다운캐스팅


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




- 타입캐스트 연산자


as?, as! 두가지가 있음

타입캐스트 연산자를 이용해서 자식 클래스의 타입으로 다운캐스팅할 수 있음

다운캐스팅은 실패의 여지가 있기 때문에 ?, ! 두가지로 나뉨


as? : 다운캐스팅을 시도해보는 조건부 연산자로 실패하면 nil 반환. 성공하면 옵셔널로 반환

[변수 or 상수] as? [타입]

-> 해당 변수나 상수가 참조하는 인스턴스가 해당 타입이나 해당 타입의 부모 타입의 인스턴스라면 다운 캐스팅 진행


as! : 다운캐스팅을 강제하는 연산자로 실패하면 런타임 에러 발생. 성공하면 반환타입이 옵셔널이 아님

[변수 or 상수] as! [타입]

-> 해당 변수나 상수가 참조하는 인스턴스가 해당 타입의 인스턴스인지 상관없이 다운 캐스팅 진행 (실패하면 런타임 오류)


다운캐스팅을 실패할 여지가 있다면 as?을 사용

다운캐스팅을 무조건 성공한다고 확신하면 as! 사용



* 예시

if let actingOne: Americano = coffee as? Americano {

    print("\(coffee.name) is Americano")

} else {

    print("\(coffee.name) is not Americano")

}


if let actingOne: Latte = coffee as? Latte {

    print("\(coffee.name) is Latte")

} else {

    print("\(coffee.name) is not Latte")

}


if let actingOne: Coffee = coffee as? Coffee {

    print("\(coffee.name) is Coffee")

} else {

    print("\(coffee.name) is not Coffee")

}


if let actingOne: Americano = myCoffee as? Americano {

    print("\(myCoffee.name) is Americano")

} else {

    print("\(myCoffee.name) in nopt Americano")

}


if let actingOne: Latte = myCoffee as? Latte {

    print("\(myCoffee.name) is Latte")

} else {

    print("\(myCoffee.name) is not Latte")

}


if let actingOne: Coffee = myCoffee as? Coffee {

    print("\(myCoffee.name) is Coffee")

} else {

    print("\(myCoffee.name) si not Coffee")

}


// 성공하는 강제 타입 캐스팅

let castedCoffee: Coffee = yourCoffee as! Coffee

// 강제 타입캐스팅에 실패해서 런타임 에러 발생

// let castedAmericano: Americano = coffee as! Americano

<실행결과>

커피 is not Americano

커피 is not Latte

커피 is Coffee

아메리카노 is Americano

아메리카노 is not Latte

아메리카노 is Just Coffee




* 예시

class Latte: Coffee {

    override init() {

        super.init()

        self.name = "라떼"

    }

    

    func cream() {

        print("크림을 추가합니다.")

    }

}

-> Latte 클래스에만 cream() 메소드 구현. Latte 타입인 변수나 상수에서만 호출 가능

// Coffee인척 하는 Latte -> Coffee 타입 변수에 Latte 인스턴스 할당

let fake: Coffee = Latte()

// Coffee 타입 변수에서는 Latte 클래스에 정의된 cream() 메소드 호출 불가

// fake.cream()


// cream() 메소드를 호출하려면 Latte 타입으로 다운캐스팅 해야함

if let new: Latte = fake as? Latte {

    // fake가 참조하는 인스턴스는 Latte 타입의 인스턴스이기 때문에 다운캐스팅 가능

    new.cream()

}

else {

    print("fail..")

}

<실행결과>

크림을 추가합니다.




컴파일러가 다운캐스팅을 확신하는 경우에는 as? 나 as! 대신  as 사용 가능

캐스팅 하려는 타입이 같은 타입이거나 부모 클래스 타입이라는 것을 알 때만 가능


* 예시

// 항상 다운캐스팅을 성공한다는 것을 컴파일러가 알고 있음

let casted: Coffee = yourCoffee as Coffee




* 타입캐스팅의 의미

실제로 인스턴스를 수정하거나 값을 변경하는 작업이 아님

인스턴스를 사용할 때 어떤 타입으로 다루고 어떤 타입으로 접근해야 할 지 판단할 수 있도록 컴퓨터에 알려주는 것




5. Any, AnyObject의 타입캐스팅


Swift에는 특정 타입을 지정하지 않고 여러 타입의 값을 할당할 수 있는 Any와 AnyObject 타입이있음

Any는 함수 타입을 포함하는 모든 타입

AnyObject는 클래스 타입만 해당


* Any와 AnyObject 타입은 예기치 못한 오류 발생 확률을 높이므로 되도록이면 사용을 지양



Swift 표준 라이브러리에서는 Any와 AnyObject 타입을 거의 사용하지 않지만 다른 프레임워크의 API에서는 Any 와 AnyObject 타입을 종종 사용 -> 어떤 타입의 데이터라도 전달할 수 있음

Swift는 암시적 타입 변환을 허용하지 않고 타입에 굉장이 엄격하기 때문에 API를 사용할 때 Any 나 AnyObject 타입의 값이 반환되면 전달된 데이터가 어떤 타입인지 확인하고 사용해야 함



* 예시 - AnyObject의 활용

let coffee: Coffee = Coffee()

let myCoffee: Americano = Americano()

let yourCoffee: Latte = Latte()


func checkType(of item: AnyObject) {

    if item is Latte {

        print("This is Latte")

    }

    else if item is Americano {

        print("This is Americano")

    }

    else if item is Coffee {

        print("This is Coffee")

    }

    else {

        print("Unknown Type")

    }

}


checkType(of: coffee)

checkType(of: myCoffee)

checkType(of: yourCoffee)

<실행결과>

This is Coffee

This is Americano

This is Latte

-> 매개변수 타입이 AnyObject 라서 모든 클래스의 인스턴스가 매개변수로 전달될 수 있음

-> 타입이 AnyObject이기 때문에 해당 인스턴스의 메소드나 프로퍼티에는 접근을 못하고 어느 클래스의 인스턴스인지만 확인할 수 있음

=> 해당 인스턴스를 사용하기 위해서는 해당 타입으로 다운캐스팅 해야 함



* 예시 - AnyObject 타입의 전달인자의 타입을 확인하고 해당 타입으로 다운캐스팅

let coffee: Coffee = Coffee()

let myCoffee: Americano = Americano()

let yourCoffee: Latte = Latte()


func checkType(of item: AnyObject) {

    if let casted = item as? Latte {

        // 해당 타입으로 다운캐스팅해서 인스턴스의 프로퍼티 이용가능

        print(casted.name)

    }

    else if let casted = item as? Americano {

        // 해당 타입으로 다운캐스팅해서 인스턴스의 프로퍼티 이용가능

        print(casted.name)

    }

    else if let casted = item as? Coffee {

        // 해당 타입으로 다운캐스팅해서 인스턴스의 프로퍼티 이용가능

        print(casted.name)

    }

    else {

        print("Unknown Type")

    }

}


checkType(of: coffee)

checkType(of: myCoffee)

checkType(of: yourCoffee)

<실행결과>

커피

아메리카노

라떼




AnyObject 타입은 클래스의 인스턴스만 취할 수 있다면 Any 타입은 모든 타입의 인스턴스를 취할 수 있음


* 예시 - Any 타입의 타입캐스팅

func checkAnyType(of item: Any) {

    switch item {

    case 0 as Int:

        print("Int 0")

    case 0 as Double:

        print("Double 0")

    case let somelnt as Int:

        print("Int \(somelnt)")

    case let someDouble as Double where someDouble > 0:

        print("양수 Double \(someDouble)")

    case is Double:

        print("양수가 아닌 Double")

    case let someString as String:

        print("String \"\(someString)\"")

    case let (x, y) as (Double, Double):

        print("(x, y) point \(x), \(y)")

    case let latte as Latte:

        print(latte.name)

    case let stringconverter as (String) -> String:

        print(stringconverter("Lee"))

    default:

        print("something else : \(type(of: item))")

    }

}


checkAnyType(of: 0)

checkAnyType(of: 0.0)

checkAnyType(of: 42)

checkAnyType(of: 3.14159)

checkAnyType(of: -0.25)

checkAnyType(of: "hello")

checkAnyType(of: (3.0, 5.0))

checkAnyType(of: yourCoffee)

checkAnyType(of: coffee)

checkAnyType(of: { (name: String) -> String in "Hello, \(name)" })

<실행결과>

Int 0

Double 0

Int 42

양수 Double 3.14159

양수가 아닌 Double

String "hello"

(x, y) point 3.0, 5.0

라떼

something else : Coffee

Hello, Lee

-> Any 타입의 매개변수에 다양한 타입의 인스턴스를 전달인자로 전달

-> switch, as, let 바인딩 등을 이용해서 해당 인스턴스의 타입을 확인하고 해당 타입으로 다운캐스팅



- 옵셔널과 Any

Any 타입은 모든 타입의 값을 표현할 수 있어서 옵셔널 타입도 표현 가능

그런데 Any 타입의 값이 들어와야할 자리에 옵셔널 값이 들어오면 Swift 컴파일러는 경고함

의도적으로 옵셔널 값을 Any 타입의 값으로 사용하려면 as 연산자로 옵셔널 타입을 Any 탕입으로 타입캐스팅 하면 경고가 발생하지 않음


* 예시 - print() 함수의 매개변수는 Any 타입





<실행결과>

Optional(10)

Optional(10)






댓글

이 블로그의 인기 게시물

Spring Boot 6. 계시판 프로젝트

Spring Boot 2. Maria 데이터베이스 & Spring Data JPA

Django 10. DB 연동