반응형

[2019.03.07]

원문 : https://www.hackingwithswift.com/articles/126/whats-new-in-swift-5-0
참고 : https://www.whatsnewinswift.com

What’s New in Swift 5.0

Swift 5.0은 Swift의 다음 주요 릴리즈 이고, 마침내(at long last) ABI(Application Binary Interface: 앱과 라이브러리에서 필요한 저수준의 인터페이스를 정의) 안정성을 제공합니다. 그게 다가 아닙니다: 중요한(key) 몇가지의 새로운 기능은 이미 구현되어있으며, 원시(raw) 문자열, 미래의 enum case, Result 타입, 정수 배수에 대한 검사등을 포함합니다.

출시 전에 Swift 5.0을 사용해보고 싶으면, 최신 Swift 개발 스냅샷을 다운로드하며, 현재 Xcode 버젼에서 활성화해서, 아래 예제들을 따라하세요. 또한 Swift 5.0은 Xcode 10.2에 제공되며, 아직 베타버젼입니다 - https://developer.apple.com/download/를 방문해서 다운로드 합니다.

표준 Result 타입(A standard Result type)

SE-0235 표준 라이브러리에서 Result 타입을 도입했으며, 비동기 API 처럼 복잡한 코드에서 오류를 처리하는 더 단순하고 명확한 방법을 제공합니다.

Swift의 Result 타입은 2가지 케이스(cases)를 가진 열거형(enum)으로 구현되었습니다: success와 failure. 둘 모두 제네릭(generics)을 사용해서 구현되었으므로 사용자가 선택한 연관된 값을 가질수 있지만, failure는 Swift의 Error 타입을 준수해야만 합니다.

Result를 설명하기 위해, 사용자가 읽지 않은 메시지가 얼마나 많이 있는지 파악하기 위해 서버에 연결하는 함수를 작성할 수 있습니다. 예제 코드에서 하나의 가능한 오류만을 가질 것이며, 요청한 URL 문자열이 유효한 URL이 아니라는 것입니다.

enum NetworkError: Error {
    case badURL
}

가져오는 함수는 첫번째 매개변수로 URL 문자열을 사용할 것이고, 두번째 매개변수로 완료 처리기(completion handler)를 사용합니다. 완료 처리기(completion handler)는 성공한 경우에 정수를 저장하고, 실패한 경우에 NetworkError 종류가 되는, Result를 받아들일 것입니다. 우리는 여기에서 실제로 서버에 연결하지는 않을 것이지만, 적어도 완료 처리기를 사용해서 비동기 코드를 시뮬레이션 할 수 있습니다.

코드는 다음과 같습니다.

import Foundation

func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }

    // complicated networking code here
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}

이 코드를 사용하기 위해서, 다음과 같이, Result 값을 검새해서 호출이 성공했거나 실패햇는지 확인해야 합니다.

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}

Result를 여러분의 코드에 사용하기 전에 알아야 할 것이 3개가 더 있습니다.

첫번째, Result는 성공한 값이 있으면 반환하거나, 그렇지 않으면 오류를 던지는(throws) get() 메소드를 가지고 있습니다. 이는 다음과 같이 Result를 규직적인(regular) 오류를 던지는(throwing) 호출로 변환할 수 있도록 허용합니다.

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\(count) unread messages.")
    }
}

두번째, Result는 오류를 던지는 클로져(throwing closure)를 받아들이는 초기화를 가지고 있습니다: 클로져는 success 케이스(case)에서 사용된 값을 성공적으로 반환하며, 그렇지 않으면, failure 케이스(case)에서 오류를 던집니다.

예를들어:

let result = Result { try String(contentsOfFile: someFile) }

세번째, 여러분이 만든 특정 오류 열거형을 사용하는 대신에, 일반 Error 프로토콜을 사용할 수도 있습니다. 사실상, Swift 발전(evolution) 제안(proposal)에서 Result의 대부분 사용은 Error 타입 인자로 Swift.Error을 사용할 것으로 예상됩니다라고 말합니다.

