타입캐스팅 업? 다운? 죽여? 살려? ㅈㅅ
타입캐스팅 ... 이름이 낯설 수도 있는데 사실 그렇지 않다. 우리가 알게 모르게 많이 사용하고 있는 개념이다.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeMainPublicGroupCollectionViewCell.cellIdentifier, for: indexPath) as? HomeMainPublicGroupCollectionViewCell else { return UICollectionViewCell() }
이런 코드들을 한번쯤 작성한 적이 있을 것이다. (없다면 돌아가.)
위 코드를 해석하면, collectionView에 register 된 cell 중에서 HomeMainPublicGroupCollectionViewCell 이 아이를 꺼내와서 cell로 사용하겠다 ~ 이런 의미이다.
이 때, 그냥 단순히 꺼내오면 UICollectioViewCell 타입으로 지정이 되겠지만 그 셀을 HomeMainPublicGroupCollectionViewCell 로 다운캐스팅을 해서 사용하고 있다.
왜 다운캐스팅을 할까?
저 타입(= HomeMainPublicGroupCollectionViewCell) 으로 지정을 해야 사용할 수 있는 프로퍼티/메서드가 있을 수 있기 때문이다.
즉, 단순히 꺼내온다면 그 타입은 UICollectionViewCell 이므로 셀의 원하는 프로퍼티/메서드를 접근하거나 호출할 수 없다.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeMainPublicGroupCollectionViewCell.cellIdentifier, for: indexPath) as? HomeMainPublicGroupCollectionViewCell else { return UICollectionViewCell() }
cell.initCell(group: publicGroupData[indexPath.item])
cell.cellView.delegate = self
cell.cellView.index = indexPath.item
위와 같이 다운캐스팅을 해야 원하는 값에 접근하거나 함수를 호출할 수 있다.
_
이런 식으로 타입캐스팅을 사용할 수 있다.
그렇다면 타입캐스팅이 정확히 무엇인지 알아보도록 하자.
보다 자세한 내용은 .. 공식문서 ㄱ ㄱ
Type Casting
타입캐스팅은 인스턴스의 타입을 확인하거나 인스턴스를 같은 계층에 있는 다른 superclass나 subclass로 취급하는 방법이다.
타입캐스팅에는 is 와 as 두 연산자를 사용하고, 타입캐스팅을 이용하면 특정 프로토콜을 따르는지 확인할 수도 있다.
타입캐스팅을 위한 클래스 계층구조 선언
미디어아이템 클래스를 만들어보자.
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)
}
}
그리고 이 두 개의 클래스를 아이템으로 갖는 library 배열을 선언해보자.
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")
]
이 때, library의 타입을 확인해보면 [MediaItem] 이다.
library가 갖고 있는 Movie, Song의 인스턴스 공통 부모는 MediaItem이기 때문이다. library를 순회(iterate)하면 배열의 아이템은 Movie, Song이 아니라 MediaItem이라는 것을 확인할 수 있다.
(-> 타입 지정을 하고 싶다면, downcasting을 해야 한다.)
형 확인
형 확인을 할 수 있는 키워드는 is이다.
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 contains \(movieCount) movies and \(songCount) songs")
// "Media library contains 2 movies and 3 songs"
-> for-in문을 이용하여 library 배열의 아이템을 모두 순회, is 를 통해 타입을 확인할 수 있다.
다운캐스팅
특정 클래스 타입의 상수/변수는 특정 서브클래스의 인스턴스를 참조하고 있을 수 있다.
as?와 as! 연산자를 이용해 어떤 타입의 인스턴스인지 확인할 수 있다.
(-> 이 두 연산자를 다운캐스팅 연산자라고 할 수 있다.)
as?
특정 타입이 맞는지 확신할 수 없을 때 사용한다.
as!
특정 타입이라는 것을 확신할 수 있을 때 사용한다.
만약 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
-> library를 순회하면서 Movie 타입이라면 그에 맞는 프로퍼티를 출력하고 Song이라면 그것에 맞는 프로퍼티를 출력한다.
🔴 주의할 것은
캐스팅은 실제 인스턴스/값을 바꾸는 것이 아니라 지정한 타입으로 취급하는 것뿐이다.
Any, AnyObject의 타입캐스팅
Swift에서는 두 가지 특별한 타입을 제공합니다.
타입캐스팅을 수행할 때 일반적으로 상속 관계에 있는 클래스끼리만 캐스팅이 가능하지만, Any/AnyObject 타입을 사용할 경우 상속 관계에 있지 않아도 타입캐스팅을 할 수 있다.
- Any : 함수 타입을 포함해 모든 타입을 나타낸다.
- AnyObject : 모든 클래스 타입의 인스턴스를 나타낸다.
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 타입의 배열의 경우, 모든 타입이 들어갈 수 있다.
Int, Double 뿐만 아니라 튜플, 함수도 Any 타입에 포함될 수 있다..
🔴 주의할 것은
Any 타입은 옵셔널 타입을 포함한다.
그러나 Swift에서는 Any 타입을 사용해야 하는 곳에 옵셔널을 사용하면 오류를 발생한다.
let optionalNumber: Int? = 3
things.append(optionalNumber) // 경고
things.append(optionalNumber as Any) // 경고 없음
Any : 모든 타입을 저장
Swift는 타입에 민감한 언어이다. 그러므로 만약 Int형 배열에 다른 타입을 넣으려고 하면 오류가 난다.
var nums: [Int] = []
nums.append(1)
nums.append(1.0) // Cannot convert value of type 'Double' to expected argument type 'Int'
nums.append("SO-KYTE") // Cannot convert value of type 'String' to expected argument type 'Int'
nums.append(false) // Cannot convert value of type 'Bool' to expected argument type 'Int
그런데 Any를 사용하게 되면 여러 타입을 저장할 수 있다.
var things: [Any] = []
things.append(1)
things.append(1.0)
things.append("SO-KYTE")
things.append(false)
things.append(Human.init()))
things.append({ print("I am SO-KYTE)!") })
AnyObject : 모든 클래스 타입을 저장
Any는 구조체/열거형/클래스 등 어떤 타입이건 모두 허용했다면, AnyObject는 그것보다 한층 까다로워진 것을 말한다.
타입이 AnyObject로 선언될 경우, 클래스 타입만 저장할 수 있다.
var things: [AnyObject] = []
things.append(1) // Argument type 'Int' expected to be an instance of a class
things.append(1.0) // Argument type 'Double' expected to be an instance of a class
things.append("SO-KYTE") // Argument type 'String' expected to be an instance of a class
things.append(false) // Argument type 'Bool' expected to be an instance of a class
things.append(Teacher.init()))
things.append({ print("I am SO-KYTE!") }) // Argument type '()->()' expected to be an instance of a class
클래스타입인 Teacher 인스턴스를 제외하면 모두 오류가 난다.
Any/AnyObject에서의 다운캐스팅
Any/AnyObject가 모든 타입을 저장할 수 있기 때문에 편하다고 생각할 수 있지만 그만큼 불편한 점도 많다.
var name: Any = "소깡이"
위와 같이 변수를 선언한 경우, 이는 String 타입이라는 것을 알 수 있다.
그러나 name에 대고 String의 프로퍼티/메서드에 접근하려고 하면,
이렇게 아무것도 보이지 않는 것을 알 수 있다.
억지로 메서드를 작성해주면 오류가 난다.
🔴 왜냐하면
Any 와 AnyObject는 런타임 시점에 타입이 결정되기 때문에 컴파일 시점에서 위와 같이 해당 타입에 대해 알 수가 없다.
그러므로 해당 타입의 멤버에도 접근할 수 없는 것이다.
만약, name을 String으로 인식해서 멤버에 접근하고 싶다면
if var name = name as? String {
name.append("후리방구")
}
이렇게 as?를 통해 다운캐스팅을 한 뒤에 사용할 수 있다.
Swift는 타입에 민감한 언어이다.
Any와 AnyObject는 그것과 조금 상이하고, 런타임 시점에서 타입이 결정되므로 만약 오류가 있을 때 런타임 오류로 빠지게 된다.
그리고 Any로 선언할 경우, 타입 캐스팅을 하기 전까지 원하는 멤버에 접근할 수 없기 때문에 남발하지 않는 것이 좋다.
'Swift' 카테고리의 다른 글
URLSession (개념) (0) | 2022.09.01 |
---|---|
Codable (0) | 2022.08.31 |
Framework (Dynamic VS Static) (1) | 2022.08.19 |
Access Control (feat. Framework) (1) | 2022.08.16 |
Attribute (@어쩌구) (0) | 2022.08.10 |