What’s new in Swift 5.1

Swift/Tip 2019. 12. 17. 15:55
반응형

Hacking with Swift 사이트의 강좌 번역본입니다.

[원문 : https://www.hackingwithswift.com/articles/182/whats-new-in-swift-5-1]

 

What’s new in Swift 5.1

불분명한 반환 타입(Opaque return typs), 암시적인 반환(implicit returns), universal self, 등등

함수 만들기(function builders)가 아직 Swift Evolution 과정을 거치지 않았기 때문에, 약간 불안정 하지만 Swift 5.1이 마침내 나왔고, 여전히, 모듈의 안정성(module stability) 형태의 또 다른 핵심 기능들을 제공받고 있으며, 내장된 Swift 컴파일러의 버젼을 걱정하지 않고 타사 라이브러리를 사용할 수 있습니다.

그렇습니다. Swift 5.0에서 제공한 ABI 안정성(stability)과 비슷한 이야기라는 것을 알지만, 미묘한 차이가 있습니다: ABI 안정성은 런타임으로 Swift 차이를 해결해주지만, 모듈(module) 안정성은 컴파일시(compile time)에 차이를 해결해 줍니다.

주요 이정표(milestone)과 함께 많은 중요한 언어 개선을 제공하고, 이 글에서는 이에 대해서 살펴보고 실제 예제를 볼수 있도록 코드 예제를 제공할 것입니다. 여러분은 이를 읽으면서, SwiftUI와 관련해서 얼마나 많은 기능들이 제공되는지 확인할수 있고, 사실상 Swift 5.1에서 도입된 새로운 기능 없이는 SwiftUI를 인식할 수 없다고 말하는 것이 옳다고 생각합니다. 

  • Swift 5.1에서의 새로운 기능 모두 모르는 경우, what’s new in Swift 5.0에서 부터 시작하세요.
  • SwiftUI By Example라는 SwiftUI 온라인 무료책이 있습니다 - 새로운 Swift 5.1 기능들을 보고 싶으면 바로 시작하세요.

동영상 강좌

합성된 멤버단위 초기화의 대대적인 개선(Massive improvements to synthesized memberwise initializers)

SE-0242로 Swift에서 가장 일반적으로 사용되는 기능중 하나가 크게 개선되었습니다.

Swift의 이전 버젼에서는, 다음과 같이, 멤버단위 초기화가 구조체의 프로퍼티와 일치하는 매개변수를 허용하도록 자동으로 만들어졌습니다. 

struct User {
    var name: String
    var loginCount: Int = 0
}

let piper = User(name: "Piper Chapman", loginCount: 0)

 

Swift 5.1에서는 멤버단위 초기화가 모든 프로퍼티에 대해 기본 매개변수로 사용하도록 향상되었습니다. User 구조체에서 loginCount에 기본 값으로 0을 주었으며, 이는 그것을 지정하거나 멤버단위 초기화로 남겨둘수 있는 것을 의미합니다.

let gloria = User(name: "Gloria Mendoza", loginCount: 0)
let suzanne = User(name: "Suzanne Warren")

 

반복되는 코드를 피할수 있고 항상 환영받습니다.

한줄로 표현하는 함수의 암시적인 반환 (Implicit return from single-expression functions)

SE-0255는 언어에서 작지만 중요한 모순을 제거했습니다 : 하나의 값을 반환하는 단일 표현(single-expression) 함수는 return 키워드를 제거할 수 있고 Swift는 암시적으로 알게 될것입니다.

Swift의 이전 버젼에서는, 하나의 값이 반환되는 한 줄(single-line) 클로져는 값을 반환하는 코드가 반드시 한 줄이어야 하기 때문에, return 키워드를 생략할 수 있었습니다. 따라서, 이러한 2개의 코드는 동일했습니다.

let doubled1 = [1, 2, 3].map { $0 * 2 }
let doubled2 = [1, 2, 3].map { return $0 * 2 }

 

Swift 5.1 에서는, 이러한 동작이 이제 함수로 확장되었습니다: 단일 표현(효과적으로 값을 평가하는 단일 코드 부분)을 포함하는 경우에 다음과 같이 return 키워드를 없앨수 있습니다. 

func double(_ number: Int) -> Int {
    number * 2
}

 

아마도 일부 사람들은 처음에 2가지 사용할수 있지만, 시간이 지나면 습관이 될것입니다. 

Universal Self

SE-0068은 Swift의 Self 사용하는 것을 확장함으로써 클래스, 구조체, 열거형 내부에서 사용될때 포함하는 타입을 참조합니다. 이는 동적인(dynamic) 타입, 정확한 타입이 무엇인지 런타임에 결정해야 하는 경우에 특히 유용합니다.

예를들어, 다음 코드를 고려해보세요.

class NetworkManager {
    class var maximumActiveRequests: Int {
        return 4
    }

    func printDebugData() {
        print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
    }
}

 

네트워크 관리에 대한 정적인 maximumActiveRequests 프로퍼티를 선언하고, 정적인 프로퍼티를 출력하기 위해 printDebugData() 메소드를 추가합니다. 해당 작업은 지금 잘 동작하지만, NetworkManager가 서브클래싱(subclassed)되면 상황이 더 복잡해 집니다.

class ThrottledNetworkManager: NetworkManager {
    override class var maximumActiveRequests: Int {
        return 1
    }
}

 

해당 서브클래스(subclass)는 maximumActiveRequests 를 변경하므로 한 번에 하나의 요청만 허용하지만, printDebugData()을 호출하는 경우에 부모 클래스로부터 값을 출력합니다.

let manager = ThrottledNetworkManager()
manager.printDebugData()

 

그것은 4 대신에 1을 출력해야 하고, SE-0068이 나타납니다: 이제 현재 타입을 참조하기 위해서 Self(대문자 S)를 작성할 수 있습니다. 따라서 printDebugData()를 다음과 같이 다시 작성할 수 있습니다.

class ImprovedNetworkManager {
    class var maximumActiveRequests: Int {
        return 4
    }

    func printDebugData() {
        print("Maximum network requests: \(Self.maximumActiveRequests).")
    }
}

 

이는 Self가 이전 Swift 버젼에 있는 프로토콜과 동일한 방법으로 동작하는 것을 의미합니다. 

불분명한 타입(Opaque return Types)

SE-0244은 Swift에서 불분명한 타입의 개념을 도입했습니다. 불분명한 타입은 객체의 종류를 구체적으로 알지 못하고 객체의 기능에 대해서 말하는 것입니다.

언뜻보면 프로토콜 처럼 들리지만, 불투명한 반환 타입은 연관된 타입으로 작업할 수 있고, 매번 내부적으로 같은 타입을 사용해야 하고, 구현 세부사항을 숨길 수 있기 때문에, 프로토콜 개념을 훨씬 더 발전시켰습니다.

예를 들어, Rabel 기지에서 다른 종류의 전투기를 발친시키려는 경우에 다음과 같이 코드를 작성할 수 있습니다.

protocol Fighter { }
struct XWing: Fighter { }

func launchFighter() -> Fighter {
    return XWing()
}

let red5 = launchFighter()

해당 함수를 호출하는 이는 Fighter 종류를 반환할 것을 알고 있지만, 정확히 무엇인지는 모릅니다. 결과적으로, struct YWing: Fighter { } 또는 다른 타입을 추가할수 있고 그것들을 반환할 수 있습니다.

하지만 여기에는 문제가 하나 있습니다: 특정 전투기가 Red 5 인지 확인하려면 어떻게 하나요? 해결책으로 Fighter Equtable. 프로토콜을 준수하도록 만드는 것을 생각할 수 있으므로 ==를 사용할 수 있습니다. 하지만, 곧 Swift가 launchFighter 함수에 대해 까다로운 두려운 오류가 발생할 것입니다: Fighter 프로토콜은 Self 또는 연관된 타입이 요구되기 때문에, 제네릭 제약사항으로 사용될수 있습니다.

오류의 Self 부분이 여기에서 부딪히고 있습니다. Equatable 프로토콜은 그것들이 같은지 확인하기 위해 자체적으로(Self) 2개의 인스턴스를 비교해야 하지만, Swift는 2개의 equatable한 것들이 원격으로 동일하다는것을 보장하지 않습니다 - 예제처럼, 정수형 배열과 Fighter를 비교할 수 있습니다. 

불분명한 타입은 프로토콜이 사용되는 것을 확인해서 이러한 문제를 해결하며, 내부적으로 Swift 컴파일러는 해당 프로토콜이 실제로 무엇을 해결하는지 정확히 알고 있습니다 - 그것이 XWing이고, 문자열의 배열 등 무엇이든 알고 있습니다.

불분명한 타입을 다시 보내기 위해, 프로토콜 이름 앞에 some 키워드를 사용합니다.

func launchOpaqueFighter() -> some Fighter {
    return XWing()
}

 

Fighter를 다시 가져오는 호출자의 관점에서,  XWing, YWing 또는 Fighter 프로토콜을 준수하는 다른것이 될 수 있습니다. 하지만 컴파일러의 관점에서 무엇이 반환되는지 정확히 알고 있으므로, 모든 규칙을 정확히 따르도록 만들 수 있습니다.

예를들어, 다음과 같이 some Equatable을 반환하는 함수를 고려해보세요.

func makeInt() -> some Equatable {
    Int.random(in: 1...10)
}

 

그것을 호출할때, 알고 있는 전부는 그것이 Equatable 값의 종류라는 것이며, 2번 호출하고 나서 2번 호출한 결과를 비교할 수 있습니다.

let int1 = makeInt()
let int2 = makeInt()
print(int1 == int2)

 

다음과 같이, Equatable를 반환하는 2번재 함수가 있다면 이는 사실이 아닙니다.

func makeString() -> some Equatable {
    "Red"
}

 

우리의 관점에서 볼때 Equatable 타입을 돌려주고 makeString() 을 2번 호출하거나  makeInt()를 2번 호출한 결과를 비교할수 있으며, Swift는 문자열과 정수형을 비교하는 것은 의미가 없다는 것을 알기 때문에, makeString()의 반환값과 makeInt()의 반환 값을 비교할 수 없습니다. 

여기에서 중요한 단서는 불분명한 반환 타입의 함수는 항상 하나의 특정 타입을 반환해야 한다는 것입니다. 예를들어 XWing 또는 YWing을 랜덤으로 출격하기 위해, Bool.random()을 사용하는 경우에 Swift는 컴파일러가 더 이상 무엇을 다시 보낼지 알수 없기 때문에, 우리의 코드를 빌드하는 것을 거부합니다. 

항상 같은 타입을 반환해야 하는 경우, 왜 func launchFighter() -> XWing으로 작성하지 않는가?를 생각할 수 있습니다. 때로는 동작할지 모르지만, 다음과 같이 새로운 문제들이 발생합니다:

  • 실제 세상에 노출하고 싶지 않는 타입으로 끝납니다. 예를들어, someArray.lazy.drop { … }을 사용하면 LazyDropWhileSequence를 다시 보냅니다 - Swift 표준 라이브러리로부터 전용의 매우 구체적인 타입. 우리가 실제로 신경쓰는 것은 이것이 시퀀스라는 것입니다; 우리는 Swift의 내부 동작 방식을 알 필요가 없습니다.
  • 나중에 마음을 바꿀수 있는 능력을 잃어버렸습니다. launchFighter() XWing만을 반환하도록 만드는 것은 나중에 다른 타입으로 전환할 수 없다는 것을 의미하고, Disney가 Star Wars 장난감 판매에 얼마나 의존하고 있는지가 문제가 될 것입니다! 오늘 불분명한 타입으로 X-Wings를 반환 할 수 있으며, 1년 안에 B-Wing으로 바꿉니다 - 코드를 빌드해서 하나만 반환 하지만, 여전히 유연하게 변경 할 수 있습니다.

어떤 면에서는 모든것이 제네릭과 비슷하게 들릴수 있으며, Self 또는 연관된 타입(associated type) 요구사항 문제를 해결합니다. 다음과 같이 제네릭 코드를 작성할 수 있습니다.

protocol ImperialFighter {
    init()
}

struct TIEFighter: ImperialFighter { }
struct TIEAdvanced: ImperialFighter { }

func launchImperialFighter<T: ImperialFighter>() -> T {
    return T()
}

 

매개변수 없이, 초기화하도록 준수하는 타입을 새로운 프로토콜을 정의하고, 해당 프로토콜을 준수하는 2개의 구조체를 정의하고, 그것을 사용하기 위한 제네릭한 함수를 만듭니다. 하지만, 여기에서 차이점은 다음과 같이, launchImperialFighter()의 호출자는 어떤 종류의 전투기를 선택할 수 있다는 것입니다. 

let fighter1: TIEFighter = launchImperialFighter()
let fighter2: TIEAdvanced = launchImperialFighter()

 

호출자가 데이터 타입을 선택할수 있도록 하려면 제네릭으로는 잘 동작하지만, 함수가 반환 타입을 결정하기 위해 아래로 내려갑니다.

따라서, 불분명한 결과 타입은 여러가지 작업을 수행할 수 있습니다:

  • 해당 함수 호출자가 아니라, 함수가 어떤 타입의 데이터가 반환되는지를 결정합니다.
  • 컴파일러 내부에서 어떤 타입인지 정확하게 알고 있기 때문에, Self나 연관된 타입이 요구되는 것에 대해서 걱정할 필요가 없습니다. 
  • 필요할때마다 나중에 변경할 수 있습니다.
  • 비공개 내부적인 타입을 외부로 드러내지 않습니다.

Static and class subscripts

SE-0254에서 정적(static)으로 첨자(subscripts)로 표시하는 기능이 추가했으며, 타입의 인스턴스가 아닌 타입에 적용되는 것을 의미합니다.

정적인 프로퍼티와 메소드는 해당 타입의 모든 인스턴스간에 하나의 값 세트(set)로 공유될때 사용됩니다. 예를들어, 앱 설정을 저장하기 위한 중앙 집중화된 타입이 하나 있다면, 다음과 같이 코드를 작성할 수 있습니다.

public enum OldSettings {
    private static var values = [String: String]()

    static func get(_ name: String) -> String? {
        return values[name]
    }

    static func set(_ name: String, to newValue: String?) {
        print("Adjusting \(name) to \(newValue ?? "nil")")
        values[name] = newValue
    }
}

OldSettings.set("Captain", to: "Gary")
OldSettings.set("Friend", to: "Mooncake")
print(OldSettings.get("Captain") ?? "Unknown")

 

딕셔너리로 타입 내부를 래핑(wrapping)하는 것은 사용하는데 더 신중하게 제어할 수 있다는 것을 의미하고, case 없이 열거형(enum)을 사용하는 것은 타입을 인스턴스화 하려고 시도 할 수 없다는 것을 의미합니다 - 다양한 Settings의 인스턴스를 만들수 없습니다.

Swift 5.1에서 정적 첨자를 대신 사용할 수 있고, 다음과 같이 코드를 다시 작성할 수 있습니다. 

public enum NewSettings {
    private static var values = [String: String]()

    public static subscript(_ name: String) -> String? {
        get {
            return values[name]
        }
        set {
            print("Adjusting \(name) to \(newValue ?? "nil")")
            values[name] = newValue
        }
    }
}

NewSettings["Captain"] = "Gary"
NewSettings["Friend"] = "Mooncake"
print(NewSettings["Captain"] ?? "Unknown")

 

이와 같은 사용자정의 첨자는 항상 타입의 인스턴스가 가능했습니다; 이러한 개선으로 정적 또는 클래스 첨자도 가능합니다. 

모호하지 않은 케이스에 대한 경고(Warnings for ambiguous none cases)

Swift의 옵셔널은 2가지 경우(case)에 대한 열거형(enum)으로 구현됩니다: some none. none인 경우에 대한 열거형을 만들고나서 내부를 옵셔널로 래핑(wrapped) 한 경우에 혼란스러울 수 있습니다.

예를들어:

enum BorderStyle {
    case none
    case solid(thickness: Int)
}

 

옵셔널이 아닌것으로 사용하면 항상 명확합니다.

let border1: BorderStyle = .none
print(border1)

 

none를 출력할 것입니다. 열거형에 옵셔널을 사용한 경우(어떤 테두리 스타일을 사용해야하는지 모르는 경우)에 문제가 생겼습니다.

let border2: BorderStyle? = .none
print(border2)

 

Swift는 .none BorderStyle.none 값으로 된 옵셔널이 아니라 옵셔널이 비어있는 것을 의미하기 때문에, nil을 출력할 것입니다. 

Swift 5.1에서는 이러한 혼란스러움을 경고로 출력합니다: ‘Optional.none'라고 의미하는 것을 가정합니다; 대신 'BorderStyle.none’ 을 의미하나요? 이렇게 하면 오류의 소스 호환성이 손상되는것을 피할수 있지만, 적어도 개발자에게 코드가 생각한 것을 의미하지 않는 다는 것을 알려줍니다. 

옵셔널 열거형과 옵셔널이 아닌것 일치시키기(Matching optional enums against non-optionals)

Swift는 항상 문자열과 정수형에 대한 옵셔널과 옵셔널이 아닌것 간의 switch/case 패턴을 처리 할만큼 충분히 똑똑했지만, Swift 5.1 이전은 열거형으로 확장되지 않았습니다.

흠, Swift 5.1에서는 다음과 같이 옵셔널 열거형과 옵셔널이 아닌것을 일치하기 위해 switch/case 패턴을 사용할 수 있습니다.

enum BuildStatus {
    case starting
    case inProgress
    case complete
}

let status: BuildStatus? = .inProgress

switch status {
case .inProgress:
    print("Build is starting…")
case .complete:
    print("Build is complete!")
default:
    print("Some other build status")
}

 

Swift는 옵셔널 열거형과 옵셔널이 아닌 case를 직접 비교하는 것이 가능하므로, 해당 코드는 Build is starting…을 출력할 것입니다.

정렬된 컬렉션 차이(Ordered collection diffing)

SE-0240은 정렬된 컬렉션간의 차이를 계산하고 적용하는 기능을 소개합니다. 이것은 테이블 뷰에 복잡한 컬렉션을 가진 개발자에게 특히 흥미로울 수 있으며, 애니메이션을 사용해서 부드럽게 많은 항목들을 추가하고 삭제하길 원합니다.

기본 원칙은 간단합니다: Swift 5.1은 2개의 정렬된 컬렉션간에 차이점을 계산하는 새로운 difference(from:) 메소드를 제공합니다 - 제거할 항목과 삽입할 항목. Equatable 요소들을 포함하는 모든 정렬된 컬렉션에서 사용될 수 있습니다.

이를 증명하기 위해, 점수의 배열을 만들수 있고 하나에서 다른 하나와의 차이점을 계산하고, 이러한 차이점을 반복하고 각각 2개의 컬렉션을 같게 만듭니다.

주의: Swift는 이제 Apple의 운영체제 시스템내부에 제공되기때문에, 새로운 기능은 코드가 새로운 함수가 포함하는 OS에서 실행되는지 확인하기 위해서 #available 확인하는것과 함께 사용되야 합니다. 알려지지 않는 기능, 미래에 어느 시점에 운영체제 시스템에 탑재될 발표되지 않은 기능, 9999 특별한 버젼의 숫자는 실제 숫자가 무엇인지 아직 모릅니다라는 의미로 사용됩니다.

다음은 해당 코드 입니다:

var scores1 = [100, 91, 95, 98, 100]
let scores2 = [100, 98, 95, 91, 100]

if #available(iOS 9999, *) {
    let diff = scores2.difference(from: scores1)

    for change in diff {
        switch change {
        case .remove(let offset, _, _):
            scores1.remove(at: offset)
        case .insert(let offset, let element, _):
            scores1.insert(element, at: offset)
        }
    }

    print(scores1)
}

 

