본문 바로가기

Swift

Type Casting Up? Down?

728x90

타입캐스팅 업? 다운? 죽여? 살려? ㅈㅅ

 

타입캐스팅 ... 이름이 낯설 수도 있는데 사실 그렇지 않다. 우리가 알게 모르게 많이 사용하고 있는 개념이다. 

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