궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [타입 캐스팅(Type Casting)]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과Zedd0202님의 게시글을 참고하여 작성되었습니다.

 

타입 캐스팅

타입캐스팅이란 인스턴스 타입을 확인하거나, 인스턴스의 타입을 슈퍼클래스 또는 서브클래스 타입처럼 다루기 위해 사용한다.

  • Swift에서 타입 캐스팅은 isas 연산자를 사용하여 구현함
  • 이 두 연산자는 값의 타입을 확인하거나, 값을 다른 타입으로 전환(Cast)하는 간단하고 표현적인 방법을 제공함
  • 타입 캐스팅을 통해 특정 클래스 인스턴스의 타입을 확인 가능
  • 타입 캐스팅을 통해 프로토콜을 준수하는지도 확인 가능

먼저 알아볼것은 '타입 캐스팅을 사용하여 특정 클래스 인스턴스의 타입을 확인 가능' 부분이다.

 

인스턴스의 타입은 is 키워드를 사용해서 확인이 가능하다.

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

var seogun = Person(name: "서근")

if seogun is Person {
   print(true)  //true
}

Person이라는 클래스를 정의하고 초깃값이 없기 때문에 초기화를 해줬다. 그리고 is 키워드를 통해 'segoun'이 'Person' 타입이 맞는지 확인한다.

 

이런 식으로 인스턴스 자체를 확인할 수 도 있지만, 인스턴스의 프로퍼티도 확인 가능하다.

var seogun = Person(name: "서근")

if seogun.name is String {
    print(true)
}

is 타입 확인 키워드는 학상 true 이여야만 한다는 걸 기억해야 한다!

 

Swift 타입 캐스팅은 실제로 참조 타입에 주로 사용된다. 아래 예제를 통해 확인해 보자면

class Coffee {
    let name: String
    let shot: Int
    
    var description: String {
        return "\(shot)샷 \(name)"
    }
    
    init(shot: Int) {
        self.shot = shot
        self.name = "커피"
    }
}
//Coffee 클래스를 상속받은 Lattee 클래스
class Latte: Coffee {
    var flavor: String
    
    //부모클래스의 description 재정의
    override var description: String {
        return "\(shot)샷 \(flavor) 라떼"
    }
    
    init(flavor: String, shot: Int) {
        self.flavor = flavor
        super.init(shot: shot)
    }
}
//슈퍼클래스인 Coffee를 상속받은 Americano 클래스
class Americano: Coffee {
    let iced: Bool
    
    //마찬가지로 desctiption 재정의.
    override var description: String {
                               //삼항연산자
        return "\(shot)샷 \(iced ? "아이스" : "핫") 아메리카노"
    }
    
    init(shot: Int, iced: Bool) {
        self.iced = iced
        super.init(shot: shot)
    }
}

LatteAmericano 클래스는 Coffee 클래스를 상속받았고, 슈퍼클래스의 특성들을 모두 포함하여 받은 것을 확인할 수 있다.

 

즉, LatteAmericano 클래스는 Coffee 인척 할 수 있지만, Coffee는 그렇게 하지 못한다.

 

이때 타입 캐스팅을 활용할 수 있다.

데이터 타입 확인 - is 

타입 확인 연산자 is를 사용하여 타입을 확인해보자!

let coffee: Coffee = Coffee(shot: 3)
print(coffee.description) //3 샷 커피

let myLatte: Latte = Latte(flavor: "바닐라", shot: 2)
print(myLatte.description) //2샷 바닐라 라떼

let yourAmericano: Americano = Americano(shot: 2, iced: true)
print(yourAmericano.description) //2샷 아이스 아메리카노

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

print(myLatte is Coffee) //true
print(yourAmericano is Coffee) //true

print(myLatte is Americano) //false
print(yourAmericano is Americano) //true

