스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift의 새로운 기능
Swift의 새로운 소식을 만나 보세요. 매개변수 팩과 매크로와 같은 기능으로 API가 더욱 확장성 있고 표현적으로 바뀐 점을 다룹니다. 또한, 상호운용성과 관련된 개선 사항을 살펴보고 Swift의 성능을 확장하여 Foundation과 서버의 대규모 분산 프로그램까지 안전성을 확보할 수 있는 방법을 공유합니다.
챕터
- 0:39 - Swift project update
- 2:44 - Using if/else and switch statements as expressions
- 3:52 - Result builders
- 4:53 - type parameter packs
- 9:34 - Swift macros
- 19:47 - Swift foundation
- 23:25 - Ownership
- 27:59 - C++ interoperability
- 32:41 - What's new in Swift Concurrency
- 38:20 - FoundationDB: A case study
리소스
관련 비디오
WWDC23
- 구조화된 동시성의 기초를 넘어
- 매개변수 팩으로 API 범용화하기
- Swift 매크로 상세히 알아보기
- Swift 매크로 작성하기
- Swift와 C++ 혼합하기
- SwiftData 만나보기
- SwiftUI의 새로운 기능
WWDC21
-
다운로드
♪ ♪
안녕하세요 'Swift 5.9의 새로운 기능' 세션입니다 저는 Ben이고 동료 Doug와 함께 올해 Swift 언어의 개선 사항에 대해 알려드리죠 Swift의 깔끔한 구문과 강력한 새로운 기능을 통해 여러분이 전달하려는 내용을 쉽게 표현하고 프레임워크 작성자가 새로운 API를 자연스럽게 사용하도록 만듭니다 또한 성능을 제어할 수 있는 새로운 방법과 저급 코드의 안전에 관해 다루겠습니다 그전에 Swift 오픈 소스 프로젝트 얘기로 시작하죠 Swift를 위한 멋진 업데이트이며 언어의 공헌자와 사용자로 구성된 Swift 커뮤니티 덕분에 가능했으며 모두 swift.org에 모여 언어를 진화시키고 새로운 계획을 지원합니다 Swift는 언어 진화를 위해 오픈 절차를 따르죠 새로운 기능이나 중대한 행동 변화를 Swift 포럼에서 공개적으로 발의하고 검토합니다 여러분도 동참하고 싶으시면 Swift 웹사이트 대시보드에서 언어에 관한 제안 사항을 볼 수 있습니다 1년 전 Swift 프로젝트 운영의 중대한 구조 조정이 있었죠 핵심 팀에서 결성한 언어 운영 집단이 Swift 언어의 감독과 표준 라이브러리 진화의 감독을 맡았습니다 이후 언어 운영 집단에서 40개의 언어 제안을 감독하였는데 그중 여러 개를 오늘 다룰 예정입니다
때로는 개별적인 언어 제안이 더 큰 주제의 일부로 통합되는데 Swift 동시성의 추가가 그 예로 10개의 개별적 제안을 통해 시작됐습니다 이런 사례의 경우 언어 운영 집단이 이런 제안을 통합하는 새로운 방법을 도입했는데 비전 문서를 이용하는 거죠 언어의 대규모 변화에 대한 제안을 열거합니다 언어 운영 집단에서 처음으로 받아들인 안건은 Swift 매크로인데 Swift 5.9의 새로운 기능으로 나중에 자세히 다루겠습니다 물론 언어의 진화는 Swift 커뮤니티가 하는 일의 일부에 불과합니다 성공적인 언어는 더 많은 것이 필요하죠 좋은 툴이 필요하고 여러 플랫폼을 충분히 지원하고 문서화가 잘돼야 합니다 이런 분야의 발전을 감독하기 위해 핵심 팀에서 언어 운영 집단과 병행한 생태계 운영 집단을 결성하고 있죠 새로운 집단은 Swift.org의 블로그를 통해 공개되었으며 향후 공지를 통해 새로운 집단 결성 소식을 알 수 있습니다 이제 올해 Swift 언어의 변동 사항을 얘기해 보죠 코드로 의도를 표현하는 방식이 개선됐습니다 Swift 5.9에는 가장 많은 요청을 받았던 언어 개선 사항을 포함하는데 if/else와 switch 문을 식으로 사용하는 걸 허용하여 코드를 깔끔하게 정리하는 방법을 제공하는 거죠
예를 들어, 복잡한 조건을 통해 let 변수를 초기화하고 싶을 때 변칙을 이용하여 알아보기 힘든 복잡한 식을 구성했습니다
If 식을 사용하면 훨씬 더 익숙하고 알아보기 쉬운 if 문을 사용할 수 있죠
이것이 도움이 되는 건 전역 변수나 저장 프로퍼티를 초기화할 때입니다 단일 식으로도 문제가 없지만 조건이 필요한 경우 클로저 안에 래핑을 하는 변칙을 이용한 뒤 바로 실행해야 합니다
이제는 if 문을 식으로 사용할 수 있어서 불필요한 부분을 지우고 깔끔한 코드만 남길 수 있죠 SwiftUI와 같은 기능을 지원하는 선언형 구문 Result builder가 올해 굉장한 발전을 이뤘습니다 최적화된 타입 체커 기능과 코드 완성 및 개선된 오류 메시지가 있죠
이 개선 사항은 무효 코드에 집중됐습니다 오류가 있는 Result Builder 코드는 오랜 시간 후에 실패했는데 타입 체커가 수많은 무효 경로를 확인했기 때문이죠
Swift 5.8부터 무효 코드의 타입 체크가 훨씬 빨라졌고 무효 코드의 오류 메시지가 정확해졌습니다 예를 들어 이전에는 무효 코드로 인해 Result Builder의 다른 부분을 오류로 잘못 안내했죠 Swift 5.7에서는 이런 오류 메시지가 나타나는데 실제 실수는 이곳에서 발생했습니다
최근 버전에서는 더 정확한 컴파일러 진단으로 실제 문제를 식별하죠 다음은 제네릭 시스템에 기능을 추가하여 매일 사용하는 프레임워크를 개선하는 방법을 다루겠습니다
여러분이 작성하는 Swift 코드 대부분은 제네릭을 사용하죠 타입 추론은 고급 기능을 이해하지 않아도 해당 타입을 사용할 수 있게 해 줍니다 예를 들어 표준 라이브러리인 Array 타입은 제네릭을 사용하여 어떤 종류의 데이터도 저장할 수 있는 목록을 제공합니다 Array를 사용할 때는 요소만 입력하면 되죠 명시적인 매개변수를 요소의 타입으로 지정하지 않아도 됩니다 요소의 값을 통해 추론할 수 있으니까요
Swift의 제네릭 시스템은 자연적인 API를 기능하게 하고 타입 정보를 보존하여 여러분이 제공하는 타입에서 코드가 문제없이 작동하게 하죠 Swift 컴파일러의 코드 베이스에서 영감을 받은 예시입니다 API에서 요청 타입을 받아 평가한 뒤 강력한 타입의 값을 생산하죠 따라서 Boolean 값을 요청한 뒤에 Boolean 값을 반환받을 수 있습니다
일부 API는 확고한 타입뿐만 아니라 통과시키는 인수의 수도 추상화하려고 합니다 따라서 함수가 하나의 요청에 하나의 결괏값을 반환하거나 2개의 요청에 2개의 결괏값 3개도 마찬가지죠
이를 지원하기 위해 제네릭 시스템과 다수 인수를 처리할 수 있는 메커니즘을 함께 사용하여 통과시킨 모든 타입과 결괏값이 연결됩니다 Swift 5.9 이전에는 이런 패턴을 완수하려면 API가 지원하는 구체적인 인수의 길이에 대한 오버로드를 추가했죠 하지만 이 방식에는 제약이 있습니다 통과시킬 수 있는 인수의 수에 대한 인위적인 상한이 있어 인수를 너무 많이 통과시키면 컴파일러 오류가 발생하죠 여기서는 6개를 초과하는 인수를 처리하는 오버로드가 없는 상황에서 7개를 통과시켰습니다 이런 오버로드 패턴은 한계가 있고 API 전반에 걸쳐 임의의 인수만 처리할 수 있죠
Swift 5.9에서는 API 전반에 걸친 지원으로 인수의 길이에 대한 제네릭 추상화를 가능케 합니다 새로운 언어 개념을 통해 가능해졌는데 다수의 개별 타입 매개변수를 하나로 묶었기 때문이죠 이 새로운 개념을 타입 매개변수 팩이라고 합니다 매개변수 팩을 이용하여 고정된 인수 길이에 대한 개별적인 오버로드가 있는 API의 경우 단일 함수로 축약할 수 있죠
단일 타입 매개변수인 Result를 받아들이는 대신 단일 요청의 결과 타입을 대표함으로써 evaluate 함수가 각 Result 타입의 개별 요청을 받아들입니다 함수가 각 결과를 괄호에 반환하는데 단일 값일 수도 있고 각 값을 포함한 튜플일 수도 있죠 이제 evaluate 함수가 제한 없이 모든 인수 길이를 처리합니다
타입 추론을 통해 매개변수 팩을 사용하는 API를 자연스럽게 이용할 수 있으며 API의 팩 사용 사실도 모르죠
이제 새로운 evaluate 함수를 호출하면 어떤 수의 인수도 처리할 수 있으며 고정된 길이의 오버로드를 호출하는 것처럼 보입니다 Swift는 각 인수의 타입과 총개수를 추론할 때 함수 호출 방식을 기반으로 하죠 제네릭 라이브러리 API의 작성 방법이 궁금하면 '매개변수 팩으로 API 일반화하기'를 시청하세요 제네릭 API의 자연스러운 호출은 Swift의 근본적 설계 목표인 간결한 코드를 통한 명확한 표현을 나타냅니다
Swift의 발전된 언어 기능이 멋진 API를 가능하게 하여 여러분이 원하는 바를 쉽게 표현할 수 있죠
Swift에서 작성하는 첫 구문부터 발전된 언어 기능의 이점을 누릴 수 있는데 배열이나 딕셔너리를 통한 제네릭을 사용하거나 SwiftUI에서 UI를 설계할 때도 마찬가지입니다 Swift의 단계적 공개를 통해 언제든 발전된 기능에 관해 알아볼 수 있죠
Swift 5.9는 이러한 설계 방식을 다음 단계로 발전시켜 라이브러리 작성자에게 새로운 도구를 제공하여 매크로 시스템을 통한 표현적 API 설계를 지원합니다 이 내용은 Doug가 알려드리겠습니다 매크로를 통해 언어 자체의 능력을 확장하고 보일러플레이트를 제거하고 Swift의 표현력을 이용할 수 있죠 assert 함수를 살펴봅시다 조건이 참인지 확인하죠 assert는 조건이 거짓이면 프로그램을 멈추지만 그런 일이 일어났을 때 오류에 대한 정보는 파일과 줄 번호에 불과합니다 로그를 추가하거나 디버거를 설치해야 더 알 수 있죠 이런 문제를 개선하려는 시도가 있었습니다 XCTest가 assert-equal 작업으로 두 값을 분리하여 조건을 만족하지 않았을 때 두 값이 다르다는 걸 볼 수 있죠 하지만 어떤 값이 틀렸는지는 아직도 모릅니다 a, b, max의 결괏값 중 어느 것일까요? 이 방식은 assert 함수에서 확인하는 모든 규모에 적용되지도 않죠 기존의 assert로 돌아가면 assert가 실패했을 때 소스 코드에서 볼 수 있는 정보가 많습니다 코드가 뭐였죠? a, b, c 값이 뭐였을까요? max가 생산한 값은 뭘까요? Swift에서는 커스텀 기능 없이 이 문제를 개선하지 못했지만 매크로가 있으면 가능합니다
이 예에서 #assert 구문이 assert라는 매크로를 확장하죠 해시 구문이 익숙해 보일 겁니다 Swift에서 같은 기호를 사용하고 있기 때문이죠 #file, #selector #warning 등이 있습니다 assert 매크로는 함수와 비슷해 보이지만 매크로이기 때문에 assert가 실패했을 때 풍부한 경험을 제공하죠 실패한 assert에 대한 코드를 보여 주고 결과에 기인한 값을 모두 표시합니다
Swift에서는 매크로가 타입과 함수 같은 API이므로 매크로를 정의하는 모듈을 임포트하여 접근할 수 있죠 다른 API와 마찬가지로 매크로도 패키지로 배포됩니다 여기 있는 assert 매크로는 PowerAssert 라이브러리에 있죠 GitHub에 올라와 있는 오픈 소스 Swift 패키지입니다
매크로 패키지의 안을 들여다보면 assert를 위한 매크로 선언을 찾을 수 있죠 'macro' 키워드로 소개됩니다 그것 말고는 함수랑 비슷해 보이죠 레이블이 없는 Bool 매개변수 하나가 조건 확인으로 들어 있습니다 만약 이 매크로가 값을 생산한다면 결과 타입은 화살표 구문으로 작성되겠죠 매크로의 사용은 매개변수에 대해 타입 체크가 실행됩니다 그 의미는 매크로를 사용하다가 실수했을 경우 예를 들면 최댓값을 다른 값과 비교하는 걸 잊었다면 유용한 오류 메시지가 매크로가 확장되기 전에 즉시 출력될 겁니다 이를 통해 Swift가 매크로를 사용할 때 멋진 개발 경험을 제공하는데 매크로가 제대로 된 타입의 입력값을 통해 프로그램을 보완하는 코드를 예상할 수 있는 방식으로 생산하기 때문이죠 매크로 대부분은 '외부 매크로'로 정의되며 string을 통해 매크로 적용의 모듈과 타입을 구체화합니다 외부 매크로 타입은 별도의 프로그램에 정의돼 있으며 컴파일러 플러그인으로 작동하죠 Swift 컴파일러가 소스 코드를 통과시켜 매크로의 사용을 플러그인에 보냅니다 플러그인이 새로운 소스 코드를 생산하면 다시 Swift 프로그램에 통합되죠 여기서 매크로가 assert 함수를 코드를 확장하여 개별 값을 포착하고 소스 코드의 어느 부분에 표시돼야 하는지 알아냅니다 직접 보일러플레이트를 작성하지 않아도 매크로가 대신 해 주죠 매크로 선언에는 또 하나의 정보인 매크로의 역할이 있습니다 여기 있는 assert 매크로는 freestanding(expression)이죠 해시 구문을 이용하기 때문에 freestanding이라고 부르며 구문에서 직접 작동하여 새로운 코드를 생성합니다 값을 생산하는 곳에는 어디든 쓸 수 있어 식 매크로라고 할 수 있죠 새로운 Predicate API가 식 매크로의 좋은 예입니다 Predicate 매크로를 통해 클로저로 타입 안정성을 확보하여 predicate 코드를 쓸 수 있습니다 predicate 결괏값은 다른 API에서 사용할 수 있는데 Swift 컬렉션 연산인 SwiftUI와 SwiftData를 포함하죠 매크로 자체는 입력 타입에 대해 일반적입니다 클로저 인수를 받아들이는데 입력 타입의 값으로 운영되는 함수이며 Boolean의 결괏값이 나오죠 입력값이 일치할까요? 매크로는 새로운 Predicate 타입의 인스턴스를 반환하는데 이는 프로그램의 어디에든 사용할 수 있죠
매크로의 이점은 더 있습니다 우리가 작성하는 보일러플레이트 대부분은 우리가 작성한 코드를 다른 것을 도출하는 코드로 보완해야 하기 때문이죠 예를 들어 봅시다 저는 제 코드에 enum을 많이 사용하는데요 상대 또는 절대 경로를 포착하는 Path enum이 그 예죠 하지만 특수한 사례를 확인할 때가 많습니다 모든 절대 경로를 컬렉션에서 필터하려고 하죠 isAbsolute를 산출된 프로퍼티로 작성할 수 있습니다 하지만 언젠가는 또 다른 코드를 작성해야 하죠 갈수록 지루해지는 작업입니다
매크로가 우리를 위해 보일러플레이트를 생성할 수 있죠 @CaseDetection은 첨부된 매크로이며 똑같은 커스텀 속성의 구문을 프로퍼티 래퍼로 사용합니다 Attached 매크로는 적용하는 선언문의 구문인 enum 선언 자체를 입력값으로 간주하여 새 코드를 생성하죠
매크로로 확장된 코드가 정상적인 Swift 코드이며 컴파일러가 여러분의 프로그램에 통합시킵니다 에디터에서 매크로로 생성된 코드를 조사하고 디버그한 뒤 복사하여 커스텀 작업을 추가할 수도 있죠
Attached 매크로는 5개의 역할로 구분할 수 있는데 첨부된 선언을 보완하는 방식에 따라 구분합니다 방금 얘기한 CaseDetection은 @attached(member) 매크로이며 타입이나 익스텐션 안에 새로운 멤버를 생성합니다 peer 매크로는 첨부된 선언과 함께 새로운 선언문을 추가하는데 예를 들면 async 메서드에 Completion Handler를 생성하거나 반대도 가능합니다
accessor는 저장된 프로퍼티를 산출된 프로퍼티로 바꿀 수 있으며 프로퍼티에 접근 시 특정 행동을 수행할 때 사용하거나 실제 저장된 값을 프로퍼티 래퍼와 비슷하지만 더 유연한 방식으로 추상화하죠 Attached 매크로는 새로운 속성을 타입의 특정 멤버에게 소개하고 새 프로토콜 컨포먼스를 추가하죠 Attached 매크로 역할을 함께 작성하여 유용한 효과를 낼 수 있습니다 한 가지 중요한 예는 Observation과 관련돼 있죠 Observation은 늘 SwiftUI의 일부였습니다 클래스 프로퍼티의 변화를 감지하기 위해 ObservableObject에 타입을 맞춰야 하며 모든 프로퍼티를 @Published로 표시하여 ObservedObject 프로퍼티 래퍼를 뷰에 사용해야 합니다 여러 단계가 있지만 한 단계만 빠뜨려도 UI가 예상한 대로 업데이트되지 않죠 매크로 기반의 Observation으로 개선할 수 있습니다
Observable 매크로를 클래스에 첨부하면 클래스에 저장된 모든 프로퍼티를 감지할 수 있죠 저장 프로퍼티에 일일이 주석을 달거나 주석이 없는 걸 걱정하지 않아도 됩니다 Observable 매크로가 모든 걸 처리하니까요
Observable 매크로는 3가지 매크로 역할의 통합이죠 이 역할이 어떻게 함께 작용하는지 알아봅시다 각 매크로의 역할은 Observable 매크로가 Person 클래스를 보완하는 특정한 방식과 연결되죠 member가 새 프로퍼티와 메서드를 소개합니다
member 속성이 @ObservationTracked 매크로를 감지 중인 클래스에 저장된 프로퍼티에 추가하면 해당 클래스가 확장하여 감지 이벤트를 발동합니다 마지막으로 conformance가 Observable 프로토콜에 컨포먼스를 도입하죠
코드의 양이 많아 보이지만 정상적인 Swift 코드이며 Observable 매크로 안에 가지런히 정리돼 있습니다
매크로가 어떻게 확장되는지 알고 싶거나 매크로가 프로그램에 주는 영향을 이해하고 싶다면 Xcode에서 바로 볼 수 있죠
'매크로 확장' 기능을 이용하여 매크로가 확장된 소스 코드를 에디터에서 보세요 매크로로 생성된 코드 내의 오류 메시지가 자동으로 확장된 코드를 보여 주며 디버거를 통해 드나들 수 있습니다
Swift 매크로는 표현성이 좋은 API를 가능케 하는 툴을 제공하고 Swift 코드에서 보일러플레이트를 제거하여 Swift의 표현력을 활용할 수 있게 해 주죠 Macro가 입력값의 타입을 체크하고 정상적인 Swift 코드를 생산하며 프로그램의 지정한 위치에 통합시킴으로써 효과를 쉽게 추론할 수 있습니다 매크로가 어떤 역할을 했는지 이해하고 싶을 때는 에디터에서 확장된 소스 코드를 열람할 수 있습니다 지금까지 매크로의 기본만 다뤘는데요 'Swift 매크로 확장'에서 매크로의 원리를 자세히 다루며 여러분의 궁금증을 해결해 줄 겁니다 여러분이 만든 매크로를 적용하는 방법은 'Swift 매크로 작성'에서 배울 수 있죠 Swift 커뮤니티가 만들 새로운 매크로가 기대됩니다
Swift는 처음부터 확장 가능한 언어로 설계했죠 Swift는 명확하고 간결한 코드를 통한 표현성을 강조하며 양식이 적고 쉽게 읽고 쓸 수 있습니다 Swift의 강력한 기능인 제네릭이나 자체적인 동시성 지원 SwiftUI나 SwiftData 같은 프레임워크를 통해 여러분이 원하는 결과를 빠르게 달성하여 더 중요한 것에 집중할 수 있게 해 주죠
하지만 이러한 고급 능력에도 불구하고 Swift는 효율적입니다 자체적으로 컴파일하고 값 타입이나 쓰레기 수집 대신 참조 횟수를 계산하기 때문에 적은 메모리로 구동할 수 있죠
이러한 확장성 덕분에 Objective-C를 통해 이전보다 더 많은 곳에 Swift를 적용할 수 있으며 C나 C++을 사용할 것으로 예상했던 저급 시스템에도 사용할 수 있습니다 Swift의 명확한 코드와 안정성을 통해 더 많은 곳으로 확장할 수 있죠 최근에 Swift의 Foundation 프레임워크를 새로 작성하는 것을 오픈 소스에 공개했습니다 이 정책을 통해 단일의 공유된 Foundation을 Apple과 다른 플랫폼에 적용할 예정이죠 대량의 Objective-C와 C 코드도 Swift에서 재작성해야 했습니다 macOS Sonoma와 iOS 17부터 새로운 Swift의 적용 사례로 필수 타입인 Date와 Calendar가 있으며 포맷 및 국제화의 필수 요소인 Locale과 AttributedString에 적용되었고 JSON 인코딩 및 디코딩 관련 Swift 적용이 추가됐죠 그 결과 성능 개선이 상당했습니다
Calendar에서 중요한 날짜를 계산하는 능력이 Swift의 값 시맨틱을 통해 중간 할당을 우회하여 일부 벤치마크에서 20%의 개선을 보였죠 Date 포맷의 경우 FormatStyle을 활용하여 놀라운 성능 향상을 통해 150% 증가했습니다 표준 날짜 및 시간 템플릿의 포맷 벤치마크 기준이죠 JSON을 새로운 패키지에 디코딩하는 것은 더욱 놀랍습니다 Foundation은 JSONDecoder와 JSONEncoder에 Swift를 적용하여 Objective-C 컬렉션 타입으로 왕복해야 했던 비용을 제거했죠 Codable 타입 초기화를 위한 Swift의 JSON을 파싱 통합도 성능을 향상시킵니다 새롭게 적용한 기능의 테스트 데이터 파싱 벤치마크는 2배에서 5배까지 빠르죠 이러한 성능 향상은 Objective-C의 Swift 적용에 대한 브리징 비용을 줄인 것도 있지만 Swift를 기반으로 한 새로운 적용 사항이 빠르기 때문입니다
벤치마크 하나를 예로 살펴보죠 Ventura에서 enumerateDates를 Objective-C에서 호출하는 것이 Swift에서 호출하는 것보다 빨랐고 이는 브리징 비용 때문입니다 MacOS Sonoma에서는 Swift에서 같은 기능을 호출 시 20% 빨랐죠 브리징 비용을 제거해서 속도가 빨라진 것도 있지만 새로운 기능 구현 자체가 Objective-C에서 호출했을 때보다 빠릅니다 여기 나오는 날짜 계산은 아주 복잡하지 않으므로 두 언어의 오버헤드가 얼마나 감소했는지 살펴볼 수 있죠 시스템의 저급 단계에서 운영 중이라면 파인 그레인드 컨트롤을 통해 필요한 성능 수준에 도달할 수 있습니다 Swift 5.9는 새로운 옵트인 기능을 도입하여 높은 수준의 제어를 가능하게 해 주죠 이런 기능이 집중하는 건 소유권의 개념인데 값을 '소유'하고 전달하는 코드의 부분을 뜻합니다
언제 이용할 수 있는 기능인지 알아보기 위해서 샘플 코드를 살펴보도록 하죠 이것은 파일 디스크립터를 위한 간단한 래퍼로 저급 시스템 호출의 Swift 인터페이스가 좋아집니다 하지만 이런 API에서도 쉽게 실수할 수 있는데 예를 들어 close를 호출한 뒤 파일에 값을 쓰려고 할 수 있죠 주의가 필요하며 수동으로 닫기 위해 타입이 범위를 벗어나기 전에 close 메서드를 호출해야 합니다 그렇지 않으면 리소스 누수가 생기죠 하나의 해결책은 deinit를 통해 클래스로 만들어 타입이 범위를 벗어날 때 자동으로 닫아줍니다
하지만 이 방법은 다른 단점이 있죠 추가 메모리를 할당해야 하는데 대개는 큰 문제가 아니지만 시스템 컨텍스트 제약이 많을 때는 예외입니다
클래스는 참조 구문도 있죠 의도치 않게 스레드에 걸쳐 파일 디스립터 타입을 공유하거나 경쟁 상태로 이어지거나 의도치 않게 저장할 수 있습니다
일단 구조체 버전을 다시 살펴보죠
이 구조체는 참조 타입처럼 행동합니다 열려 있는 파일인 참 값을 참조하는 정수를 들고 있죠 이 타입을 복사하면 의도치 않게 뮤터블 상태를 프로그램 전반에 걸쳐 공유하여 버그를 일으킬 수 있습니다 이 구조체의 복사본을 만드는 능력을 제한해야 하죠
Swift 타입은 구조체든 클래스든 기본적으로 복사가 가능합니다 대개는 이게 옳은 선택이죠 과도하고 불필요한 복사가 코드에 병목 현상을 일으키지만 인스트루먼트에서 병목 현상을 찾는 데 시간을 쓰는 것이 컴파일러에서 복사를 명시하라고 주기적으로 방해하는 것보다 낫습니다 하지만 암시적인 복사가 의도한 게 아닐 수 있죠 값을 복사하는 것이 파일 디스크립터 래퍼처럼 정확도 문제로 이어질 수 있으니까요 Swift 5.9는 구조체와 이넘 선언에 새로운 구문을 적용하여 그렇게 할 수 있습니다 이를 통해 타입을 복사하는 암시적 능력을 억제하죠 타입이 복사 불가능할 때 클래스에 할 수 있는 것처럼 deinit를 부여하면 타입 값이 범위를 벗어났을 때 실행됩니다
복사 불가능 타입은 close를 호출하고 다른 메서드를 사용하는 문제에도 사용할 수 있죠
close 작업은 consuming으로 지정할 수 있습니다 consuming 메서드나 인수 호출 시 호출한 메서드 값의 소유권을 포기하죠 복사 가능한 타입이 아니므로 소유권을 포기한다는 건 값을 사용하지 않는다는 겁니다
기본적으로 Swift의 메서드는 자신을 포함한 인수를 빌리죠 따라서 write 메서드를 호출하면 파일 디스크립터를 빌리고 그걸 이용하여 버퍼에 쓰고 그다음에는 값의 소유권이 호출자에게 돌아가므로 close와 같은 다른 메서드를 호출할 수 있습니다
하지만 close를 consuming으로 지정하였고 빌리는 것의 기본값이 아니므로 최종으로 사용해야 하죠
이 말은 파일을 먼저 닫고 write 같은 메서드를 호출하면 런타임 오류 대신 컴파일 단계에서 오류 메시지가 나오게 됩니다 컴파일러는 consuming을 언제 사용했는지 나타내죠
복사 불가능한 타입은 Swift의 시스템 수준 프로그래밍의 강력하고 새로운 기능입니다 아직 진화 초기 단계죠 이후의 Swift에는 제네릭 코드로 복사 불가능 타입을 확장할 겁니다 이런 작업의 진척 상황에 관심이 있다면 Swift 포럼에서 활발하게 논의 중이죠 Swift의 성공은 Objective-C와의 상호 운용성 덕분입니다 처음부터 개발자들은 기존의 코드 베이스에 Swift를 단계적으로 도입하기 위해 단일 파일이나 모듈 단위로 Swift를 통합했지만 많은 분이 Objective-C로만 코드를 작성하지는 않았을 겁니다 핵심 사업 로직을 C++로 적용한 프로그램이 많으며 인터페이싱도 쉽지 않았죠 수동 브리징 레이어를 추가하여 Swift에서 Objective-C를 통해 C++까지 작업이 이어졌다가 되돌아갔습니다 Swift 5.9는 C++ 타입 및 함수가 Swift와 직접 상호작용하는 기능을 도입하죠 C++의 상호 운용성은 Objective-C와 똑같이 작동하여 C++ API를 Swift와 동일한 항목에 매핑하여 Swift 코드에서 직접 사용할 수 있습니다 C++는 방대한 언어로 자체의 개념과 관념이 있어 클래스, 메서드와 컨테이너 등이 있죠 Swift 컴파일러는 일반적인 C++ 이디엄을 이해하므로 많은 타입을 직접 사용할 수 있습니다 예를 들어 Person 타입은 5개의 특수 멤버 함수를 정의하며 C++ 값 타입인 복사와 이동 생성자와 대입 연산자와 소멸자를 기대하죠 Swift 컴파일러는 이를 값 타입으로 취급하고 필요할 때 필요한 특수 멤버를 자동으로 호출할 겁니다 추가로 벡터나 맵 같은 C++ 컨테이너는 Swift 컬렉션만큼 접근이 용이하죠
이를 통해 명확한 Swift 코드를 써서 C++ 함수와 타입을 바로 사용할 수 있습니다 Person 인스턴스의 벡터 위에 필터를 적용할 수 있고 C++ 멤버 함수를 호출하고 데이터 멤버에 직접 접근하죠
반대 방향으로 C++에서 Swift 코드를 사용하는 건 Objective-C와 같은 메커니즘을 기반으로 합니다 Swift 컴파일러가 만든 생성된 헤더에는 Swift API의 C++ 뷰를 포함하죠 하지만 Objective-C와는 달리 objc 속성으로 주석이 달린 Swift 클래스만 사용해야 하는 제약이 없습니다 C++는 대부분의 Swift 타입과 전체 API를 바로 사용할 수 있으며 프로퍼티, 메서드와 이니셜라이저를 포함하며 오버헤드 브리징도 요구하지 않죠 C++가 Swift의 Point 구조체를 활용하는 법을 살펴보겠습니다 생성된 헤더를 포함한 뒤 C++가 Swift 이니셜라이저를 호출해 Point 인스턴스를 생성하고 변하는 메서드를 인보크하고 저장 및 산출된 프로퍼티에 모두 접근하는데 Swift 코드를 전혀 수정하지 않았습니다
Swift의 C++ 상호 운용성으로 기존의 C++ 코드 베이스와 Swift를 통합하는 것이 어느 때보다 쉬워졌죠 다수의 C++ 이디엄을 Swift에서 직접 표현할 수 있고 이는 자동으로 이루어지지만 때로는 목표 시맨틱을 나타내는 주석을 요구할 수 있습니다 Swift API는 C++에서 직접 접근할 수 있고 주석이나 코드 수정이 필요하지 않으며 C, C++, Objective-C가 혼합된 코드 베이스에 점진적으로 Swift를 도입하는 게 가능하죠
C++ 상호 운용성은 아직도 진화하고 있으며 C++ 상호 운용성 워크그룹이 주도하고 있습니다 'Swift와 C++ 혼합하기' 세션을 시청하거나 Swift 포럼 토론에 참여하여 더 많은 정보를 얻어 가세요
언어 수준의 상호 운용성은 정말 중요합니다 하지만 여러분의 코드를 빌드할 수 있어야 하죠 기존의 빌드 시스템을 Xcode나 Swift Package Manager로 대체하거나 Swift를 시작하는 건 엄청난 양의 코드를 다시 쓰는 것만큼 큰 장애물입니다 그래서 우리가 CMake 커뮤니티와 작업하여 CMake의 Swift 지원을 개선했죠 CMake 빌드에 Swift 코드를 통합하는 방법은 Swift를 프로젝트 언어로 선언하고 Swift 파일을 타겟에 넣는 겁니다
더 중요한 건 단일 타겟 안에 C++와 Swift를 혼합하면 CMake가 따로 컴파일하고 적절한 지원 라이브러리와 런타임을 두 언어에 연결한다는 거죠 교차 플랫폼 C++ 프로젝트에 파일과 타겟 별로 Swift를 도입할 수 있다는 의미입니다 또한 샘플 저장소를 제공하는데 Swift를 포함한 CMake 프로젝트나 C++/Swift 혼합 타겟 브리징 및 생성 헤더의 사용이 포함되어 여러분의 시작을 돕죠
몇 년 전에는 Swift에 새로운 동시성 모델을 도입했는데 async/await나 구조화된 동시성, 행위자 등의 기본 요소를 바탕으로 했습니다 Swift의 동시성 모델은 추상 모델이며 다양한 환경과 라이브러리에 도입할 수 있죠 추상 모델의 주요 부분이 2개인데 태스크와 행위자입니다 태스크는 어디서나 실행할 수 있는 순차적인 작업 단위를 대표하죠 태스크는 프로그램에 'await'가 있으면 유보되며 다시 계속할 수 있을 때 재개됩니다
행위자는 동기화 메커니즘으로 고립된 상태에 상호 배제된 접근을 제공하죠 외부에서 행위자에 진입하려면 await가 요구되는데 태스크를 유보할 수 있기 때문입니다
태스크와 행위자는 추상 언어 모델에 통합되지만 해당 모델 안에서 다양한 방식으로 다양한 환경에 적용할 수 있죠 태스크는 전역 동시성 풀에서 실행됩니다 전역 동시성 풀의 스케줄링 방식은 환경에 달려 있죠 Apple의 플랫폼은 Dispatch 라이브러리가 전체 운영 체제에 대한 최적화 스케줄링을 제공하며 각 플랫폼에 광범위하게 맞춰져 있습니다 더 제한적인 환경에서는 멀티스레드 스케줄러의 오버헤드가 받아들여지지 않을 수 있죠 그런 곳에 적용되는 Swift의 동시성 모델은 싱글 스레드의 협조적인 큐를 지원합니다 같은 Swift 코드가 두 환경에서 작동하는데 추상 모델이 아주 유연하여 다양한 런타임 환경을 매핑할 수 있기 때문이죠
또한 콜백 기반의 라이브러리와의 상호 운용성을 Swift의 async/await 지원에 처음부터 구축했습니다 withCheckedContinuation 작업은 태스크 하나를 유보했다가 콜백에 대한 반응으로 재개할 수 있죠 이를 통해 태스크를 자체적으로 관리하는 기존 라이브러리와 통합할 수 있습니다
Swift의 동시성 런타임 내 행위자의 기본적인 적용은 행위자에 실행할 수 있는 록프리 큐의 태스크지만 그것이 유일한 적용 방법은 아니죠 더 제한적인 환경에는 원자성이 없을 수도 있고 스핀록 같은 다른 동시성 프리미티브를 사용할 수 있습니다 그 환경이 싱글 스레드라면 동기화가 필요 없지만 프로그램과 상관없이 행위자 모델이 추상 동시성 모델로 남죠 같은 코드를 멀티 스레드인 다른 환경으로 가져갈 수 있습니다 Swift 5.9에서는 커스텀 행위자 실행자가 특정 행위자에게 자체 동기화 메커니즘을 적용하게 하죠 그럼 행위자가 기존 환경에 대해 유연성과 적응성을 확보합니다 예를 들어 보죠 데이터베이스 연결을 관리하는 행위자를 가정해 봅시다 Swift가 행위자의 저장소로 상호 배제된 접근을 제공하여 데이터베이스에 대한 동시성 접근이 없을 겁니다 하지만 동기화를 완성하는 특정한 방식에 대한 제어가 더 필요하면 어떨까요? 예를 들어 데이터베이스 연결에 특정 디스패치 큐를 사용하고 싶다고 합시다 해당 큐가 행위자를 도입하지 않는 다른 코드와 공유되고 있기 때문이죠 커스텀 행위자 실행자로 그렇게 할 수 있습니다
여기서 순차적인 디스패치 큐를 행위자에 추가하였고 소유되지 않은 실행자 프로퍼티를 적용했는데 이는 디스패치 큐에 상응하는 실행자를 생산하죠 이러한 변화로 행위자 인스턴스의 모든 동기화는 해당 큐를 통해 진행됩니다
행위자 밖에서 pruneOldEntries 호출에 await를 실행한다면 상응하는 큐에 디스패치 비동기를 실행하죠 이를 통해 개별 행위자가 동기화를 제공하는 방식을 제어하고 Objective-C나 C++로 작성된 코드여서 아직 행위자를 사용하지 않는 코드와 행위자를 동기화하는 것도 가능해집니다
디스패치 큐를 통한 행위자의 동기화가 가능해진 건 디스패치 큐가 SerialExecutor 프로토콜을 따르기 때문이죠 행위자에 사용할 자체 동기화 메커니즘을 제공하려면 이 프로토콜에 따르는 새로운 타입을 정의해야 하는데 몇 가지 핵심 작업만 진행하면 됩니다 실행자의 컨텍스트에서 코드가 실행되는지 확인하는 거죠 예를 들어 메인 스레드에서 실행되고 있는지 확인하는 겁니다
소유하지 않는 참조를 실행자로 추출하여 접근을 보장하면서 참조 횟수 계산 트래픽이 줄죠 가장 핵심 작업인 enqueue는 실행자 'job'의 소유권을 가져갑니다 job은 실행자에서 동기화되어 실행돼야 하는 비동기적인 태스크의 일부죠 enqueue를 호출한 시점에 실행자가 책임을 지고 SerialExecutor에 다른 코드가 실행 중이지 않을 때 해당 job을 실행해야 합니다 예로, 디스패치 큐의 enqueue는 큐의 디스패치 비동기를 호출하죠
Swift 동시성은 몇 년간 사용 중이며 추상 모델에 포함된 태스크와 행위자가 광범위한 동시성 프로그래밍 태스크를 아우릅니다 추상 모델 자체는 꽤 유연하여 서로 다른 실행 환경인 iPhone, Apple Watch 서버와 다른 곳에도 도입할 수 있죠 주요 지점의 커스텀화를 허용하여 Swift 동시성을 도입하지 않은 코드와도 상호 운용이 가능합니다 더 많은 정보를 원하시면 '뒷이야기' 세션을 확인하세요 '구조화된 동시성의 기초를 너머' 영상도 확인하세요 이제 마무리하면서 Swift 운영 사례를 살펴보죠 우리에게 익숙한 iOS나 MacOS 앱과 전혀 다른 환경에서 운영한 사례입니다 확장성 있는 솔루션을 제공하는 분산 데이터베이스 FoundationDB는 대규모 하드웨어에서 실행하는 엄청난 규모의 키-값 저장과 MacOS, 리눅스, 윈도우 등 다양한 플랫폼을 지원합니다 FoundationDB는 C++로 코드를 작성한 오픈 소스 프로젝트죠 코드는 매우 비동기적이며 자체적인 형태의 분산 행위자와 런타임을 통해 매우 중요한 결정론적 시뮬레이션 환경을 테스트 목적으로 제공합니다 FoundationDB는 코드 베이스를 현대화하려고 노력했고 Swift가 DB의 성능과 안전성, 코드 명확성에 잘 맞는다고 판단했죠 코드를 완전히 다시 쓰는 건 규모가 크고 위험한 시도입니다 대신 Swift의 상호 운용성을 활용하여 기존 코드 베이스에 통합하였습니다 예를 들어 FoundationDB의 마스터 데이터 행위자를 C++ 적용한 것의 일부인데
내용이 너무 많고 모든 C++를 이해할 필요도 없죠 대신 코드의 핵심 특성을 짚어 보겠습니다 먼저, C++는 async/await가 없죠 따라서 FoundationDB에는 전처리기와 같은 방식으로 이를 모방합니다
많은 C++ 코드 베이스처럼 자체적인 C++ Future 타입을 적용하여 비동기 태스크를 관리하죠 이와 함께 명시적 메시징을 통해 요청에 반응을 보냅니다 답을 전송하는 것과 함수의 결괏값을 조심스럽게 짝짓는 걸 볼 수 있죠 마지막으로 FoundationDB에는 참조 횟수를 계산하는 스마트 포인터로 메모리를 자동으로 관리합니다 이 모든 것들을 Swift로 더 깔끔하게 적용할 수 있죠
훨씬 낫네요 이 함수는 Swift에서 직접 비동기 함수로 적용할 수 있죠 정상적인 리턴 타입과 리턴 문이 있어서 이 요청에 대한 응답을 제공하여 항상 동기화되어 있습니다 await를 통해 유보 지점을 표시하는 건 다른 Swift 비동기 코드와 같은 방식이죠 이 Swift 코드는 C++ Future 타입과 연동되며 후속문을 이용하여 적용됩니다
여기서 여러 개의 C++ 타입을 사용하는데 C++ MasterData 타입은 참조 횟수 계산 스마트 포인터를 사용했죠 C++에서 타입의 주석을 달면 Swift 컴파일러가 이 타입을 다른 클래스처럼 사용하여 참조 횟수를 자동으로 관리해 줍니다
다른 타입인 요청이나 응답 타입은 Swift에서 직접 사용하고 있는 C++ 값 타입이죠 상호 운용성은 양쪽으로 진행됩니다 여기 비동기 함수는 Swift 동시성 모델로 인해 도입된 작업으로 FoundationDB 고유의 결정론적 런타임에서 실행돼서 Swift의 장점을 원하는 곳에서 사용하고 기존 C++과 인터페이싱하여 점진적 채택을 가능하게 하죠
이 세션에서 많은 주제를 다뤘습니다 더 표현적인 API를 가능하게 하는 매개변수 팩이나 매크로의 기능을 설명하여 좋은 코드를 빠르게 작성하도록 해 줬죠 성능에 민감한 코드에 Swift를 사용하는 것과 복사 불가능 타입의 도입을 통해 참조 횟수 계산 오버헤드 없는 리소스 관리 제공을 다뤘습니다 그다음에는 C++ 상호 운용성을 통한 Swift의 C++ API 지원과 그 반대의 경우를 살펴봤죠 이를 통해 Swift의 이점을 코드에 사용할 수 있습니다
마지막으로 Swift의 유연한 동시성 모델을 이야기했는데 다양한 환경에 채택하여 각종 기기와 언어를 통해 동시성이 쉽고 안전해집니다 매개변수 팩, 매크로 복사 불가능 타입과 Swift 5.9의 모든 언어 개선 사항은 Swift 진화 절차를 통해 공개적으로 개발됐고 커뮤니티 피드백이 기능을 만드는 데 크게 기여했죠 Swift 5.9는 커뮤니티 일원들의 노력과 적극적인 설계 논의, 버그 리포트 풀 리퀘스트, 교육적인 콘텐츠의 정점입니다 Swift 5.9를 훌륭하게 만들어 주셔서 감사합니다 ♪ ♪
-
-
3:06 - Hard-to-read compound ternary expression
let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ "
-
3:19 - Familiar and readable chain of if statements
let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " }
-
3:30 - Initializing a global variable or stored property
let attributedName = AttributedString(markdown: displayName)
-
3:46 - In 5.9, if statements can be an expression
let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" }
-
4:31 - In Swift 5.7, errors may appear in a different place
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { // The issue actually occurs here Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
4:47 - In Swift 5.9, you now receive a more accurate compiler diagnostic
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 } } }
-
5:47 - An API that takes a request type and evaluates it to produce a strongly typed value
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) }
-
6:03 - APIs that abstract over concrete types and varying number of arguments
let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
-
6:35 - Writing multiple overloads for the evaluate function
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)
-
6:47 - Overloads create an arbitrary upper bound for the number of arguments
//This will cause a compiler error "Extra argument in call" let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
-
7:12 - Individual type parameter
<each Result>
-
7:36 - Collapsing the same set of overloads into one single evaluate function
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
-
8:21 - Calling updated evaluate function looks identical to calling an overload
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) } let results = RequestEvaluator.evaluate(r1, r2, r3)
-
10:01 - It isn't clear why an assert function fails
assert(max(a, b) == c)
-
10:20 - XCTest provides an assert-equal operation
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
-
11:02 - Assert as a macro
#assert(max(a, b) == c)
-
11:42 - Macros are distributed as packages
import PowerAssert #assert(max(a, b) == c)
-
12:07 - Macro declaration for assert
public macro assert(_ condition: Bool)
-
12:26 - Uses are type checked against the parameters
import PowerAssert #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
-
12:52 - A macro definition
public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:11 - Swift compiler passes the source code for the use of the macro
#assert(a == b)
-
13:14 - Compiler plugin produces new source code, which is integrated back into the Swift program
PowerAssert.Assertion( "#assert(a == b)" ) { $0.capture(a, column: 8) == $0.capture(b, column: 13) }
-
13:33 - Macro declarations include roles
// Freestanding macro roles @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:53 - New Foundation Predicate APIs uses a `@freestanding(expression)` macro role
let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred)
-
14:14 - Predicate expression macro
// Predicate expression macro @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input>
-
14:48 - Example of a commonly used enum
enum Path { case relative(String) case absolute(String) }
-
15:01 - Checking a specific case, like when filtering all absolute paths
let absPaths = paths.filter { $0.isAbsolute }
-
15:09 - Write an `isAbsolute` check as a computer property...
extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } }
-
15:12 - ...And another for `isRelative`
extension Path { var isRelative: Bool { if case .relative = self { true } else { false } } }
-
15:17 - Augmenting the enum with an attached macro
@CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute }
-
15:36 - Macro-expanded code is normal Swift code
enum Path { case relative(String) case absolute(String) //Expanded @CaseDetection macro integrated into the program. var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } }
-
16:57 - Observation in SwiftUI prior to 5.9
// Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:25 - Observation now
// Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:42 - Observable macro works with 3 macro roles
@attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...).
-
17:52 - Unexpanded macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
18:05 - Expanded member attribute role
@Observable final class Person { var name: String var age: Int var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:12 - Member attribute role adds `@ObservationTracked` to stored properties
@Observable final class Person { @ObservationTracked var name: String @ObservationTracked var age: Int @ObservationTracked var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:16 - The @ObservationTracked macro adds getters and setters to stored properties
@Observable final class Person { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:33 - All that Swift code is folded away in the @Observable macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
23:59 - A wrapper for a file descriptor
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) } }
-
24:30 - The same FileDescriptor wrapper as a class
class 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) } deinit { self.close(fd) } }
-
25:05 - Going back to the struct
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) } }
-
26:06 - Using Copyable in the FileDescriptor struct
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) } }
-
26:35 - `close()` can also be marked as 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) } }
-
26:53 - When `close()` is called, it must be the final use
let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close()
-
27:20 - Compiler errors instead of runtime failures
let file = FileDescriptor(fd: descriptor) file.close() // Compiler will indicate where the consuming use is file.write(buffer: data) // Compiler error: 'file' used after consuming
-
28:52 - Using C++ from Swift
// 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)!") } }
-
29:51 - Using Swift from C++
// Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = “origin” mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, “unit”) unit.moveBy(2, -2) std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl; }
-
35:30 - An actor that manages a database connection
// Custom actor executors actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> init(filename: String) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
35:58 - MyConnection with a serial dispatch queue and a custom executor
actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> private let queue: DispatchSerialQueue nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
36:44 - Dispatch queues conform to SerialExecutor protocol
// Executor protocols protocol Executor: AnyObject, Sendable { func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { func asUnownedSerialExecutor() -> UnownedSerialExecutor func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … }
-
39:22 - C++ implementation of FoundationDB's "master data" actor
// C++ implementation of FoundationDB’s “master data” actor ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { req.reply.send(itr->second); return Void(); } // ... }
-
40:18 - Swift implementation of FoundationDB's "master data" actor
// Swift implementation of FoundationDB’s “master data” actor func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { myself.getCommitVersionRequests += 1 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.