소피it블로그

[Swift 공식문서] Type Casting 정리 본문

개발_iOS/스위프트

[Swift 공식문서] Type Casting 정리

sophie_l 2022. 8. 10. 11:58

https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html

 

Type Casting — The Swift Programming Language (Swift 5.7)

Type Casting Type casting is a way to check the type of an instance, or to treat that instance as a different superclass or subclass from somewhere else in its own class hierarchy. Type casting in Swift is implemented with the is and as operators. These tw

docs.swift.org

타입 캐스팅은 인스턴스의 타입을 확인하거나 해당 인스턴스 자신의 클래스 위계질서 안에 있는 다른 슈퍼클래스 혹은 서브클래스로 치환하는 것을 의미한다.

스위프트에서의 타입캐스팅은 is 와 as 연산자로 구현된다. 이 연산자들은 특정 값의 타입을 확인하거나 값을 새로운 타입으로 cast할 수 있는 간단하고 명료한 방법을 제공한다.

 

1. 타입 캐스팅을 위한 클래스 위계 정의하기

 

특정 클래스 인스턴스의 타입을 확인하거나 해당 인스턴스를 같은 위계질서 내의 다른 클래스로 캐스팅하기 위해 타입 캐스팅을 사용할 수 있다. 이하의 코드 스니펫 세 가지는 타입 캐스팅의 예시로 사용될 클래스들의 위계와 해당 클래스들의 인스턴스를 포함하는 배열을 정의한다.

첫 번째 스니펫은 MediaItem이라는 기본 클래스를 정의한다. 이 클래스는 디지털 미디어 라이브러리에 해당되는 아이템들에 대한 기본적인 기능을 제공한다. 구체적으로는 String 타입의 name 프라퍼티와 initializer라는 이름의 이니셜라이징 메서드를 선언한다(영화나 노래 등과 같은 미디어 아이템은 전부 이름이 있을 것이기 때문).

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

다음 스니펫은 MediaItem 클래스의 서브클래스 두 가지를 정의한다. 첫 번째 서브클래스인 Movie는 영화에 관한 추가 정보를 포함한다. 이는 기본 MediaItem 클래스의 위에 director 프라퍼티와 이니셜라이저를 덧붙인다. 두 번째 서브클래스인 Song은 베이스 클래스 위에 artist 프라퍼티와 이니셜라이저를 덧붙인다.

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라고 하는 상수 배열을 만드는데, 이는 Movie 인스턴스 두 개와 Song 인스턴스 세 개를 포함한다. library 배열의 타입은 array literal의 내용물로 이니셜라이징하는 데서 추론된다. 스위프트의 타입 체커는 Movie와 Song이 공통의 슈퍼클래스인 MediaItem을 갖는다는 것을 추론할 수 있기 때문에 library 배열에 대하여 [MediaItem]이라는 타입을 추론한다.

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")
]
// the type of "library" is inferred to be [MediaItem]

library 안에 저장된 아이템 각각은 여전히 Movie와 Song의 인스턴스들이지만, 배열의 내용물을 iterate해본다면 이들이 반환하는 타입은 Movie나 Song이 아닌 MediaItem인 것을 확인할 수 있다. 이들을 본래의 타입인 Movie나 Song으로써 활용하기 위해서는 이하와 같이 그들의 타입을 확인하거나 다른 타입으로 downcast해야 한다.

 

2. 타입 확인하기

 

타입 확인 연산자인 is를 통해 인스턴스가 어느 특정한 서브클래스 타입을 갖는지를 확인할 수 있다. 타입 확인 연산자는 해당 인스턴스가 특정 서브클래스에 해당한다면 true를, 아니면 false를 반환한다.

이하의 예시는 movieCount와 songCount라는 두 변수를 정의하는데, 이는 library 배열 내의 Movie와 Song 인스턴스들의 개수를 각각 계산한다.

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")
// Prints "Media library contains 2 movies and 3 songs"

이 예시는 library 배열 내의 모든 아이템을 iterate하며, 각각의 iteration에서 for-in 루프는 배열 내의 다음 MediaItem에 대해 item 상수를 설정한다.

item is Movie는 현재의 MediaItem이 Movie의 인스턴스일 경우 true를, 아닐 경우 false를 리턴한다. 마찬가지로 item is Song은 아이템이 Song의 인스턴스인지를 확인한다. for-in 루프의 끝에서 movieCount와 songCount의 값들은 MediaItem 인스턴스에서 각각의 타입이 얼마나 발견되었는지를 숫자로서 나타낸다.

 

3. 다운캐스팅

 

특정 클래스 타입의 상수나 변수는 실제로는 그 서브클래스의 인스턴스를 의미할 수도 있다. 이런 상황일 때는 타입 캐스트 연산자인 as? 또는 as!를 통해 해당 상수 또는 변수를 서브클래스 타입으로 다운캐스팅해줄 수 있다.

다운캐스팅이 언제나 성공하는 것은 아니기 때문에 타입 캐스트 연산자는 두 개의 다른 형태로 존재한다. as?는 conditional form으로서 다운캐스팅하려는 타입의 옵셔널 값을 반환한다. 한편, forced form인 as!의 경우 하나의 복합 행동으로써 다운캐스팅과 force-unwrapping을 한다.

