[WWDC2023] Swift 업데이트 정리

정유진·2023년 6월 8일
0

swift

목록 보기
21/25
post-custom-banner

Swift 5.9

1. Declaration

if, else if, else 를 사용해 조건을 만족하는 변수 초기화 가능! 보기 좋아졌다.

  1. ternary expression 삼항 연산자 대체하기
  • old
let bullet =
    isRoot && (count == 0 || !willExpand) ? ""
        : count == 0    ? "- "
        : maxDepth <= 0 ? "▹ " : "▿ "
  • now
let bullet =
    if isRoot && (count == 0 || !willExpand) { "" }
    else if count == 0 { "- " }
    else if maxDepth <= 0 { "▹ " }
    else { "▿ " }
  1. global / stored property 멤버 초기화 시
  • old
let attributedName = {
				if let displayName, !displayName.isEmpty {
            AttributedString(markdown: displayName)
        } else {
            "Untitled"
        }
}() // immediately execute
  • now
let attributedName = 
				if let displayName, !displayName.isEmpty {
            AttributedString(markdown: displayName)
        } else {
            "Untitled"
        }

2. Resultbuilders

Faster type checking, improved code completion, more accurate error message

resultBuilder?

SwiftUI에서 body 안에서 함수(ViewBuilder)를 나열하여 view를 그렸던 예시를 생각하면 이해가 쉽다.

@resultBuilder
struct StringBuilder {
	// 반드시 구현해야 하는 부분
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
    
    // 선택 구현
    // if 문에서 true일 경우 
    static func buildEither(first component: String) -> String {
        return component
    }

	// else일 경우 
    static func buildEither(second component: String) -> String {
        return component
    }
}

@StringBuilder func example() -> String {
    "다짜고짜 3개의 문장을"
    "연산자 또는 콤마 없이 나열해도"
    "결과는 한 문장으로 반환된다."
}
  • now
    컴파일 에러가 모호한 위치에서 발생했던 과거와 달리 보다 정확한 라인에 표시된다.
struct ContentView: View {
    enum Destination { case one, two }

    var body: some View {
        List {
            NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic
                Text("one")
            }
            NavigationLink(value: .two) {
                Text("two")
            }
        }.navigationDestination(for: Destination.self) {
            $0.view // Error occurs here in 5.7
        }
    }
}

3. Generic

구현 시에는 type을 특정하지 않더라도 사용 시 type을 지정하여 strong typed 되게 하는 type inference - using these types without needing to understand the advanced capabilities they're built with.

  • old

메서드 오버로딩을 하는 데에 파라미터 수의 제한이 있다.

func evaluate<Result>(_:) -> (Result)

func evaluate<R1, R2>(_:_:) -> (R1, R2)

func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3)

func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4)

func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5)

func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
  • now
    위의 오버로딩을 <each Result>한 줄로 줄였다고 보면 된다.
This is done with a new language concept 
that can represent multiple individual type parameters 
that are "packed" together. = Parameter pack

길이 제한 없이 n개의 request를 각각 받아 다룬다.

func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
// return a single or a tuple containing each value. 

4. Macros

코드 작성 시 boiler plate 를 내가 작성하지 않아도 된다.
샘플 확인 https://github.com/DougGregor/swift-macro-examples

  • macro로 할 수 있는 일 : error를 더 자세하게 확인할 수 있게 로그 찍기
import PowerAssert
#assert(max(a, b) == c)

public macro assert(_ condition: Bool) // condition to be checked

  • external macro
    컴파일러의 플러그인 처럼 사용되고 있다.
public macro assert(_ condition: Bool) = #externalMacro(
    module:PowerAssertPlugin,
    type:PowerAssertMacro"
)

  • 위의 두 개념을 조합하면, Freestanding macro roles
@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
    module:PowerAssertPlugin,
    type:PowerAssertMacro"
)
  • anotation 처럼 사용하는 attached roles
@attached(member, names: arbitrary)
public macro CaseDetection() = #externalMacro(module: "MacroExamplesPlugin", type: "CaseDetectionMacro")