코드를 보면 coffee 타입이 Coffee 타입이죠? print(coffee is Coffee)true 지만, print(coffee is Lattee)false이다. 즉 Coffee 클래스는 LatteAmericano 클래스 타입이 될 수 없다는 뜻이다.

 

반대로 myLatteyourAmericano는 각각 Coffee 클래스를 상속받은 클래스들 이기 때문에 Coffee 타입 인지 확인했을 때 true를 반환받았다!

메타 타입 타입

is 연산자 이외에도 타입을 확인해 볼 수 있는 방법은 메타 타입(Meta Type) 타입을 사용하는 것이다.

 

메타 타입 타입 이란 타입의 타입이라는 뜻!

클래스 / 구조체 / 열거형 / 프로토콜 등의 타입의 타입! 즉, 타입 자체가 하나의 타입으로 또 표현할 수 있다는 의미

 

무슨 소리인가 하면....

 

클래스 / 구조체 / 열거형의 이름은 타입의 이름인데, 그래서 클래스는 참조 타입, 구조체/열거형은 값 타입이라고 하는 것! 그 타입의 이름 뒤에 .Type을 붙이면 이것은 메타 타입을 나타냄!

 

프로토콜 타입 메타 타입은 .Protocol 키워드를 사용한다.

 

예를 들어보자면, SomeClass 클래스의 메타 타입은 SomeClass.Type이다. SomeProtocol의 메타 타입은 SomeProtocol.Protocol

 

또, self를 사용하여 타입을 값처럼 표현도 가능하다.

 

예를 들어 SomeClass.selfSomeClass의 인스턴스가 아닌 SomeClass 타입을 값으로 표현한 값을 반환한다. SomeProtocol.self 이면 SomeProtocol 프로토콜을 값으로 표현한 값을 반환한다는 의미!

protocol SomeProtocol { }
class SomeClass: SomeProtocol { }

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

var someType: Any.Type

someType = stringType //String.Type
print(someType) //String

someType = classType
print(someType) //someClass

TIP
 
 

인스턴스 self와 타입 self의 의미
.self 표현은 값 뒤에 사용하면 그 값 자신을 의미, 타입 이름 뒤에 쓰면 타입을 표현하는 값을 반환한다.

"StringValue".self"StringValue" 그 자체를 String.selfString 타입을 나타내는 값이다.

프로그램 실행 중에 인스턴스 타입을 표현한 값을 알아보려면 type(of:) 함수를 하면 된다.

 

만약, type(of: someInstance).self 라고 정의하면 someInstance의 타입을 값으로 표현한 값을 반환하게 된다.

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

다운캐스팅으로 넘어가기 전에 타입캐스팅 Apple 예제를 보고 넘어가 보자!

class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}
// MediaItem(슈퍼)클래스를 상속받은 Movie
class Movie: MediaItem {

    var director: String

    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

// MediaItem(슈퍼)클래스를 상속받은 Song
class Song: MediaItem {

    var artist: String

    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}
let library = [
    Movie(name: "도둑들", director: "최동훈"),
    Song(name: "light Switch", artist: "찰리 푸스"),
    Movie(name: "이터널스", director: "클로이 자오"),
    Song(name: "다이너마이트", artist: "방탄소년단"),
    Song(name: "Rumor", artist: "Lizzo")
]

배열을 하나 만들었는데 이 안에는 MediaItem을 상속받는 MovieSong 클래스가 섞여 있다. 이렇게 library 배열은 MediaItem타입의 배열을 갖게 된다.

 

이제 is 타입 캐스팅을 사용해보자

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는 \(movieCount)개의 영화가 있고, \(songCount)개의 노래가 있습니다")
// Media library는 2개의 영화가 있고, 3개의 노래가 있습니다s"

library 배열을 돌며 해당 인스턴스가 Movie타입 인지, Song타입 인지 is 키워드를 통해 확인하고 있다!

다운 캐스팅

특정 클래스 타입의 상수 또는 변수는 하위 클래스의 인스턴스를 참조할 수 있다. 이 경우, 타입 캐스트 연산자 as? 또는 as! 를 사용하여 서브 클래스 타입으로 downcasting을 시도할 수 있다.

 

다운 캐스팅은 실패할 여지가 충분히 있기 때문에 타입 캐스트 연산자는 두 가지 형태로 제공된다.

 

as?