conditional form의 타입 캐스트 연산자 as?는 다운캐스팅이 성공할지 확실하지 않은 경우에 사용하라. 이 연산자는 항상 옵셔널 값을 반환할 것이며 다운캐스팅이 불가능한 경우에는 nil값이 될 것이다. 따라서 성공적인 다운캐스팅이 될 것인지를 확인할 수 있게 한다.

반면, forced form의 타입 캐스트 연산자 as!는 다운캐스팅이 확실히 성공할 상황에서만 사용하라. 이 연산자는 잘못된 클래스 타입으로 다운캐스팅을 시도할 경우 런타임 에러를 트리거한다.

이하의 예시는 library 내의 각각의 MediaItem을 iterate하며 각 아이템에 대해 적절한 description을 출력한다. 이를 위해 각 아이템에 접근해 해당 아이템이 MediaItem임을 넘어 Movie이거나 Song인지를 확인해야 한다. 이 과정을 거쳐야만 description에 사용할 Movie나 Song의 director 또는 artist 프라퍼티에 접근할 수 있다.

다음의 예시에서 배열 내의 각 아이템은 Movie일 수도 있고 Song일 수도 있다. 각 아이템에 대해 실제 어떤 클래스가 사용될지를 미리 알 수는 없기 때문에 다운캐스팅을 시도함에 있어 conditional form의 타입 캐스트 연산자인 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

예시는 현재 아이템을 Movie로 다운캐스팅하는 것으로 시작한다. 아이템이 MediaItem의 인스턴스이기 때문에 Movie일 수 있으며, 마찬가지로 Song일 수도 있고 혹은 그저 MediaItem이기만 할 수도 있다. 이러한 불확실성 때문에 타입 캐스트 연산자 as?는 서브클래스로 다운캐스팅을 시도할 때 옵셔널 값을 리턴한다. item as? Movie의 결과는 Movie? 즉 optional Movie가 된다.

library 배열 내에서 Song 인스턴스에 대해 Movie로의 다운캐스팅을 실시한다면 이는 실패할 것이다. 이에 대응하기 위해 위의 예시에서는 옵셔널 Movie가 실제로 값을 가지고 있는지, 즉 다운캐스팅이 성공할지를 확인하기 위해 옵셔널 바인딩을 사용한다. 이 경우 옵셔널 바인딩은 if let movie = item as? Movie라고 적으며, 이는 다음과 같이 해석할 수 있다: "해당 아이템을 Movie로 접근해봐라. 성공할 시 리턴된 옵셔널 Movie에 저장된 값을 movie라는 일시적인 상수에 할당하라."

다운캐스팅이 성공한다면 name, director 등의 movie의 프라퍼티들은 해당 Movie 인스턴스의 description을 출력하는데 사용된다. 이와 비슷한 원리가 Song 인스턴스를 확인하는 데도 사용되며, library에서 Song이 발견될 때마다 artist name을 포함한 적절한 description을 출력한다.

주의할 점은, 캐스팅이 인스턴스 자체를 수정하거나 그 값을 바꾸지는 않는다는 것이다. 인스턴스 그 자체는 똑같으나, 캐스팅된 타입의 인스턴스로서 취급되고 접근될 뿐이다.

 

4. Any와 AnyObject로의 타입 캐스팅

 

스위프트는 불특정한 타입을 다루기 위한 두 개의 특별한 타입을 제공한다.

  • Any: function 타입을 포함한 모든 타입을 대변한다.
  • AnyObject: 클래스 타입의 인스턴스를 대변한다.

Any와 AnyObject는 해당 타입들의 특징과 기능이 반드시 필요한 경우에만 사용하라. 가능하다면 타입은 구체적일 수록 좋다.

이하는 function 타입, nonclass 타입 등을 포함한 서로 다른 여러가지 타입이 혼합된 경우 Any를 사용하여 이를 처리하는 예시이다. 예시는 things라는 배열을 만드는데, 이는 Any 타입의 값들을 저장한다.

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)" })

things 배열은 두 개의 Int 값, 두 개의 Double 값, String 값 하나, (Double, Double)의 tuple 값 하나, "Ghostbusters"라는 영화와 String값을 취하며 String 값을 리턴하는 클로저 표현으로 이루어져 있다.

Any나 AnyObject로만 알려진 상수나 변수의 구체적인 타입을 알아내기 위해서 is나 as 패턴을 switch문의 case로 사용해줄 수 있다. 이하의 예시는 things 배열을 iterate하며 각 아이템의 타입을 switch문으로 판단한다. switch문의 case들은 자신과 매칭된 값을 특정한 타입의 상수로 바인딩하여 해당 값이 출력될 수 있도록 한다.

for thing in things {
    switch thing {
    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 movie as Movie:
        print("a movie called \(movie.name), dir. \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Michael"))
    default:
        print("something else")
    }
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called Ghostbusters, dir. Ivan Reitman
// Hello, Michael