// 사용 예시
@CaseDetection
enum Pet {
  case dog
  case cat(curious: Bool)
  case parrot
  case snake
}

영상만 봐서는 macro 사용법이 이해되지 않아서 예제 샘플에서 freestanding 코드를 찾아보았다.

1) stringify macro 만들기

macro를 사용할 때 module 이름을 import해서 사용하고
macro의 구현 내용은 struct StringifyMacro에서 확인할 수 있다.

// 예시
import MacroExamplesPlugin
  • declaration
    objective-c에서 header를 선언하듯 macro declaration와 implementing이 분리되어 있다.
/// "Stringify" the provided value and produce a tuple that includes both the
/// original value as well as the source code that generated it.
// 전달 받은 값의 원본과 string화한 문자열의 튜플을 반환한다.
@freestanding(expression) public macro stringify<T>(_ value: T) 
-> (T, String) 
= #externalMacro(module: "MacroExamplesPlugin",
				 type: "StringifyMacro")
  • implementation
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

// 구현내용에 따라 원하는 macro protocol을 따른다.
public struct StringifyMacro: ExpressionMacro {
  public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
  ) -> ExprSyntax {
    guard let argument = node.argumentList.first?.expression else {
      fatalError("compiler bug: the macro does not have any arguments")
    }

// 여기에서 반환하는 형식대로 출력될 것
    return "(\(argument), \(literal: argument.description))" 
  }
}
  • 사용 예시와 결과
print(#stringify(x + y)) // (3, "x + y")

2) warning 만들기

규칙에 맞지 않는 expression에 대해 compiler warning을 낼 수 있다.

  • declaration
/// Macro that produces a warning on "+" operators within the expression, and
/// suggests changing them to "-".
// + 기호를 사용하면 - 기호를 사용하도록 block 한다.
@freestanding(expression) public macro addBlocker<T>(_ value: T) -> T = #externalMacro(module: "MacroExamplesPlugin", type: "AddBlocker")

/// Macro that produces a warning, as a replacement for the built-in
/// #warning("...").
// 지정한 형식이 아닐 경우 warning 한다.
@freestanding(expression) public macro myWarning(_ message: String) = #externalMacro(module: "MacroExamplesPlugin", type: "WarningMacro")
  • 결과

3) case detection

  • declaration
/// Add computed properties named `is<Case>` for each case 
element in the enum.
// isSometing을 enum 안에 구현하지 않아도 자동 구현
@attached(member, names: arbitrary)
public macro CaseDetection() = #externalMacro(module: "MacroExamplesPlugin", type: "CaseDetectionMacro")
  • implementation
// member macro protocol 사용
public struct CaseDetectionMacro: MemberMacro {
  public static func expansion<
    Declaration: DeclGroupSyntax, Context: MacroExpansionContext
  >(
    of node: AttributeSyntax,
    providingMembersOf declaration: Declaration,
    in context: Context
  ) throws -> [DeclSyntax] {
    declaration.memberBlock.members
      .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
      .map { $0.elements.first!.identifier }
      .map { ($0, $0.initialUppercased) }
      .map { original, uppercased in
        """
        var is\(raw: uppercased): Bool {
          if case .\(raw: original) = self {
            return true
          }

          return false
        }
        """
      }
  }
}
  • 결과
@CaseDetection
enum Pet {
  case dog
  case cat
  case parrot
  case snake
}

let pet: Pet = .cat
print("Pet is dog: \(pet.isDog)") // false
print("Pet is cat: \(pet.isCat)") // true

신기한데 어디에 쓰면 좋을지는 아직 잘 모르겠다. type 체크 등의 유효성 검사를 하는 코드를 반복적으로 짤 일이 생기면 한 번 자세히 뜯어보고 맛보기는 이 정도로 끝낸다.

5. Non-copyable

새로 출시되는 Senoma OS를 구현하는 과정에서 C 코드를 걷어내고 Swift로 구현하여 성능이 향상되었다고 한다. low-level에서도 효과적인 언어라는 점에서 괄목할만한 성과이다.

  • Calendar calculations
  • Date formatting
  • JSON coding

