(※ hackingWithSwift의 글을 번역한 것으로 아래 출처를 남겨두었습니다. 약간의 오역이 있을 수 있으니 지적해주시면 감사드리겠습니다.)
generic 타입의 파라미터에 default값을 사용할 수 있도록 확장되었다. 사소해보일지 몰라도 굉장히 큰 변화인데, generic 타입이나 generic함수를 정의할 때 default expression에 구체적인 타입을 제공할 수 있기 때문이다. (예전이라면 Swift는 이에 대해 컴파일 에러를 발생시켰을 것이다.)
func drawLotto1<T: Sequence>(from options: T, count: Int = 7) -> [T.Element] {
Array(options.shuffled().prefix(count))
}
// T라는 generic에 어떠한 타입도 들어갈 수 있다.
print(drawLotto1(from: 1...49)) // 49개의 숫자를 섞은 후 중에 7개 선택
print(drawLotto1(from: ["Jenny", "Trixie", "Cynthia"], count: 2)) // 3명의 이름 중 랜덤하게 2개를 뽑음
여기까지는 우리가 평소 사용하던 generic 타입의 파라미터를 사용하는 default expression이다. 업데이트된 Swift에서는 이제 다음과 같이 사용가능하다.
func drawLotto2<T: Sequence>(from options: T = 1...49, count: Int = 7) -> [T.Element] {
Array(options.shuffled().prefix(count))
}
// T에서 default type으로 Int를 지정했음에도 컴파일 에러 안 남.
print(drawLotto2(from: ["Jenny", "Trixie", "Cynthia"], count: 2)) // 3명의 이름 중 랜덤하게 2개를 뽑음
print(drawLotto2())
main.swift와 같은 최상위단의 코드에서도 동시성 코드를 작성할 수 있고, 이것은 굉장히 큰 변화이다. 이전에는 비동기 함수인 main()을 가지고 있는 @main Struct를 생성해야했으나, 이제는 그럴 필요가 없다.
다음 코드를 main.swift인 Top-level단에 추가해도 에러없이 사용할 수 있다.
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
print("Found \(readings.count) temperature readings")
generic 타입이 사용되는 것처럼 'some'이라는 Opaque 타입(불분명한 타입으로 타입 정보를 숨김)의 사용성도 확장하였다.
//Swift5.7이전에는 Extension혹은 Generic으로 선언하였다.
func isSortedByGeneric<T: Comparable>(array: [T]) -> Bool {
array == array.sorted()
}
extension Array where Element: Comparable {
func isSorted() -> Bool {
self = self.sorted()
}
}
//Swift5.7이후에는 다음과 같이 syntactic sugar 가능
func isSortedByOpaqueType(array: [some Comparable]) -> Bool {
array == array.sorted()
}
opaque type을 사용할 수 있는 범위가 확장되었다.
예를 들면,
func showUserDetails() -> (some Equatable, some Equatable) {
(Text("Username"), Text("@twostraws"))
}
func createUser() -> [some View] {
let usernames = ["@frankefoster", "@mikaela__caron", "@museumshuffle"]
return usernames.map(Text.init)
}
func createDiceRoll() -> () -> some View {
return {
let diceRoll = Int.random(in: 1...6)
return Text(String(diceRoll))
}
}
Swift는 이번에 Self를 가지거나 associated type 조건을 가지는 프로토콜에 대한 제약을 확실하게 풀었다. 특정 프로퍼티나 메소드가 그들이 할 수 있는 한계에서 벗어난 모델로 변화하였다.
단순히 말해서, 이제 이런 코드도 legal하다(에러를 내지 않고 허용한다).
let firstName: any Equatable = "Paul"
let lastName: any Equatable = "Hudson"
Equatable은 원래 Self 조건을 필요로 하는 프로토콜이다. 즉, 이것을 채택한 특정 타입과 관련있는 기능을 제공한다. 예를 들면, Int타입도 Equatable을 따르고 있는데, 4==4 라고 쓰게 되면 두 정수를 받아들이는 함수가 실행되고 동일하면 true를 리턴한다.
Swift에서 func ==(first: Int, second: Int) -> Bool을 사용할 수 있지만 이것을 매번 Boolean, String, Array 등등에 매번 정의하기 힘들다. 대신에, Equatable이라는 프로토콜은 해당 함수가 실행될 수 있어야하는 조건이 있다. "두 개의 동일한 타입을 받아들이고, 이 둘이 같은지를 구별할 수 있어야 한다."
이러한 비슷한 문제들을 피하기 위해, Swift 5.7이전의 Self가 매번 등장할 때마다 컴파일러는 다음과 같은 코드에 대해 에러를 냈다. 그런데 앞으로의 Swift5.7에서는 이러한 코드도 허용되며, any Equatable을 사용함으로써 특정 타입을 숨기는 것이다.
let tvShow: [any Equatable] = ["Brooklyn", 99]
그럼에도 우리는 다음과 같이 런타임 내에 데이터를 체크할 수 있다.
for parts in tvShow {
if let item = item as? String {
print("Found string: \(item)")
} else if let item = item as? Int {
print("Found integer: \(item)")
}
}
if let firstName = firstName as? String, let lastName = lastName as? String {
print(firstName == lastName)
}
우리가 이러한 변화에 대해 이해해야하는 중요한 포인트는 이전에는 내부 타입을 알지 못하고는 사용할 수 없었는데 이제는 protocol을 좀 더 자유롭게 쓸 수 있다는 점이다. 그래서 우리는 시퀀스에 있는 요소들이 Identifiable프로토콜을 따르는지 확인할 수 있는 코드를 쓸 수 있게 되었다.
func canBeIdentified(_ input: any Sequence) -> Bool {
input.allSatisfy { $0 is any Identifiable }
}
특정한 Associated type을 갖는 프로토콜을 언급할 때 더 새롭고 간단한 문법을 제공한다.
예를 들어, 이러한 각기 다른 데이터 형태들을 캐쉬할 수 있는 코드가 있다.
protocol Cache<Content> {
associatedtype Content
var items: [Content] { get set }
init(items: [Content])
mutating func add(item: Content)
}
이 코드를 보면 Cache<Content>라는 프로토콜이 프로토콜과 Generic type 둘 다인 것처럼 보인다. 해당 프로토콜을 채택할 때 반드시 써야하는 것들이 정의되어 있으며, 부등호(<,>)안에 특정 타입이 들어있기 때문이다. 여기서 부등호 사이에 들어간 Content자리에 있는 것을 swift에서 primary associated type 이라고 하며associated type이라고 해서 모두 저 자리에 선언될 수 없으므로 중요하다.
코드를 호출할 때 특별히 중요시 해야하는 것들이 primary associated type이 될 수 있는데, 예를 들면 dictionary 타입의 key혹은 value, Identifiable 프로토콜을 따르는 identifier타입이 해당된다.
이번 우리 코드의 Content는(String, Images, Users 등등)은 primary associated type인 것이다.
예전에는 프로토콜을 사용할 때, 먼저 우리가 어떤 데이터를 사용할 것인지를 정의한 후(아래 코드에서 File 구조체에 해당)에 프로토콜을 따르는 구체적인 타입을 정의하였다. (아래 코드에 LocalFileCache 구조체에 해당)
struct File {
let name: String
}
struct LocalFileCache: Cache {
var items = [File]()
mutating func add(item: File) {
items.append(item)
}
}
이제는 좀 더 똑똑하게 cache를 만드는 것에 있어서 바로 특정한 타입을 생성할 수 있다.
func loadDefaultCache() -> LocalFileCache {
LocalFileCache(items: [])
}
그렇지만 아주 가끔 다음과 같이 특정한 타입을 숨기고 싶을 때도 있다. some Cache라고 사용하면 우리가 리턴할 타입이 무엇인지 바꿀 수 있는 유연함이 있다. 그렇지만 이번 변화에서는 구체적인 타입으로 절대적으로 단정짓는 것과 opaque type을 사용해서 모호한 것 사이의 중간 단계를 제공하는 것에 의의가 있다.
func loadDefaultCacheOld() -> some Cache {
LocalFileCache(items: [])
}
그러면 우리는 다음과 같이 정의할 수 있게 된다. 이렇게 함으로써 나중에 Cache프로토콜을 따르는 다른 타입으로 변경할 수 있을 뿐만 아니라 어떤 타입으로 결정되든지 간에 내부적으로 파일을 저장한다는 것은 명확히 보여준다.
또한, 이러한 문법은 Extension과 Generic에도 적용될 수 있다.
func loadDefaultCacheNew() -> some Cache<File> {
LocalFileCache(items: [])
}
extension Cache<File> {
func clean() {
print("Deleting all cached files…")
}
}
func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C {
print("Copying all files into a new location…")
// now send back a new cache with items from both other caches
return C(items: lhs.items + rhs.items)
}
이러한 변화를 통해 Sequence, Collection과 같은 standard library primary associated type을 적용할 수 있기 때문에 굉장히 유용하다.
끝으로, 프로토콜에 대한 제약을 해제하는 것과 맞물려서 any Sequence<String>과 같이 코드를 쓰는 것이 가능해졌다.
(출처: https://www.hackingwithswift.com/articles/249/whats-new-in-swift-5-7)