더 고급스러운 애니메이션에 대해서, 변경사항의 3번째 값을 사용할 수 있습니다: associatedWith. 따라서, .insert(let offset, let element, _)을 사용하기 보다는 .insert(let offset, let element, let associatedWith)을 대신 작성할 수 있습니다. 동시에 변경사항 쌍을 추적할 수 있습니다: 컬렉션에서 항목을 2군데 아래로 이동하는 것은 제거하고 삽입되지만, associatedWith 값은 이러한 두가지 변경사항을 함께 묶음으로써 하나의 움직임으로 취급합니다.

직접 변경사항을 적용하기 보다는, 다음과 같이, 새로운 applying() 메소드를 사용해서 전체 컬렉션에 적용할 수 있습니다.

if #available(iOS 9999, *) {
    let diff = scores2.difference(from: scores1)
    let result = scores1.applying(diff) ?? []
}

 

초기화되지 않은 배열 만들기(Creating uninitialized arrays)

SE-0245는 기본 값을 미리 채우지 않는 배열에 대한 새로운 초기화를 소개합니다. 이전에는 비공개 API로 사용할 수 있었으며, Xcode가 코드 완성 목록에 없지만 여전히 사용할 수 있다는 의미입니다 - 앞으로 빠지게(withdrawn) 되지 않는 위험을 감수할 수 있는 경우입니다. 