  • 조건부 형식인 as? 는 다운 캐스팅하려는 타입의 옵셔널 값을 반환한다.
  • 캐스팅에 실패할 경우 nil을 반환한다.
  • 캐스팅에 성공하면 옵셔널 타입으로 인스턴스를 반환한다.
  • 실패할 가능성이 있으면 조건부 연산자인 as?를 사용
  • 런타임에 실행된다.

as!

  • 강제 형식인 as! 는 강제 언래핑을 하여 값을 반환한다.
  • 캐스팅에 실패할 경우 런타임 오류가 발생한다.
  • 캐스팅에 성공할 경우 옵셔널이 아닌 인스턴스를 반환한다.
  • 항상 성공할 것이라는 확신이 있으면 강제 형식인 as! 를 사용
  • 런타임에 실행된다.
for item in library {
    if let movie = item as? Movie {
      print("영화: \(movie.name), 감독: \(movie.director)")
    } else if let song = item as? Song {
      print("노래: \(song.name), 가수: \(song.artist)")
    }
}

/*
영화: 도둑들, 감독: 최동훈
노래: light Switch, 가수: 찰리 푸스
영화: 이터널스, 감독: 클로이 자오
노래: 다이너마이트, 가수: 방탄소년단
노래: Rumor, 가수: Lizzo
*/

if let 구문을 활용해 as? 조건부 형식 다운 캐스팅을 했다. libraryMediaitem 타입 배열이기 때문에, 그 안에 있는 인스턴스는 MediaItem의 서브클래스인 MovieSong 이기 때문이다.

 

즉! MediaItemSuper 클래스 / MovieSongMediaItem의 자식클래스 이기 때문에 다운 캐스팅이 당연함(as)!! 

 

for문에서 itemMediaItem인스턴스 이니까 Movie가 될 수도 Song이 될 수 도 있다.

 

또, 조건부 형식 as? 는 옵셔널을 반환하니까, if let 구문으로 꺼내왔다. 


위에서 했던 Coffee 예제를 다시 사용해서 다운 캐스팅을 해보자!

if let actingOne: Americano = coffee as? Americano {
    print("이것은 아메라카노 입니다.")
} else {
    print(coffee.description)
}
//coffee는 Americano가 아니기 때문에 print(coffee.description) 구문 출력
//3샷 커피

if let actingOne: Latte = myLatte as? Latte {
    print("이것은 라떼 입니다.")
} else {
    print(coffee.description)
}
//이것은 라떼 입니다.

if let actingOne: Americano = yourAmericano as? Americano {
    print("이것은 아메리카노 입니다")
} else {
    print(coffee.description)
}
//이것은 아메리카노 입니다.

if let actingOne: Latte = yourAmericano as? Latte {
    print("이것은 라떼 입니다.")
} else {
    print(yourAmericano.description)
}
//2샷 아이스 아메리카노

한 가지만 놓고 보면 if let actingOne: Americano = cofee as? Americano 는 "만약  coffee가 참조하는 인스턴스가 Americano 타입의 인스턴스 라면 actingOne이라는 임시 상수에 할당하라"이다.

 

하지만 coffeeCoffee 클래스 이기 때문에 Coffee 클래스는 Americano 클래스를 포함하지 않는다. 그렇기에 다운 캐스팅에 실패하여 else 구문의 print(coffee.description) 이 출력된다!

 

as 

