if, else if, else 를 사용해 조건을 만족하는 변수 초기화 가능! 보기 좋아졌다.
let bullet =
isRoot && (count == 0 || !willExpand) ? ""
: count == 0 ? "- "
: maxDepth <= 0 ? "▹ " : "▿ "
let bullet =
if isRoot && (count == 0 || !willExpand) { "" }
else if count == 0 { "- " }
else if maxDepth <= 0 { "▹ " }
else { "▿ " }
let attributedName = {
if let displayName, !displayName.isEmpty {
AttributedString(markdown: displayName)
} else {
"Untitled"
}
}() // immediately execute
let attributedName =
if let displayName, !displayName.isEmpty {
AttributedString(markdown: displayName)
} else {
"Untitled"
}
Faster type checking, improved code completion, more accurate error message
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개의 문장을"
"연산자 또는 콤마 없이 나열해도"
"결과는 한 문장으로 반환된다."
}
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
}
}
}
구현 시에는 type을 특정하지 않더라도 사용 시 type을 지정하여 strong typed 되게 하는 type inference - using these types without needing to understand the advanced capabilities they're built with.
메서드 오버로딩을 하는 데에 파라미터 수의 제한이 있다.
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)
<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.
코드 작성 시 boiler plate 를 내가 작성하지 않아도 된다.
샘플 확인 https://github.com/DougGregor/swift-macro-examples
import PowerAssert
#assert(max(a, b) == c)
public macro assert(_ condition: Bool) // condition to be checked
public macro assert(_ condition: Bool) = #externalMacro(
module: “PowerAssertPlugin”,
type: “PowerAssertMacro"
)
@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
module: “PowerAssertPlugin”,
type: “PowerAssertMacro"
)
@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 코드를 찾아보았다.
macro를 사용할 때 module 이름을 import해서 사용하고
macro의 구현 내용은 struct StringifyMacro에서 확인할 수 있다.
// 예시
import MacroExamplesPlugin
/// "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")
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")
규칙에 맞지 않는 expression에 대해 compiler warning을 낼 수 있다.
/// 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")
/// 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")
// 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 체크 등의 유효성 검사를 하는 코드를 반복적으로 짤 일이 생기면 한 번 자세히 뜯어보고 맛보기는 이 정도로 끝낸다.
새로 출시되는 Senoma OS를 구현하는 과정에서 C 코드를 걷어내고 Swift로 구현하여 성능이 향상되었다고 한다. low-level에서도 효과적인 언어라는 점에서 괄목할만한 성과이다.
파일 제어를 하기위한 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()를 하려고 하면 컴파일 에러가 발생하여 사전에 오류를 피할 수 있다.
C++ 코드를 Swift compiler가 이해할 수 있다고 한다.
// 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++이 사용할 수도 있다.
추상 모델이기 때문에 서로 다른 환경(iPhone, Watch, Server...)과 라이브러리에서 적용이 가능하다.
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
// 받은 데이터에 대한 핸들링
...
}
// 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에 적용되는 내용이라 여기서는 다루지 않는다.