해당 초기화를 사용하기 위해서는, 원하는 양(capacity)를 알려주고, 필요한 값을 채우는 클로져를 제공합니다. 클로져는 값을 작성할 수 있는 안전하지 않은 변경가능한 버퍼 포인터를 제공할 것이며, inout 두번째 매개변수는 실제로 사용된 값을 다시 볼 수 있습니다.

예를들어, 다음과 같이 10개의 임의의 정수로된 배열을 만들 수 있습니다.

let randomNumbers = Array<Int>(unsafeUninitializedCapacity: 10) { buffer, initializedCount in
    for x in 0..<10 {
        buffer[x] = Int.random(in: 0...10)
    }

    initializedCount = 10
}

 

여기에 몇가지 규칙들이 있습니다

  1. 요청한 모든 양(capacity)을 사용할 필요가 없지만, 그 양을 넘을 수는 없습니다. 따라서, 10의 양을 요청하는 경우에 initializedCount는 0에서 10까지를 설정할 수 있지만, 11은 안됩니다.
  2. 배열에 포함된 요소들을 초기화하지 않고 임의의 데이터로 채우기 쉽습니다. (예를들어 initializedCount를 5로 설정한 경우 실제로 0에서 4까지의 요소에 값을 제공하지 않습니다)
  3. initializedCount를 설정하지 않으면 0이 되므로, 할당된 모든 데이터는 잃어버리게 될 것입니다.

이제, 다음과 같이 map()을 사용해서 위 코드를 재작성할 수 있습니다.

let randomNumbers2 = (0...9).map { _ in Int.random(in: 0...10) }

 

확실히 읽기 쉽지만, 효율성이 떨어집니다: 하나의 범위를 만들고, 새로운 빈 배열을 만들고, 정확한 양의 크기로 만들고, 범위만큼 반복하고, 각 범위 항목에 대해 클로져를 한번씩 호출합니다.

 

반응형

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

What’s new in Swift 5.4  (0) 2021.04.15
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.0  (0) 2019.03.07
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
Posted by 까칠코더
,