파일 제어를 하기위한 fcntl 을 사용하기 위해 Swift에서는 Darwin 이라는 모듈을 제공하는데 struct에서 stream을 manually 하게 close하는 것을 까먹기 쉽상이다. 그래서 class로 구현하여 deinit 할 때에 close를 하자니 불필요한 메모리를 사용하게 된다. 그리고 thread 간에 공유될 경우 data race가 발생할 가능성을 배제할 수 없다.

struct FileDescriptor {
    private var fd: CInt
  
    init(descriptor: CInt) { self.fd = descriptor }

    func write(buffer: [UInt8]) throws {
        let written = buffer.withUnsafeBufferPointer {
            Darwin.write(fd, $0.baseAddress, $0.count)
        }
        // ...
    }
  
    func close() {
        Darwin.close(fd)
    }
}

위의 코드를 보면 struct라고 하더라도 file descriptor 라는 reference를 가지고 있기 때문에 의도치 않게 mutable state를 공유하여 버그를 발생시킬 위험이 있다. swift는 struct이든 class이든 기본적으로 copyable 이다. 초과 복사가 일어날 경우 병목현상이 발생할 수 있지만 복사 자체를 컴파일러가 막는 것보단 병목 현상이 발생할 때 해결하는 것이 나을 것이다. 하지만 위의 예시와 같이 객체 복사가 정합성을 깨뜨릴 수 있다면? struct, enum 타입에 대하여 복사를 제한해야 할 것이다. 그리고 copy가 가능하지 않게 된 non-copyable struct는 class에 하듯 deinit 해주어야 하는 것을 잊지 말아야 한다.

struct FileDescriptor: ~Copyable {
    private var fd: CInt
  
    init(descriptor: CInt) { self.fd = descriptor }

    func write(buffer: [UInt8]) throws {
        let written = buffer.withUnsafeBufferPointer {
            Darwin.write(fd, $0.baseAddress, $0.count)
        }
        // ...
    }
  
    func close() {
        Darwin.close(fd)
    }
  
    deinit {
        Darwin.close(fd)
        // the close operation marked as consuming
        // giving up ownership of copyable == no longer use value
    }
}

아예 close 함수 자체에 consuming 이라는 표기를 할 수도 있다.

struct FileDescriptor {
    private var fd: CInt
  
    init(descriptor: CInt) { self.fd = descriptor }

    func write(buffer: [UInt8]) throws {
        let written = buffer.withUnsafeBufferPointer {
            Darwin.write(fd, $0.baseAddress, $0.count)
        }
        // ...
    }
  
    consuming func close() {
        Darwin.close(fd)
    }
  
    deinit {
        Darwin.close(fd)
    }
}

이 경우 close() 이후 write()를 하려고 하면 컴파일 에러가 발생하여 사전에 오류를 피할 수 있다.

6. C++ interoperability

C++ 코드를 Swift compiler가 이해할 수 있다고 한다.

  • struct
  • vector
  • map
// Person.h
struct Person {
    Person(const Person &);
    Person(Person &&);
    Person &operator=(const Person &);
    Person &operator=(Person &&);
    ~Person();
  
    std::string name;
    unsigned getAge() const;
};
std::vector<Person> everyone();

// Client.swift
func greetAdults() {
    for person in everyone().filter { $0.getAge() >= 18 } {
        print("Hello, \(person.name)!")
    }
}

@objc를 사용하지 않아도 Swift 코드 자체를 C++이 사용할 수도 있다.

  • full APIs
  • properties
  • methods
  • initializers

Concurrency

Abstract concurrency model

추상 모델이기 때문에 서로 다른 환경(iPhone, Watch, Server...)과 라이브러리에서 적용이 가능하다.

  • Tasks
    • run anywhere
    • sequential unit of work
    • 어디서든 비동기가 필요할 때 호출 가능한 블록
    • 필요에 따라 task를 suspend 하거나 continue할 수 있다.
    • task의 실행 순서는 Dispatch 라이브러리가 스케줄링
    • 멀티쓰레드 사용으로 인한 오버헤드를 피하기 위해 single-threaded cooperative queue 지원
      - callback 중심 라이브러리와의 호환 위한 async-await 지원