따라서, Result<Int, NetworkError>을 사용하는 대신에 Result<Int, Error>을 사용할 수 있습니다. 비록 이는 입력된 오류의 안전성을 잃는 것을 의미하지만, 다양한 오류 열거형(enums)을 던지는 능력을 얻게됩니다 - 선호하는 코딩 스타일에 따라 다릅니다.

원시 문자열(Raw strings)

SE-0200 원시 문자열을 만드는 기능이 추가됐으며, 백슬래쉬(\)와 따옴표(")는 탈출(escapes) 문자나 문자열 종결자(terminators) 보다는 리터럴 기호로 해석됩니다(interpreted). 이는 사용하기 쉬워지지만, 특히 정규 표현식에서 유리할 것입니다.

원시(raw) 문자열을 사용하기 위해, 다음과 같이 문자열 앞에 하나 이상의 #기호를 배치합니다.

let rain = #"The "rain" in "Spain" falls mainly on the Spaniards."#

문자열의 시작과 끝에 있는 # 기호는 문자열 구분문자(delimiter)의 일부가 되므로, Swift는 문자열을 끝내기 보다는 rain과 Spain 주변을 감싼 독립적인 따옴표(")를 리터럴 따옴표로 취급해야 한다고 이해합니다.

원시 문자열은 백슬러쉬를 사용할 수 있씁니다.

let keypaths = #"Swift keypaths such as \Person.name hold uninvoked references to properties."#

백슬래쉬(\)를 탈출(escape) 문자보다는 문자열에 있는 리터럴 문자로 처리합니다. 이는 문자열 끼워넣기(interpolation) 작업이 다르게 동작한다는 것을 의미합니다.

let answer = 42
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)."#

문자열 끼워넣기(interpolation)을 사용하기 위해 \#(answer)을 사용한 것을 주의합니다 - 정규 \(answer)은 문자열에서 문자들로 처리되므로, 원시 문자열에서 문자열 끼워넣기를 원할때에는 추가적으로 #을 추가해야 합니다.