  • 컴파일 시점에 타입 캐스팅(upcasting)을 하며, 실패할 경우 에러가 발생한다. 
  • 팬턴 매칭(switch)에서도 사용한다.
  • 컴파일러가 다운캐스팅을 확신할 경우 as? / as! 대신 as를 사용한다.
  • 캐스팅하려는 타입이 같은 타입 이거나 부모클래스 타입이라는 것을 알 때 as 연사자를 사용
//항상 성공한다는 것을 컴파일러도 알고 있다.
let castedCoffee: Coffee = myAmericano as Coffee

Any, AnyObject 타입 캐스팅

Swift에는 특정 타입을 지정하지 않고 여러 타입의 값을 할당할 수 있는 AnyAnyObject 타입이 존재한다.

 

Any - 함수(function) 타입을 포함한 모든 타입의 인스턴스

AnyObject - 모든 클래스 타입의 인스턴스

 

TIP
 
 

Any / AnyObject 사용 주의사항
AnyAnyObject를 사용하면 예상하지 못한 오류가 발생할 확률이 높아지므로 되도록 사용을 지양하는 것을 권장한다. 코드에서 처리할 타입은 항상 구체적으로 기술하는 것이 좋다.

//클래스의 인스턴스만 수용할 수 있는 AnyObject 타입

func checkType(of item: AnyObject) {
    if item is Latte {
        print("라떼 입니다.")
    } else if item is Americano {
        print("아메리카노 입니다.")
    } else if item is Coffee {
        print("커피 입니다.")
    } else {
        print("Unknown 타입")
    }
}

checkType(of: coffee) //커피 입니다.
checkType(of: myLatte) //라떼 입니다.
checkType(of: yourAmericano) //아메리카노 입니다.

위 코드에서는 is 연산자를 사용하여 해당 인스턴스가 어떤 타입의 인스턴스 인지만 확인해 보았다.

 

이제 item이 어떤 타입인지 판단하는 동시에 해당 타입의 인스턴스로 사용할 수 있도록 캐스팅할 수 있다.

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("Unknwon 타입")
    }
}

castTypeToAppropriate(item: coffee) //3샷 커피
castTypeToAppropriate(item: myLatte) //2샷 바닐라 라떼
castTypeToAppropriate(item: yourAmericano) //2샷 아이스 아메리카노

AnyObject는 클래스의 인스턴스만 취할 수 있었지만, Any는 모든 타입의 인스턴스를 취할 수 있다. 

 

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

func checkAnyType(of item: Any) {
    switch item {
    case 0 as Int:
        print("zero as an Int")
    case 0 as Double:
        print("zero as an Double")
    case let someInt as Int:
        print("an integer value of \(someInt)")
    case let (x, y) as (Double, Double):
        print("an (x, y) point at \(x), \(y)")
    case let stringConverter as (String) -> String:
        print(stringConverter("서근"))
    default:
        print("something else : \(type(of: item))")
    }
}

checkAnyType(of: 0) //zero as an Int




checkAnyType(of: 0.0) //zero as an Double
checkAnyType(of: 30) //an integer value of 30
checkAnyType(of: (2.0, 5.1)) //an (x, y) point at 2.0, 5.1
//클로저 사용
checkAnyType(of: { (name: String) -> String in "\(name) 개발 블로그 입니다!"} )
//서근 개발 블로그 입니다!

TIP
 
 

as와 as? as! 차이점
as는 컴파일 타임에, as?as! 는 런타임에 실행된다. as는 업캐스팅(항상 성공하는)과 패턴 매칭(switch)에서만 사용 가능하다.

옵셔널과 Any

Any타입은 모든 값 타입을 표현하는데, 옵셔널 타입도 표현 가능하다. 하지만 Any타입의 값이 들어와야 할 자리에 옵셔널 타입의 값이 위치한다면 Swift는 컴파일러 경고를 한다. 의도적으로 옵셔널 값을 Any 타입으로 사용하려면 as 업캐스팅을 사용하여 명시적 타입 캐스팅을 해주면 경고 메시지를 받지 않는다.

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

 

 

읽어주셔서 감사합니다 🤟

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.