withCheckedContinuation { continuation in
	sendMessage(msg) {	response in
    	continuation.resume(returning: response)
    }
}

callback을 DispatchQueue를 사용하여 async하게 사용하던 것을 modern async-await를 사용하여 sync하게 코드를 바꿀 수 있다. 이는 새로운 것이 아니지만 참고로 예시를 적어둔다. 이 방법을 사용하면 반환받은 데이터를 어떻게 처리할지에 대한 callback을 미리 구현해서 넘길 필요가 없다.


func oldWayFetch(completion: @escaping([String]->Void)) {
	DispatchQueue.main.async {
    	completion(["data1", "data2"]) // 해당 데이터를 어떻게 처리할지에 대해 completion 내용을 채워서 param으로 넘겼어야 함.
    }
}

func newWayFetch() async -> [String] {
	await withCheckedContinuation { continuation in
    	oldWayFetch { items in 
        	continuation.resume(returning: items)
        }
    }
}

// 사용 예
func test() async {
	let items = await newWayFetch()
    
    for item in items 
    // 받은 데이터에 대한 핸들링
    ...
}
  • Actors
    • isolated state
    • mutually exclusive access
    • 외부에서 actor에 접근할 때는 await 와 함께 사용해야 한다.
    • lock free queue (actor가 없던 시절에는 thread safe하게 코드를 작성하기 위해 NSLock을 사용해야 했다.)
    • Swift 5.9, a particular actor to implement its own synchronization mechanism = custom actor executors
    • 다른 코드가 C++, Objc-C로 쓰여져 있어 actor를 채택하지 않아서 해당 코드들과 queue를 공유하지 않고 특정 dispatch queue를 사용하고 싶을 때
// Custom actor executors

actor MyConnection {
  private var database: UnsafeMutablePointer<sqlite3>
  private let queue: DispatchSerialQueue // added
  
  nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } // queue와 호응되는 executor를 생성

  init(filename: String, queue: DispatchSerialQueue) throws {}
  
  func pruneOldEntries() {}
  func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? {}
}

// actor의 바깥에서 이렇게 호출을 하게 되면
// 내가 지정한 queue에서 async하게 호출하는 것과 같다
await connection.pruneOldEntries()

DispatchQueue를 통해 별도의 queue 위에서 actor 사용이 가능한 이유는 위의 queue type인 DispatchSerialQueue가 SerialExecutor 프로토콜을 따르기 때문이다. 이 프로토콜을 따르는 타입을 정의하여 좀더 커스텀한 매커니즘을 적용할 수도 있다. 이 프로토콜의 주요한 동작 방식은 executor(queue)의 context에서 해당 코드가 이미 실행되었는지 안 되었는지를 확인하는 것이다.

// Executor protocols

protocol Executor: AnyObject, Sendable {
	// which takes ownership of an executor job
    // a job is part of an async task that needs to run sync on the executor
    // job이 enqueue되고 나면 그 job을 run 시키는게 executor의 일
    // 해당 dispatch의 serial queue에 run시킬 다른 코드가 없으면 run 
    func enqueue(_ job: consuming ExecutorJob)
}

protocol SerialExecutor: Executor {

	// extracting an unowned reference (weak reference와 달리 nil일 수 없음_ 이에 관해서는 추후에 다루도록 하겠음) 
    //to the executor to allow access to it without excess reference-counting traffic
    func asUnownedSerialExecutor() -> UnownedSerialExecutor
    func isSameExclusiveExecutionContext(other executor: Self) -> Bool
}

extension DispatchSerialQueue: SerialExecutor {}

이 외에도 FoundationDB에 대한 내용이 언급되었지만 MacOS에 적용되는 내용이라 여기서는 다루지 않는다.

profile
느려도 한 걸음 씩 끝까지
post-custom-banner

0개의 댓글