Swift의 원시 문자열 중에 흥미로운 기능 중 하나는, 혹시 모르니까 하나 이상을 사용할 수 있기 때문에, 시작과 끝에 해쉬 기호(#)를 사용하는 것입니다. 이는 정말 극히 드문 경우이기 때문에, 여기에서 좋은 예제를 제공하는 것은 어렵지만, 다음 문자열을 고려해보세요: My dog said woof#gooddog. 해쉬 앞에 공백이 없기 때문에. Swift는 #을 보자마자 문자열 종결자(terminator)로 해석할 것입니다. 이 상황에서 구분기호를 다음과 같이 #"에서##"로 변경해야 합니다.

let str = ##"My dog said "woof"#gooddog"##

끝에 있는 해쉬의 갯수가 시작에 있는 갯수와 반드시 같아야 한다는 것을 주의합니다.

원시 문자열은 Swift의 여러줄 문자열 시스템과 완벽하게 호환합니다 - 다음과 같이 #"""로 시작해서, """#으로 끝납니다.

let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
"""#

백슬러쉬를 많이 사용하지 않고 할수 있는 것은 정규 표현식에서 특히 도움이 될것입니다. 예를들어, 다음과 같이 사용된 \Person.name와 같은 경로(keypath)를 찾기 위해서 간단한 정규식(regex)를 작성합니다.

let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"

원시 문자열 덕분에 우리는 절반의 백슬래쉬 갯수로 동일하게 작성할 수 있습니다.

let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#

정규 표현식에서 사용하기 때문에, 여전히 약간 필요합니다.

문자열 끼워넣기를 사용자정의하기(Customizing string interpolation)

SE-0228 Swift의 문자열 끼워넣기 시스템을 극적으로 개선함으로써 더 효율적이고 유연하게 사용할 수 있고, 이전에는 불가능했던 완전히 새로운 범위의 기능을 만들어 내고 있습니다.

가장 기본적인 형식으로, 새로운 문자열 끼워넣기 시스템은 문자열에서 객체를 어떻게 나타나는지 제어합니다. Swift는 디버깅에 도움이 되는 구조체에 대해 구조체 이름 뒤에 모든 프로퍼티들이 인쇄하는, 기본 동작을 가지고 있습니다. 하지만 클래스로 작업(이런 동작이 없음)을 하거나, 사용자에게 출력물을 포멧화하길 원하는 경우에, 새로운 문자열 끼워넣기 시스템을 사용할 수 있습니다.

예를들어, 다음과 같은 구조체를 가지고 있는 경우입니다.

struct User {
    var name: String
    var age: Int
}

사용자들을 깔끔하게 출력하는 문자열 끼웒기를 추가하길 원하는 경우에, 우리는 String.StringInterpolation에 새로운 appendInterpolation()메소드를 확장에 추가할 것입니다. Swift는 이들중 몇가지를 이미 내장(built in)하고 있고, 사용자는 끼워넣기(interpolation) 타입을 사용합니다 - 이 경우에 User는 어떤 메소드를 호출해야 하는지를 파악합니다.

이 경우에, 우리는 단일 문자열 안에 사용자의 이름과 나이를 끼워넣기 하는 것을 추가하고나서, 다음과 같이 문자열을 추가하기 위해서 내장 메소드중 하나인 appendInterpolation() 메소드를 호출할 것입니다.

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: User) {
        appendInterpolation("My name is \(value.name) and I'm \(value.age)")
    }
}

이제 사용자를 만들고 데이터를 출력할 수 있습니다.

let user = User(name: "Guybrush Threepwood", age: 33)
print("User details: \(user)")

User details: My name is Guybrush Threewood and I’m 33이 출력될 것인데 반해서(whereas), 사용자정의 문자열 끼워넣기를 사용하면 **User details: User(name: Guybrush Threepwood, age: 33) **이 출력됩니다. 물론, 이 기능은 CustomStringConvertible 프로토콜을 구현하는 것과 다르지 않으므로, 더 고급인 사용법을 사용합시다.

사용자정의 끼워넣기 메소드는 필요에 따라 라벨되어 있거나(labeled) 라벨되어있지 않는(unlabeled) 많은 매개변수를 가질수 있습니다. 예를들어, 다음과 같이 다양한 스타일을 사용해서 숫자들을 출력하는 끼워넣기를 추가할 수 있습니다.

extension String.StringInterpolation {
    mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
        let formatter = NumberFormatter()
        formatter.numberStyle = style

        if let result = formatter.string(from: number as NSNumber) {
            appendLiteral(result)
        }
    }
}

NumberFormatter 클래스는 통화($72.83), 서수(1st, 12th), 철자(five, forty-three)를 포함한 많은 스타일을 가지고 있습니다. 따라서, 다음과 같이 우리는 무작위 숫자를 만들고 철자를 문자열로 가져와야 합니다.

let number = Int.random(in: 0...100)
let lucky = "The lucky number this week is \(number, style: .spellOut)."
print(lucky)

appendLiteral()을 필요한 만큼 여러번 호출할 수 있거나 필요하지 않는 경우에는 전혀 호출하지 않을 수 있습니다. 예를들어, 다음과 같이 문자열을 여러번 반복하도록 문자열 끼워넣기를 추가할 수 있습니다.

extension String.StringInterpolation {
    mutating func appendInterpolation(repeat str: String, _ count: Int) {
        for _ in 0 ..< count {
            appendLiteral(str)
        }
    }
}

print("Baby shark \(repeat: "doo ", 6)")

그리고, 이는 단지 정규 메소드처럼, Swift의 모든 기능을 사용할 수 있습니다. 예를 들어, 우리는 문자열 배열을 함께 결합한 끼워넣기를 추가할 수도 있지만, 배열이 비어있는 경우에, 문자열을 반환하는 클로져를 실행합니다.

extension String.StringInterpolation {
    mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
        if values.count == 0 {
            appendLiteral(defaultValue())
        } else {
            appendLiteral(values.joined(separator: ", "))
        }
    }
}

let names = ["Harry", "Ron", "Hermione"]
print("List of students: \(names, empty: "No one").")

@autoclosure를 사용하는 것은 기본 값을 위해 간단한 값을 사용하거나 복잡한 함수를 호출할 수 있다는 것을 의미하지만, values.count가 0이 아니면 어떤 작업도 하지 않을 것입니다.

ExpressibleByStringLiteral과 ExpressibleByStringInterpolation 프로토콜의 조합으로 이제 문자열 끼워넣기를 사용해서 전체 타입을 만들수 있고, CustomStringConvertible를 추가하는 경우에 이 타입을 우리가 원하는 대로 문자열로 출력할 수도 있습니다.

이 작업을 하기 위해서, 몇가지 구체적인 기준(criteria)을 충족(fulfill)해야 합니다:

  • 어떤 타입이든 우리는 ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible를 준수하도록 만들어야 합니다. 후자는 출력되는 타입의 방식을 사용자정의 하는것을 원할때만 필요합니다.
  • 타입 내부에는(indside) StringInterpolationProtocol을 준수하는 StringInterpolation이라는 중첩된(nested) 구조체가 되야합니다.
  • 중첩된 구조체는 데이터의 양을 대략적으로 알려주는 2개의 정수를 받는 초기화가 필요합니다.
  • 또한 하나 이상의 appendInterpolation() 메소드 뿐만아니라 appendLiteral() 메소드 구현이 필요합니다.
  • 주요 타입은 문자열 리터럴과 문자열 끼워넣기로 만들수 있는 초기화 2개가 필요합니다.

우리는 다양한 요소들로부터 HTML을 만들수 있는 예제 타입으로 모든 것을 함께 담을 수 있습니다. 중첩된 StringInterpolation 구조체 내부의 scratchpad는 문자열이 될것입니다: 새로운 리터럴이나 끼워넣기가 추가될때마다, 우리는 그 문자열에 추가할 것입니다. 정확히 무슨 일이 일어나는지 알수 있도록, 다양한 메소드 내부에 print() 호출을 추가했습니다.

다음은 그 코드 입니다.

struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
    struct StringInterpolation: StringInterpolationProtocol {
        // start with an empty string
        var output = ""

        // allocate enough space to hold twice the amount of literal text
        init(literalCapacity: Int, interpolationCount: Int) {
            output.reserveCapacity(literalCapacity * 2)
        }

        // a hard-coded piece of text – just add it
        mutating func appendLiteral(_ literal: String) {
            print("Appending \(literal)")
            output.append(literal)
        }

        // a Twitter username – add it as a link
        mutating func appendInterpolation(twitter: String) {
            print("Appending \(twitter)")
            output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
        }

        // an email address – add it using mailto
        mutating func appendInterpolation(email: String) {
            print("Appending \(email)")
            output.append("<a href=\"mailto:\(email)\">\(email)</a>")
        }
    }

    // the finished text for this whole component
    let description: String

    // create an instance from a literal string
    init(stringLiteral value: String) {
        description = value
    }

    // create an instance from an interpolated string
    init(stringInterpolation: StringInterpolation) {
        description = stringInterpolation.output
    }
}

우리는 이제 다음과 같이 문자열 끼워넣기를 사용해서 HTMLComponent의 인스턴스를 만들고 사용할 수 있습니다.

let text: HTMLComponent = "You should follow me on Twitter \(twitter: "twostraws"), or you can email me at \(email: "paul@hackingwithswift.com")."
 print(text)

내부에 흩어져 있는 print() 호출 덕분에, 문자열 끼워넣기 작업이 어떻게 동작하는지 정확히 알수 있습니다. “Appending You should follow me on Twitter”, “Appending twostraws”, “Appending , or you can email me at “, “Appending paul@hackingwithswift.com”를 보게될 것이고, 마지막에는 “Appending .”을 보게 될 것입니다 - 각 부분은 메소드를 호출하고 우리의 문자열에 추가됩니다.

동적으로 호출가능한 타입(Dynamically callable types)

SE-0216 Swift에 새로 추가한 @dynamicCallable 속성은, 타입을 직접 호출할 수 있도록 표시합니다. 이는 어떤 종류의 컴파일러 마술보다는 syntactic sugar(쉽게 읽고 쓰게 해주는 코드 ex.[1, 2, 3])으로, 코드를 효과적으로 변형시킵니다.

let result = random(numberOfZeroes: 3)

다음과 같이 변형:

let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])

@dynamicCallable은 @dynamicMemberLookup 의 자연스러운 확장이고, 동일한 목적을 가집니다: Swift 코드를 파이썬(Python)이나 자바스크립트(JavaScript)와 같은 동적 언어와 함께 작업하기 쉽게 만듭니다.

자신의 타입에 이 기능을 추가하기 위해서, @dynamicCallable 속성과 다음 메소드 중 하나 또는 둘 모두를 추가해야 합니다.

func dynamicallyCall(withArguments args: [Int]) -> Double

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double

이것들 중 첫번째는 매개변수 라벨(label) 없이 타입을 호출할때 사용되고(에를들어 a(b, c)), 두번째는 라벨을 제공할때 사용됩니다(a(b: cat, c: dog)).

@dynamicCallable은 데이터 타입을 메소드가 받고 반환하는지에 대해 정말 유연하며, 고급 사용법에 대해 여유공간(wiggle room)을 가지고 있는 동안에 Swift의 모든 타입을 안전(safety)하도록 해줍니다. 따라서, 첫번째 메소드(매개변수 라벨이 없음)에 대해서, 배열(array), 배열 조각(array slices), 세트(set) 처럼, ExpressibleByArrayLiteral을 준수하는 모든것을 사용할 수 있고, 두번째 메소드(매개변수 라벨 있음)에 대해서, 딕셔너리(dictionary)와 키-값 쌍(key value pairs) 처럼, ExpressibleByDictionaryLiteral을 준수하는 모든 것을 사용할 수 있습니다.

  • 주의: 이전에 KeyValuePairs를 사용해보지 않은 경우에, @dynamicCallable이 매우 유용하기때문에 지금이 배울수 있는 좋은 기회입니다. 자세한것은 What are KeyValuePairs?를 보세요.

다양한 입력을 허용할 뿐만아니라, 다양한 출력에 대해 많은 과부하를 제공할 수도 있습니다 - 하나는 문자열을 반환하고, 하나는 정수를 반환하고, 등등.. Swift가 어떤것을 사용할지 결정할 수 있는 한, 원하는 모든 것을 섞고 일치시킬수 있습니다.

예제를 보겠습니다. 우선, 여기에 0과 전달된 입력에 따라 특정 최대값 사이의 숫자를 생성하는 RandomNumberGenerator 구조체가 있습니다.

struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}

@dynamicCallable로 바꾸기 위해 다음과 같이 작성합니다.

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

이 메소드는 여러개의 매개변수 또는 어쩌면 0으로 호출할 수 있으며, 우리는 첫번째 값을 주의해서 읽고 적절한 기본 값을 만들기 위해 nil 결합(coalescing)을 사용합니다.

우리는 이제 RandomNumberGenerator의 인스턴스를 만들고 함수 처럼 호출할 수 있습니다.

let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)

dynamicallyCall(withArguments:)를 사용한 경우,
(또는 둘 다 하나의 타입을 가질수 있기 때문에, 동시에 사용한 경우) 다음과 같이 작성했을 것입니다.

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args[0])
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

let random = RandomNumberGenerator()
let result = random(0)

@dynamicCallable을 사용할때 알아야할 몇가지 중요한 규칙들이 있습니다.

  • 구조체(structs), 열거형(enums), 프로토콜(protocols)에 적용할 수 있습니다.
  • withKeywordArguments:을 구현하고 withArguments:를 구현하지 않는 경우에, 여러분의 타입은 여전히 매개변수 라벨 없이 호출될 수 있습니다 - 키에 대해 빈 문자열을 얻게될 것입니다.
  • withKeywordArguments: 또는 withArguments:의 구현이 오류를 던치는 것으로(throwing) 표시하는 경우, 그 타입을 호출하는 것도 오류를 던지게(throwing) 될 것입니다.
  • 확장에 @dynamicCallable을 추가할 수 없으며, 타입의 기본 정의에만 추가할 수 있습니다.
  • 여러분의 타입에 여전히 다른 메소드와 프로퍼티를 추가할 수 있고, 정상적으로 사용할 수 있습니다.

아마도 더 중요한 것은, 메소드 해결책(resolution)에 대한 지원은 없으며, 우리가 타입에서 특정 메소드를 호출(random.generate(numberOfZeroes: 5))하는 것보다는 타입을 직접 호출(random(numberOfZeroes: 5))해야만 하는 것을 의미합니다. 이미 다음과 같이 메소드 서명(signature)을 사용해서 후자를 추가하는 것에 대해서 논의하고 있습니다.

func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)

이는 미래에 Swift 버젼에서 가능해지는 경우, 테스트 목업을 위한 매우 흥미로운 가능성이 열릴것입니다.

그 동안에는(in the meantime), @dynamicCallable이 널리 사용되지는 않을 것이지만, 파이썬, 자바스크립트, 다른 언어와 상호작용하길 원하는 소수의 사람들에게는 매우 중요합니다.

나중에 열거형 케이스 처리하기(Handling future enum cases)

SE-0192 고정된 열거형과 나중에 변경할 수 있는 열거형을 구별(distinguish)할 수 있는 기능이 추가되었습니다.

Swift의 보안 기능중 하나는 모든 switch 구문들이 완벽(exhaustive)해야 하는 것입니다 - 모든 케이스에 대해서 처리해야 합니다. 안전성(safety) 관점에서는 잘 동작하지만, 나중에 새로운 케이스가 추가될때 호환성 문제가 발생합니다: 시스템 프레임워크가 
준비되지(catered) 않은 다른 것을 보내거나, 코드에 새로운 케이스가 추가되고 switch문이 더 이상 완벽하지 않기 때문에 컴파일이 중단될수 있습니다.

우리는 이제 @unknown 속성을 사용해서 미묘하게(subtly) 다른 2가지 시나리오를 구별(distinguish)할 수 있습니다: 개별적로 다루고 싶지 않기 때문에, 다른 모든 케이스에 대해 기본 케이스를 실행해야합니다.와 모든 케이스를 개별적으로 처리하길 원하지만, 나중에 오류가 발생하기보다는 이 기능을 사용합니다.

여기에 열거형 예제가 있습니다.

enum PasswordError: Error {
    case short
    case obvious
    case simple
}

우리는 switch 블럭을 사용해서 각 케이스를 처리하는 코드를 작성할 수 있습니다.

func showOld(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    default:
        print("Your password was too simple.")
    }
}

짧고 명백한(obvious) 암호에 대해서 두가지 명시적인 케이스를 사용하지만, 3번째 케이스는 기본(default) 블록으로 묶었습니다(bundles).

이제, 나중에 우리가 old 열거형에 대한 새로운 케이스를 추가하는 경우에, 이전에 사용된 암호의 경우에, 메시지가 정말 말이 안되더라도 default가 자동으로 호출될 것입니다 - 암호가 너무 단순하지 않을 수도 있습니다.

이는 기술적으로 올바르기(가장 정확함) 때문에, Swift는 이 코드에 대해서 경고할 수 없으며, 이런 실수는 쉽게 놓칠 수 있습니다. 다행히, 새로운 @unknown 속성은 이를 완벽하게 고칩니다 - 그것은 default 케이스에만 사용할 수 있고, 나중에 새로운 케이스가 오면 실행되도록 설계되어 있습니다.

예를 들어:

func showNew(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    @unknown default:
        print("Your password wasn't suitable.")
    }
}

이 코드는 switch 블럭이 더 이상 완벽(exhaustive)하지 않기 때문에, 경고가 발생할 것입니다 - Swift는 각 케이스를 명시적으로 처리하길 원합니다. 다행히 단지 경고(warning)일 뿐이며, 이 속성을 매우 유용하게 만듭니다: 프레임워크가 나중에 새로운 케이스를 추가하는 경우에 여러분은 경고를 받을 것이지만, 소스 코드가 망가지지는 않습니다.

try? 로부터 중첩된 옵셔널 결과를 병합하기(Flattening nested oprionals resulting from try?)

SE-0230 try? 방법을 수정해서 중첩된 옵셔널 동작은 정규 옵셔널이 되도록 병합(flattened)합니다. 이것는 옵셔널 체이닝과 조건부 타입형변환과 같은 방법으로 동작하며, 2가지 모두 이전 Swift 버젼에서 옵셔널을 병합합니다.

여기에 변경하는 것을 보여주는 실제(practical) 예제가 있습니다.

struct User {
    var id: Int

    init?(id: Int) {
        if id < 1 {
            return nil
        }

        self.id = id
    }

    func getMessages() throws -> String {
        // complicated code here
        return "No messages"
    }
}

let user = User(id: 1)
let messages = try? user?.getMessages()

User 구조체는 유효한 ID를 가진 사용자를 만들길 원하기 때문에, 실패가능한(failable) 초기화를 가지고 있습니다. getMessages() 메소드는 이론적으로 사용자에 대한 모든 메시지 목록을 얻기 위해 어떤 복잡한 종류의 코드를 포함할 것이므로, throws로 표시됩니다; 코드가 컴파일 되도록 고정된 문자열을 반환하게 만들었습니다.

핵심이 되는 곳은 마지막입니다: 사용자가 옵셔널이기 때문에 옵셔널 체이닝을 사용하고, getMessage()는 옵셔널로 오류를 던지는 메소드로 변환하기 위해서 try?를 사용해서 오류를 던질수 있기때문에, 중첩된 옵셔널로 끝납니다. Swift 4.2 이전에는 message를 String??로 만들것입니다 - 옵셔널 옵셔널 문자열 - 하지만 Swift 5.0 이후부터는 이미 옵셔널인 경우에, try?은 옵셔널로 값을 래핑(wrap)하지 않으며, 따라서 messages는 단지 String?이 될 것입니다.

새로운 동작은 옵셔널 체이닝과 조건부 타입형변환의 기존 동작과 일치합니다. 즉, 원하는 경우, 코드 1줄에서 옵셔널 체이닝을 12번(dozen times) 사용할 수 있지만, 12개의 중첩된 옵셔널로 끝나지는 않을 것입니다. 비슷하게, as?로 옵셔널 체이닝을 사용하는 경우에, 여전히 하나의 옵셔널 수준으로 끝날 것입니다. 그것이 일반적으로 원하는 것이기 때문입니다.

정수형 배수에 대한 검사하기(Checking for integer multiples)

SE-0225 정수형에 isMultiple(of:) 메소드를 추가했으며, 하나의 숫자가 다른 숫자의 배수인지 확인할 수 있는 나누기 나머지 연산자 %를 사용하는 것보다 훨씬 더 명확한 방법입니다.

예를 들어:

let rowNumber = 4

if rowNumber.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}

if rowNumber % 2 == 0을 사용해서 같은 검사를 할 수 있지만, 덜 명확하다는 것을 인정해야 합니다 - isMultiple(of:) 메소드는 Xcode의 코드완성 옵션 목록에 있을수 있으며, 발견할 수 있음을 의미합니다.

compactMapValues()로 딕셔너리 값을 변환하고 언래핑하기(Transforming and unwrapping dictionary values with compactMapValues())

SE-0218 딕셔너리에 새로운 compactMapValues()메소드를 추가했으며, 배열의 compactMap()의 기능(값을 변환하고, 결과를 언래핑해서 nil인것을 버립니다)과 딕셔너리의 mapValues() 메소드의 기능(키는 놔두지만 값들은 변경합니다)을 가져왔습니다.

예제에서 처럼, 여기에 경주하는 사람들의 딕셔너리가 있으며, 몇초 만에 끝내는지에 대한 시간이 있습니다. 끝내지 못한 한 사람은 DNF로 표시했습니다:

let times = [
    "Hudson": "38",
    "Clarke": "42",
    "Robinson": "35",
    "Hartis": "DNF"
]

이름과 시간을 정수형으로 DNF인 사람이 제거된 새로운 딕셔너리를 만들기 위해 compactMapValues()를 사용할 수 있습니다.

let finishers1 = times.compactMapValues { Int($0) }

또는, 다음과 같이 compactMapValues()에 직접 Int 초기화를 전달 할 수 있습니다

let finishers2 = times.compactMapValues(Int.init)

또한 다음과 같이 어떤 종류의 변환을 수행하지 않고서 옵셔널을 언래핑하고 nil 값을 버리기 위해 compactMapValues()를 사용할 수 있습니다.

let people = [
    "Paul": 38,
    "Sophie": 8,
    "Charlotte": 5,
    "William": nil
]

let knownAges = people.compactMapValues { $0 }

취소: 시퀀스에서 일치하는 항목의 갯수 세기(Withdrawn: Counting matching items in a sequence)

이 Swift 5.0 기능은 타입 검사기(type checker) 성능에 문제가 있기 때문에, 베타 테스트에서 제외되었습니다. 그 문제를 피하기 위해 아마 새로운 이름으로 Swift 5.1 에서 다시 추가되길 바랍니다.

SE-0220 한번에 filter()를 수행하고 갯수를 세는, 새로운 count(where:) 메소드를 도입했습니다. 이것은 바로 버려지는 배열의 생성을 줄여주고, 일반적인 문제에 대해 명확하고 간결한 해결책을 제공합니다.

이 예제는 테스트 결과의 배열을 만들고, 85 이상인 것의 갯수를 셉니다.

let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }

그리고 이것은 배열에서 얼마나 많은 이름이 Terry로 시작하는지 갯수르 셉니다.

let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }

이 메소드는 Sequence를 준수하는 모든 타입에서 사용가능 하므로 딕셔너리(dictionaries)나 세트(sets)에서도 사용할 수 있습니다.

반응형

'Swift > Tip' 카테고리의 다른 글

What’s new in Swift 5.3  (0) 2021.04.15
What’s new in Swift 5.2  (0) 2021.04.15
How to use opaque return types in Swift 5.1  (0) 2019.12.18
What’s new in Swift 5.1  (0) 2019.12.17
What’s New in Swift 4.2?  (0) 2018.06.14
Alamofire Tutorial: Getting Started  (2) 2018.05.05
Design Patterns by Tutorials: MVVM  (0) 2018.05.05
What’s New in Swift 4.1?  (2) 2018.04.03
Posted by 까칠코더
,