<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>coding on liang2kl's Blog</title><link>https://yeshe.ng/blog/tags/coding/</link><description>Recent content in coding on liang2kl's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>liang2kl@outlook.com (Liang Yesheng)</managingEditor><webMaster>liang2kl@outlook.com (Liang Yesheng)</webMaster><copyright>from 2021, to infinity and beyond...</copyright><lastBuildDate>Wed, 08 Jun 2022 22:27:07 +0800</lastBuildDate><atom:link href="https://yeshe.ng/blog/tags/coding/index.xml" rel="self" type="application/rss+xml"/><item><title>WWDC22 Sessions 记录</title><link>https://yeshe.ng/blog/posts/2022/06/wwdc22/</link><pubDate>Wed, 08 Jun 2022 22:27:07 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/06/wwdc22/</guid><description>记录 WWDC22 上发布的新 API 和新技术</description><content>&lt;p>虽然 iOS 16 和 macOS 13 并没有太大的变化，但是 SDK（特别是 SwiftUI）更新的东西相当多，解决了很多以前开发中的痛点。&lt;/p>
&lt;p>目录（根据观看时间顺序，大致按照发布顺序）：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="#meet-swift-regex">Meet Swift Regex&lt;/a>&lt;/li>
&lt;li>&lt;a href="#meet-desktop-class-ipad">Meet desktop class iPad&lt;/a>&lt;/li>
&lt;li>&lt;a href="#the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/a>&lt;/li>
&lt;li>&lt;a href="#hello-swift-charts">Hello Swift Charts&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/a>&lt;/li>
&lt;li>&lt;a href="#compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/a>&lt;/li>
&lt;li>&lt;a href="#embrace-swift-generics">Embrace Swift generics&lt;/a>&lt;/li>
&lt;li>&lt;a href="#design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="meet-swift-regex">Meet Swift Regex&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110357/">https://developer.apple.com/videos/play/wwdc2022/110357/&lt;/a>&lt;/p>
&lt;p>Swift 5.7 新增了强类型的正则表达式类型，自动将字符串转化为需要的类型，支持编译器检查。另外还提供了 &lt;code>RegexBuilder&lt;/code> 框架，使用 SwiftUI 所使用的 result builder 语法来写正则表达式，非常具有表达力。编译器帮忙检查的 regex 和强类型的 capture 太香了。&lt;/p>
&lt;p>Regex literal，强类型静态正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex literals&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> digits = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d&lt;span style="color:#f92672">+/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// digits: Regex&amp;lt;Substring&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Regex Builder，使用 result builder 构建正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/02/2022 Payroll from employer $200.23&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/03/2022 Suspect A $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/03/2022 Ted&amp;#39;s Pet Rock Sanctuary $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/05/2022 Doug&amp;#39;s Dugout Dogs $33.27&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">RegexBuilder&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> fieldSeparator = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>s{&lt;span style="color:#ae81ff">2&lt;/span>,}&lt;span style="color:#f92672">|&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>t&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> transactionMatcher = Regex {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { &lt;span style="color:#f92672">/&lt;/span>CREDIT&lt;span style="color:#f92672">|&lt;/span>DEBIT&lt;span style="color:#f92672">/&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.date(.numeric, locale: Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>), timeZone: .gmt)) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OneOrMore {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NegativeLookahead { fieldSeparator }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CharacterClass.any
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.localizedCurrency(code: &lt;span style="color:#e6db74">&amp;#34;USD&amp;#34;&lt;/span>).locale(Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>))) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// transactionMatcher: Regex&amp;lt;(Substring, Substring, Date, Substring, Decimal)&amp;gt;}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Named captures，给各个 group 起名（返回的结果是一个 &lt;code>Tuple&lt;/code>，因此可以在编译期加上标识）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> regex = #&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;date&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">4&lt;/span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;middle&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>P{currencySymbol}&lt;span style="color:#f92672">+&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;currency&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>p{currencySymbol})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">/&lt;/span>#
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex&amp;lt;(Substring, date: Substring, middle: Substring, currency: Substring)&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="meet-desktop-class-ipad">Meet desktop class iPad&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10069/">https://developer.apple.com/videos/play/wwdc2022/10069/&lt;/a>&lt;/p>
&lt;p>针对 iPad 生产力问题作的改进，推出了更像 macOS 的 toolbar。不过，这远远不能解决 iPad 缺少足够 productive 的应用的问题。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-ipad.webp" alt="">&lt;/p>
&lt;h2 id="the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10054/">https://developer.apple.com/videos/play/wwdc2022/10054/&lt;/a>&lt;/p>
&lt;p>苹果终于改进了 SwiftUI 上视图导航的逻辑。简单来说就是以前只能傻傻地打开另一个视图而无法获取之前的视图的情况，现在可以获取并可以用程序控制当前所在的层级了。&lt;/p>
&lt;p>官方的示例代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">SwiftUI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Pushable stack&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PushableStack&lt;/span>: View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @State &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> path: [Recipe] = []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @StateObject &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> dataModel = DataModel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List(Category.allCases) { category &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Section(category.localizedName) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(dataModel.recipes(&lt;span style="color:#66d9ef">in&lt;/span>: category)) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationTitle(&lt;span style="color:#e6db74">&amp;#34;Categories&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .environmentObject(dataModel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>相比于之前 &lt;code>NavigationLink&lt;/code> 提供一个 &lt;code>View&lt;/code>，现在可以提供一个用于区分不同目标的值来代替（这样就可以记录栈上的内容）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，使用 &lt;code>navigationDestination()&lt;/code> 来返回对应的 &lt;code>View&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用新的 &lt;code>NavigationStack&lt;/code>，我们传递一个 &lt;code>path&lt;/code> 来获取当前导航栈上的所有 destination 所对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们可以通过更改 &lt;code>path&lt;/code> 来随意更改导航栈：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>path.removeAll()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，还可以通过 &lt;code>SceneStorage&lt;/code> 持久化当前的状态。&lt;/p>
&lt;h2 id="hello-swift-charts">Hello Swift Charts&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10136/">https://developer.apple.com/videos/play/wwdc2022/10136/&lt;/a>&lt;/p>
&lt;p>官方的图表框架，使用 SwiftUI 快速构建图表，非常简洁。比如，创建折线图：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>Chart(partyTasksRemaining) { element &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LineMark(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x: .value(&lt;span style="color:#e6db74">&amp;#34;Date&amp;#34;&lt;/span>, element.date, unit: .day),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> y: .value(&lt;span style="color:#e6db74">&amp;#34;Tasks Remaining&amp;#34;&lt;/span>, element.remainingCount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .symbol(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后效果很不错（也很苹果）：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart.webp" alt="">&lt;/p>
&lt;p>声明式语法真是太适合做图表了，matplotlib 虽然强大但是画个图挺麻烦的，在想着以后科研能不能用 Swift 画图（&lt;/p>
&lt;h2 id="whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10052/">https://developer.apple.com/videos/play/wwdc2022/10052/&lt;/a>&lt;/p>
&lt;p>除了上面提到的 Swift Charts 和 navigation 之外，这次 SwiftUI 的主要更新还有：&lt;/p>
&lt;h3 id="window-和-menu-bar-extras">Window 和 Menu bar extras&lt;/h3>
&lt;p>macOS 可以创建单独的、可以控制显示/关闭的窗口了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WindowGroup(&lt;span style="color:#e6db74">&amp;#34;Party Planner&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PartyPlannerHome()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Window(&lt;span style="color:#e6db74">&amp;#34;Party Budget&amp;#34;&lt;/span>, id: &lt;span style="color:#e6db74">&amp;#34;budget&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .keyboardShortcut(&lt;span style="color:#e6db74">&amp;#34;0&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，终于支持状态栏 app 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MenuBarExtra(&lt;span style="color:#e6db74">&amp;#34;Bulletin Board&amp;#34;&lt;/span>, systemImage: &lt;span style="color:#e6db74">&amp;#34;quote.bubble&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BulletinBoard()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .menuBarExtraStyle(.window)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="resizable-sheets">Resizable sheets&lt;/h3>
&lt;p>加入了去年 UIKit 新增的可变高度的 sheet：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.sheet(isPresented: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>presented) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDetents([.height(&lt;span style="color:#ae81ff">250&lt;/span>), .medium])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDragIndicator(.visible)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="其他">其他&lt;/h3>
&lt;p>其他的更新包括：&lt;/p>
&lt;ul>
&lt;li>macOS 上 &lt;code>Form&lt;/code> 新的外观&lt;/li>
&lt;li>支持多行的 &lt;code>TextField&lt;/code> 和限定行高范围&lt;/li>
&lt;li>iPad 支持 &lt;code>Table&lt;/code>&lt;/li>
&lt;li>可自定义的 toolbar&lt;/li>
&lt;li>可自定义的 layout&lt;/li>
&lt;/ul>
&lt;h2 id="whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10068/">https://developer.apple.com/videos/play/wwdc2022/10068/&lt;/a>&lt;/p>
&lt;p>现在基本不写 UIKit 了，这部分就没怎么记录。比较有意思的是，现在创建 &lt;code>UICollectionView&lt;/code> 和
&lt;code>UITableView&lt;/code> 可以用 SwiftUI 来写 cell 的 UI 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>cell.contentConfiguration = UIHostingConfiguration {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Image(systemName: &lt;span style="color:#e6db74">&amp;#34;wand.and.stars&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Like magic!&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title2).bold()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(Color.purple)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其他的更新，如新的 toolbar，到暑假可以用来改进一下 Whiz Reader（不过可能用 SwiftUI 重写而不是继续用 UIKit）。&lt;/p>
&lt;h2 id="compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10056/">https://developer.apple.com/videos/play/wwdc2022/10056/&lt;/a>&lt;/p>
&lt;p>支持自定义布局应该是本次 SwiftUI 最重要的更新了。现在我们可以访问到一些以前获取不到的信息来放置子视图了。&lt;/p>
&lt;h3 id="grid">Grid&lt;/h3>
&lt;p>在介绍自定义布局之前，首先介绍了新增的 &lt;code>Grid&lt;/code> 类型，可以构建静态的对齐与网格的布局。&lt;/p>
&lt;p>与之前的 &lt;code>LazyHGrid&lt;/code> 和 &lt;code>LazyVGrid&lt;/code> 不同，&lt;code>Grid&lt;/code> 不是懒加载的，因此高/宽是确定的，适合于静态 grid 的构建。&lt;/p>
&lt;p>与 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 的组合相比，&lt;code>Grid&lt;/code> 的各行/列可以自动根据最大元素调整大小，而 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 不行。比如，现在可以实现这样一个列表：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-grid.webp" alt="">&lt;/p>
&lt;p>左侧的名称一栏自动将宽度调整到适合于最大的 &lt;code>Goldfish&lt;/code>，右侧同理。&lt;/p>
&lt;blockquote>
&lt;p>在以前，使用 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 想要实现各列大小一致，只能给各列指定固定的大小。比如，&lt;a href="https://github.com/treehollow/treehollow-v3-ios/blob/main/Source/Hollow/View/Hierarchy/Hollow/Content/HollowCommentContentView.swift">树洞的评论界面&lt;/a>大概是这样实现的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(comments) { comment &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .fixedSize(.horizontal, &lt;span style="color:#ae81ff">20&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当 &lt;code>comment.name&lt;/code> 过长时，就会变成两行，非常影响体验。&lt;/p>
&lt;/blockquote>
&lt;h3 id="自定义布局">自定义布局&lt;/h3>
&lt;p>自定义布局对应一个新的协议：&lt;code>Layout&lt;/code>。&lt;/p>
&lt;p>&lt;code>Layout&lt;/code> 通过两个函数参与布局过程：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">sizeThatFits&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) -&amp;gt; CGSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">placeSubviews&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">in&lt;/span> bounds: CGRect,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一个函数 &lt;code>sizeThatFits()&lt;/code> 通过返回一个大小来告诉父 &lt;code>View&lt;/code> 我们需要多少空间；第二个函数 &lt;code>placeSubviews()&lt;/code> 将所有的子 &lt;code>View&lt;/code> 放置在合适的位置（使用我们在 &lt;code>sizeThatFits&lt;/code> 中要求的空间）。&lt;/p>
&lt;h4 id="sizethatfits">sizeThatFits&lt;/h4>
&lt;p>我们先来看 &lt;code>sizeThatFits()&lt;/code>。它有三个参数：&lt;/p>
&lt;ul>
&lt;li>&lt;code>proposal&lt;/code>：父 &lt;code>View&lt;/code> 所期望这个布局的大小。我们（可以）根据 &lt;code>proposal&lt;/code> 的大小来决定我们最终要占据多少空间，当然也可以忽略&lt;/li>
&lt;li>&lt;code>subviews&lt;/code>：布局中所有的子 &lt;code>View&lt;/code>。这并不是 &lt;code>View&lt;/code> 本身，而是它的“代理”（proxy），仅给我们提供必要的信息和接口&lt;/li>
&lt;li>&lt;code>cache&lt;/code>：可用来缓存中间计算结果&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>关于 &lt;code>proposal&lt;/code>，官方文档是这样介绍的：&lt;/p>
&lt;p>A proposed size for the subview. In SwiftUI, views choose their own size, but can take a size proposal from their parent view into account when doing so.&lt;/p>
&lt;p>可以看到，&lt;code>proposal&lt;/code> 是父视图对子视图大小的“建议”。父视图在进行布局时，将建议的大小传入每个子视图，子视图经过计算返回最终的大小（可以是 &lt;code>proposal&lt;/code> 的大小，也可以不是），父视图最终根据子视图返回的大小来进行布局的计算（而不是它所认为的大小，即 &lt;code>proposal&lt;/code>）。&lt;/p>
&lt;/blockquote>
&lt;p>我们以官方的例子为例。这个例子实现了一个 &lt;code>EqualWidthHStack&lt;/code>，有几个需求：&lt;/p>
&lt;ul>
&lt;li>水平布局&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 大小相同&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 的大小都等于最大的子 &lt;code>View&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>对于这个 layout，&lt;code>sizeThatFits()&lt;/code> 如下：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-sizethatfits.webp" alt="">&lt;/p>
&lt;p>原理很简单：计算子 &lt;code>View&lt;/code> 的最大的 size，加上之间的 spacing，即是布局需要的大小。我们通过 &lt;code>LayoutSubview&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 获取每个子 &lt;code>View&lt;/code> 的大小，然后计算最大值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">maxSize&lt;/span>(subviews: Subviews) -&amp;gt; CGSize {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CGSize(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> width: max(currentMax.width, subviewSize.width),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> height: max(currentMax.height, subviewSize.height))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> maxSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>sizeThatFits()&lt;/code> 中，我们忽略了 &lt;code>proposal&lt;/code>，这样在父视图的任何大小下这个布局所占的空间都是一样的。如果需要更灵活的布局，如空间不足时自动缩小，可以根据不同的 &lt;code>proposal&lt;/code> 返回不同的大小。&lt;/p>
&lt;h4 id="placesubviews">placeSubviews&lt;/h4>
&lt;p>&lt;code>placeSubviews()&lt;/code> 相较于 &lt;code>sizeThatFits()&lt;/code> 多了一个参数 &lt;code>bounds&lt;/code>，其余参数均相同；我们通过调用 &lt;code>LayoutSubview&lt;/code> 的方法来放置子视图。其实完全可以理解为两个函数是同一个函数的两个部分，&lt;code>sizeThatFits()&lt;/code> 用于计算视图大小，&lt;code>placeSubviews()&lt;/code> 用我们计算好的大小来放置视图；只是因为上层调用是两个不连续的过程，才将它们分开为两个函数。&lt;/p>
&lt;p>因此，这两个函数一般具有部分相同的逻辑，比如上面的 &lt;code>maxSize()&lt;/code> 和 &lt;code>spacing()&lt;/code>，最好将其写到一个新的函数中以便复用，或者直接将中间结果放到 &lt;code>cache&lt;/code> 中（待考证）。&lt;/p>
&lt;p>实现 &lt;code>EqualWidthHStack&lt;/code> 的 &lt;code>placeSubviews()&lt;/code> 很简单，我们只需要通过相同的方法计算 &lt;code>maxSize&lt;/code> 和 &lt;code>spacing&lt;/code>，然后将每个 &lt;code>subView&lt;/code> 放置在合适的位置就可以了。这个过程通过 &lt;code>LayoutSubView&lt;/code> 的 &lt;code>place()&lt;/code> 方法完成。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-placesubviews.webp" alt="">&lt;/p>
&lt;h4 id="传递额外信息">传递额外信息&lt;/h4>
&lt;p>这种方式有一个问题：传入的 &lt;code>subviews&lt;/code> 只是视图的代理，不带有视图的信息。如果我们需要传递子视图的额外信息用于布局的计算，官方提供了一种（强类型的）方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Rank&lt;/span>: LayoutValueKey {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> defaultValue: Int = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MyLayout(&lt;span style="color:#ae81ff">1.&lt;/span>..&lt;span style="color:#ae81ff">10&lt;/span>) { rank &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>rank&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .layoutValue(key: Rank.&lt;span style="color:#66d9ef">self&lt;/span>, value: rank)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用下标运算符，我们可以从 &lt;code>subview&lt;/code> 中获取对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> ranks = subviews.map { subview &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subview[Rank.&lt;span style="color:#66d9ef">self&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样就实现了额外信息的传递。&lt;/p>
&lt;h3 id="自适应布局">自适应布局&lt;/h3>
&lt;p>有意思的是，官方还实现了一个能够根据可用空间的大小自动调整布局的布局 &lt;code>ViewThatFits&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>ViewThatFits {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EqualWidthHStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当空间足够时，选择第一个 &lt;code>EqualWidthHStack&lt;/code>；空间不够时，选择所需空间更少的 &lt;code>VStack&lt;/code>。&lt;/p>
&lt;p>其实这个非常 fancy 的布局的实现应该很简单：根据 &lt;code>EqualWidthHStack&lt;/code> 和 &lt;code>VStack&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 的返回值，按顺序选择能够在可用空间内放置的一个。当然，我们没办法在自定义排布中选择哪些子视图不显示，因此是没办法完美地手动实现一个类似的容器的（可以通过不给子视图分配空间来实现，不过这样子视图仍然会加载）。&lt;/p>
&lt;h3 id="anylayout">AnyLayout&lt;/h3>
&lt;p>最后，官方提供了一个 type-erased 的 &lt;code>Layout&lt;/code>，即 &lt;code>AnyLayout&lt;/code>，可以用于动态改变布局类型：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> layout = isThreeWayTie ? AnyLayout(HStack())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : AnyLayout(MyRadialLayout())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>layout {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(pets) { pet &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Avatar(pet: pet)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.animation(...)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，由于 &lt;code>layout&lt;/code> 中的 &lt;code>View&lt;/code>（在这里是若干个 &lt;code>Avatar&lt;/code>）在不同的布局下具有相同的 view identity，因此布局变化前后子视图是不变的。通过这一点，我们甚至可以为不同布局的变化加上动画。&lt;/p>
&lt;h2 id="swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10137/">https://developer.apple.com/videos/play/wwdc2022/10137/&lt;/a>&lt;/p>
&lt;p>Swift Charts 框架的进阶介绍，主要介绍了一些自定义方法。&lt;/p>
&lt;p>增加标注：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-annotation.webp" alt="">&lt;/p>
&lt;p>指定图表颜色：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-color.webp" alt="">&lt;/p>
&lt;p>自定义刻度：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-axis.webp" alt="">&lt;/p>
&lt;p>自定义图表区域：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-area.webp" alt="">&lt;/p>
&lt;p>在图表上添加一层 overlay，可用于交互：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy.webp" alt="">
&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy-2.webp" alt="">&lt;/p>
&lt;h2 id="embrace-swift-generics">Embrace Swift generics&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110352/">https://developer.apple.com/videos/play/wwdc2022/110352/&lt;/a>&lt;/p>
&lt;p>介绍了 Swift 范型的更新，主要针对 protocol。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Farm&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feedAll&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animals: [any Animal]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> animal &lt;span style="color:#66d9ef">in&lt;/span> animals {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> feed(animal)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上面的代码中有几个关键点：&lt;/p>
&lt;ul>
&lt;li>协议限制在 Swift 5.7 中可以用 opaque type，即 &lt;code>some&lt;/code> 关键字表示&lt;/li>
&lt;li>可以用 &lt;code>type(of:)&lt;/code> 来获取确定的具体类型&lt;/li>
&lt;li>在 Swift 5.7 中可使用 &lt;code>any&lt;/code> 实现针对 protocol 的类型消除（type erasure）&lt;/li>
&lt;/ul>
&lt;p>&lt;code>any&lt;/code> 非常实用。之前，数组只能储存同一具体类型的数据，不能储存遵循相同协议而类型不同的数据（主要原因应该是内存大小不同）；在 Swift 5.7 中，使用 &lt;code>any&lt;/code> 可以储存遵循某协议的任意类型的数据。现在，数组就不一定直接储存对象本身了。如果对象大小合适（这里不太清楚“合适”的含义，估计是指针大小以内？），可以直接存到数组里，否则存在外面：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-type-erasure.webp" alt="">&lt;/p>
&lt;p>这种方式有点像存了一堆指针，但是又不完全是（较小的对象可以直接放在数组内），可能是为了减少虚函数查表的开销？&lt;/p>
&lt;p>另外，&lt;code>any&lt;/code> 类型可以用于 &lt;code>some&lt;/code>，具体类型的获取在运行时完成。这种转换叫做“opening boxes”，即“打开盒子看究竟是什么动物”：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-opening-boxes.webp" alt="">&lt;/p>
&lt;p>最后，官方建议：&lt;/p>
&lt;blockquote>
&lt;p>Write &lt;strong>some&lt;/strong> by default&lt;/p>
&lt;/blockquote>
&lt;p>道理很简单，直接调用带 &lt;code>some&lt;/code> 参数的函数实际上在编译期是确定了类型的，而 &lt;code>any&lt;/code> 在编译期去掉了具体类型的信息，在运行期才能确定，相比之下效率较低。&lt;/p>
&lt;h2 id="design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110353/">https://developer.apple.com/videos/play/wwdc2022/110353/&lt;/a>&lt;/p>
&lt;p>在上面 Embrace Swift generics 的基础上，进一步介绍使用新的语言特性设计协议。&lt;/p>
&lt;h3 id="type-erasure">Type erasure&lt;/h3>
&lt;p>和上面类似的例子。当我们使用类型消除时，所有类型变成“上界”（不太明白这个名词的意思&amp;hellip;），与之相关的所有泛型都变成对应的上界。&lt;/p>
&lt;p>比如，&lt;code>any Animal&lt;/code> 调用 &lt;code>produce()&lt;/code> 的结果类型为 &lt;code>any Food&lt;/code>：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics.webp" alt="">&lt;/p>
&lt;p>但是，反过来，不能够将某一个符合 associatedtype 的类型作为参数传入一个上界类型的函数中。&lt;/p>
&lt;p>比如，&lt;code>Crop()&lt;/code> 不能作为 &lt;code>any Animal&lt;/code> 的 &lt;code>eat()&lt;/code> 的参数：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics-2.webp" alt="">&lt;/p>
&lt;p>这里的关键在于，一个具体类型总是可以安全地转换为 &lt;code>any&lt;/code>，而 &lt;code>any&lt;/code> 不能反过来安全地转换为某一个具体的类型。&lt;/p>
&lt;h3 id="hide-implementation-details">Hide implementation details&lt;/h3>
&lt;p>有时候，我们不希望暴露太多实现细节，比如下面的这个例子：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter.webp" alt="">&lt;/p>
&lt;p>我们希望使用 &lt;code>LazyFilterSequence&lt;/code> 来提高性能，然而并不希望将这个信息暴露在 API 中。Swift 为了解决这个问题，增加了一个叫做 primary associated types 的语法：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter-opaque.webp" alt="">&lt;/p>
&lt;p>Primary associated types 大致的意思是，将 protocol 中与实现细节相关的 associatedtype 使用尖括号暴露出来，从而能够使用 &lt;code>some&lt;/code> 或者 &lt;code>any&lt;/code> 来表示满足这个 associatedtype 要求的类型。&lt;/p>
&lt;p>比如，标准库中的 &lt;code>Collection&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Collection&lt;/span>&amp;lt;Element&amp;gt;: Sequence {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Element
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="关于-typeof">关于 &lt;code>type(of:)&lt;/code>&lt;/h3>
&lt;p>看完视频最主要的疑问是：&lt;code>type(of:)&lt;/code> 究竟是完全由运行时决定的，还是有编译器参与的。比如上面的例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器知道 &lt;code>some Animal&lt;/code> 是一个具体的、遵循 &lt;code>Animal&lt;/code> 的类型（包括运行时才能确定类型的 &lt;code>any Animal&lt;/code>），因此可以确定 &lt;code>type(of: animal).Feed&lt;/code> 的类型就是具体类型的 &lt;code>Feed&lt;/code>，上面的代码得以通过编译。&lt;/p>
&lt;p>但是，脱离了 &lt;code>some&lt;/code> 的语境，&lt;code>any&lt;/code> 就没有这种机制了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> animal: any Animal = Cow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// error: type of expression is ambiguous without more context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// let crop = type(of: animal).Feed.grow()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// ~~~~~~~~~~~~~~~~~^^^^~~~~~~~&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器实际上通过 &lt;code>some&lt;/code> 给 &lt;code>any&lt;/code> 提供了便利，即虽然不能在编译期知道具体的类型，但是可以保证 associatedtype 的正确性，这也是上面所说的“Open boxing”实现的效果。要想利用这种便利，我们需要使用带有 &lt;code>some&lt;/code> 参数的函数。&lt;/p></content></item><item><title>协程上下文切换</title><link>https://yeshe.ng/blog/posts/2022/05/context-switch/</link><pubDate>Thu, 26 May 2022 13:02:01 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/05/context-switch/</guid><description>在用户态进行上下文切换</description><content>&lt;p>最近在研究 &lt;a href="https://github.com/PlatformLab/Arachne">Arachne&lt;/a> 做用户态线程调度的方法。&lt;/p>
&lt;p>众所周知，一个线程的状态包括：&lt;/p>
&lt;ul>
&lt;li>PC&lt;/li>
&lt;li>一些寄存器（即“上下文”）&lt;/li>
&lt;/ul>
&lt;p>切换线程时，需要切换上下文；为了将来能够恢复上下文，在切换时我们需要将当前的上下文保存在某个地方。我们尝试用 C 写一个简单的上下文切换。&lt;/p>
&lt;h2 id="上下文切换">上下文切换&lt;/h2>
&lt;p>进行上下文切换的汇编代码在上汇编课时就讲过：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-assembly" data-lang="assembly">swapcontext:
# save current context
pushq %r12
pushq %r13
pushq %r14
pushq %r15
pushq %rbx
pushq %rbp
movq %rsp, (%rsi)
# restore previously saved context
movq (%rdi), %rsp
popq %rbp
popq %rbx
popq %r15
popq %r14
popq %r13
popq %r12
&lt;/code>&lt;/pre>&lt;p>上面的代码做了几件事情：&lt;/p>
&lt;ol>
&lt;li>保存当前上下文
&lt;ol>
&lt;li>将被调用者保存寄存器保存到当前的调用栈上&lt;/li>
&lt;li>将当前的栈指针 &lt;code>%rsp&lt;/code> 保存到 &lt;code>%rsi&lt;/code> 所指向的内存地址上&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>恢复之前保存的上下文
&lt;ol>
&lt;li>将栈指针恢复为 &lt;code>%rdi&lt;/code> 所保存的另一个协程的栈顶&lt;/li>
&lt;li>恢复另一个协程的栈上面保存的被调用者保存寄存器&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;p>这里需要理解一个问题：&lt;strong>为什么需要保存被调用者保存寄存器&lt;/strong>？&lt;/p>
&lt;p>“被调用者保存寄存器”指的是在调用子过程前后不发生改变的寄存器，即子过程需要在返回前将这些寄存器的值恢复。上面的 &lt;code>swapcontext&lt;/code> 是一个过程调用，因此需要遵循这一惯例。然而，与一般的过程调用不同，我们在这里实际上是切换到了另一个上下文中，因此我们实际上是恢复了另一个协程的上下文。&lt;/p>
&lt;p>这里可能会产生疑惑：调用者保存寄存器呢？实际上，调用 &lt;code>swapcontext&lt;/code> 的过程在调用它之前就已经把调用者保存寄存器保存好了（如果有必要的话）。理解的关键是：&lt;code>swapcontext&lt;/code> 是一个过程，而不是内联的汇编代码。&lt;/p>
&lt;h2 id="调用栈结构">调用栈结构&lt;/h2>
&lt;p>由于 &lt;code>swapcontext&lt;/code> 中会将当前的上下文保存到某个内存地址，我们之后只需要将这个内存地址传入 &lt;code>swapcontext&lt;/code> 就可以切换回来了。在这里，我们主要考虑在一开始如何切换到一个指定的函数上；我们需要初始化调用栈，使得 &lt;code>swapcontext&lt;/code> 后能够跳转到指定的函数。&lt;/p>
&lt;p>不难画出这样的一个调用栈：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Return Address |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Registers |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sp-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们将需要切换到的函数的地址保存在 &lt;code>Return Address&lt;/code> 中。在使用 &lt;code>popq&lt;/code> 指令恢复所有保存的寄存器后，&lt;code>rsp&lt;/code> 指向此函数地址，&lt;code>swapcontext&lt;/code> 返回（&lt;code>ret&lt;/code> 指令）后便跳转到函数的入口地址。需要注意的是 &lt;code>Return Address&lt;/code> 上方需要保留 8 字节的空间。&lt;/p>
&lt;p>栈的初始化过程如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define SPACE_FOR_SAVED_REGISTERS 48UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define STACK_SIZE 1024UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">char&lt;/span> stack[&lt;span style="color:#ae81ff">8&lt;/span> &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Initialize stack for routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> stack &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">sizeof&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Set return address
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">*&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">**&lt;/span>)sp2 &lt;span style="color:#f92672">=&lt;/span> routine2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Point to bottom of the register stack
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>)sp2 &lt;span style="color:#f92672">-&lt;/span> SPACE_FOR_SAVED_REGISTERS;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在这里，我们创建一个栈，并将 &lt;code>routine2&lt;/code> 的地址写到相应位置；&lt;code>sp2&lt;/code> 指向最后一个储存寄存器值的位置。&lt;/p>
&lt;p>此时，&lt;code>stack&lt;/code>、&lt;code>sp2&lt;/code> 和调用栈的结构如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Address of `routine2` |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Registers |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sp2-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Stack Space |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>stack-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="程序">程序&lt;/h2>
&lt;p>最后，我们写几个简单的上下文切换的 demo。&lt;/p>
&lt;h3 id="例子-1">例子 #1&lt;/h3>
&lt;p>在这里，我们进行两次上下文切换，从 &lt;code>routine&lt;/code> 切换到 &lt;code>routine2&lt;/code>，再切换回 &lt;code>routine&lt;/code>。&lt;/p>
&lt;p>其中，&lt;code>swapcontext&lt;/code> 需要加上 &lt;code>noinline&lt;/code> 的 attribute，这是为了保证 &lt;code>swapcontext&lt;/code> 作为过程被调用。&lt;/p>
&lt;p>另外，编译时需要加上 &lt;code>-fomit-frame-pointer&lt;/code>，否则编译器在编译时会在过程前后分别加上 &lt;code>pushq %rbp&lt;/code> 和 &lt;code>popq %rbp&lt;/code>，导致栈结构遭到破坏。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#include&lt;/span> &lt;span style="color:#75715e">&amp;lt;stdio.h&amp;gt;&lt;/span>&lt;span style="color:#75715e">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define SPACE_FOR_SAVED_REGISTERS 48UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define STACK_SIZE 1024UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">__attribute__&lt;/span>((noinline)) swapcontext(&lt;span style="color:#66d9ef">void&lt;/span>&lt;span style="color:#f92672">**&lt;/span> saved, &lt;span style="color:#66d9ef">void&lt;/span>&lt;span style="color:#f92672">**&lt;/span> target) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">asm&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;pushq %r12&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r13&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r14&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r15&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %rbx&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %rbp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;movq %rsp, (%rsi)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">asm&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;movq (%rdi), %rsp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %rbp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %rbx&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r15&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r14&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r13&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r12&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">char&lt;/span> stack[&lt;span style="color:#ae81ff">8&lt;/span> &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Switch to routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine() end&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Switch back
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Unreachable
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2() end&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Initialize stack for routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> stack &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">sizeof&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Set return address
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">*&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">**&lt;/span>)sp2 &lt;span style="color:#f92672">=&lt;/span> routine2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Point to bottom of the register stack
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>)sp2 &lt;span style="color:#f92672">-&lt;/span> SPACE_FOR_SAVED_REGISTERS;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> routine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;main routine&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>routine()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine() end
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>main routine
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="例子-2">例子 #2&lt;/h3>
&lt;p>继续尝试一些其他的用法。在这里，我们计算 0 累加至 9 的结果，并在每次循环时都切换一次上下文，然后立刻切换回来。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> sum &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">int&lt;/span> i &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; i &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>; i&lt;span style="color:#f92672">++&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sum &lt;span style="color:#f92672">+=&lt;/span> i;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;sum: %d&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>, sum);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> (;;) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>routine()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sum: 45
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="汇编结果">汇编结果&lt;/h3>
&lt;p>使用 &lt;code>-O1&lt;/code> 编译上面第二个例子（去掉所有 &lt;code>printf&lt;/code>），&lt;code>routine&lt;/code> 的汇编代码如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-assembly" data-lang="assembly">0000000000000639 &amp;lt;routine&amp;gt;:
639: 55 push %rbp
63a: 53 push %rbx
63b: bb 0a 00 00 00 mov $0xa,%ebx
640: 48 8d 2d f9 09 20 00 lea 0x2009f9(%rip),%rbp # 201040 &amp;lt;sp&amp;gt;
647: 48 89 ee mov %rbp,%rsi
64a: 48 8d 3d 47 0e 20 00 lea 0x200e47(%rip),%rdi # 201498 &amp;lt;sp2&amp;gt;
651: e8 af ff ff ff callq 605 &amp;lt;swapcontext&amp;gt;
656: 83 eb 01 sub $0x1,%ebx
659: 75 ec jne 647 &amp;lt;routine+0xe&amp;gt;
65b: 5b pop %rbx
65c: 5d pop %rbp
65d: c3 retq
&lt;/code>&lt;/pre>&lt;p>可以看到，&lt;code>for&lt;/code> 循环使用 &lt;code>%ebx&lt;/code>（被调用者保存）进行计数，在调用 &lt;code>swapcontext&lt;/code> 前后并没有保存其值。保存这个寄存器是 &lt;code>swapcontext&lt;/code> 的责任。&lt;/p></content></item><item><title>Jinja 继承、函数和块</title><link>https://yeshe.ng/blog/posts/2021/09/jinja/</link><pubDate>Fri, 24 Sep 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/09/jinja/</guid><description>对 HTML 模版引擎 Jinja 的一些探索</description><content>&lt;p>最近在为 &lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">mkdocs-blogging-plugin&lt;/a> 添加自定义的功能，因为本来就是用 Jinja 的 HTML 模版来生成 HTML，所以也想用 Jinja 给用户提供自定义的接口。&lt;/p>
&lt;p>大概的要求有：&lt;/p>
&lt;ul>
&lt;li>提供自定义单个 post 而保留大体框架的方法&lt;/li>
&lt;li>提供自定义 style sheet 的方法&lt;/li>
&lt;li>提供整体重写的方法&lt;/li>
&lt;/ul>
&lt;p>这篇文章只会覆盖到部分。&lt;/p>
&lt;h2 id="继承extends">继承：&lt;code>extends&lt;/code>&lt;/h2>
&lt;p>Jinja 给我们提供了一种类似于继承的方法，可以在保留原来模版所有内容的情况下增加、重写部分内容的方法。&lt;/p>
&lt;p>在“子模版”中，通过 &lt;code>extends&lt;/code> 引用“父模版”：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#e6db74">&amp;#34;blog.html&amp;#34;&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果只有这一句，相当于将原来 &lt;code>blog.html&lt;/code> 中所有内容原封不动地搬到新的模版中。至于增加和重写内容，稍后再谈。&lt;/p>
&lt;h2 id="函数macro">函数：&lt;code>macro&lt;/code>&lt;/h2>
&lt;p>在用户提供的覆写模版中，我们需要用户向父模版提供一个“函数”，用来生成单个博客的内容。&lt;/p>
&lt;p>Jinja 中的“函数”叫做 &lt;code>macro&lt;/code>，通过这样的方法定义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{# content #}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以及，这样调用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">call&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{# caller_content #}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endcall&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>call&lt;/code> 调用 &lt;code>macro&lt;/code> 后，将整个 &lt;code>call&lt;/code> 块 替换为 &lt;code>macro&lt;/code> 块里面的所有内容 &lt;code>{# content #}&lt;/code>，与 C 中的宏定义一致（这应该也是为什么叫做 &lt;code>macro&lt;/code> 的原因）。&lt;/p>
&lt;p>另外，&lt;code>macro&lt;/code> 有一个隐含参数 &lt;code>caller&lt;/code>，其作用是返回 &lt;code>call&lt;/code> 块中间的内容 &lt;code>{# caller_content #}&lt;/code>，如：&lt;/p>
&lt;p>&lt;code>macro&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> foo&lt;span style="color:#f92672">(&lt;/span>name&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> name &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#34; says: &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> caller&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>call&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">call&lt;/span> foo&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;liang2kl&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Hello, World!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endcall&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>call&lt;/code> 的结果为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">div&lt;/span>&amp;gt;liang2kl says: Hello, World!&amp;lt;/&lt;span style="color:#f92672">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>值得注意的是，如果不想传递 &lt;code>caller&lt;/code> 中的内容，也需要在 &lt;code>macro&lt;/code> 中调用 &lt;code>caller()&lt;/code>，否则会报错：&lt;/p>
&lt;p>TypeError: macro &amp;lsquo;foo&amp;rsquo; was invoked with two values for the special caller argument. This is most likely a bug.&lt;/p>
&lt;/blockquote>
&lt;p>现在，我们只需要用户提供一个产生博客内容的 macro 给我们的父模版就可以了。&lt;/p>
&lt;h2 id="重写block">重写：&lt;code>block&lt;/code>&lt;/h2>
&lt;p>Jinja 的继承机制允许我们在“子模版”中选择性地重写部分内容，而保持其他部分不变。实现的机制是 &lt;code>block&lt;/code>。&lt;/p>
&lt;p>在父模版中，我们用 &lt;code>block&lt;/code> 块划出允许重写的部分。如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;style&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/style&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在子模版中，重写：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>即可将父模版中的 &lt;code>style&lt;/code> 块替换为新的块。&lt;/p>
&lt;p>另外，可以用 &lt;code>super()&lt;/code> 保留原来内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{{&lt;/span> super&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="放在一起">放在一起&lt;/h2>
&lt;p>将上面几个功能放在一起，我们给用户提供这样的自定义方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;&lt;span style="color:#75715e">{{&lt;/span> url &lt;span style="color:#75715e">}}&lt;/span>&amp;#34;&amp;gt;&amp;lt;h3&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> title &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/h3&amp;gt;&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> description &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> time &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{{&lt;/span> caller&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#e6db74">&amp;#34;blog.html&amp;#34;&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .md-typeset .blog-post-title {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> color: blue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其作用：&lt;/p>
&lt;ul>
&lt;li>在引入 &lt;code>blog.html&lt;/code> 之前，定义 &lt;code>render_blog&lt;/code> 的 macro，以便在 &lt;code>blog.html&lt;/code> 中调用&lt;/li>
&lt;li>引入 &lt;code>blog.html&lt;/code>&lt;/li>
&lt;li>通过 &lt;code>block&lt;/code> 修改 &lt;code>blog.html&lt;/code> 的 &lt;code>style&lt;/code> 块&lt;/li>
&lt;/ul></content></item><item><title>做一个 MkDocs 博客插件</title><link>https://yeshe.ng/blog/posts/2021/09/making-mkdocs-plugin/</link><pubDate>Thu, 09 Sep 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/09/making-mkdocs-plugin/</guid><description>之前使用 MkDocs 构建博客，但是 MkDocs 本身并不是为博客设计的，于是着手写一个插件。（当然最后还是放弃了 Mkdocs（划掉））</description><content>&lt;p>&lt;a href="https://github.com/mkdocs/mkdocs">MkDocs&lt;/a> 是个好东西，非常适合用来写一些 tech 文章；而 MkDocs 的 &lt;a href="https://github.com/squidfunk/mkdocs-material">Material&lt;/a> 主题更是 geek 风格十足。第一次看到 Material for MkDocs，就一见钟情，马上搭了个博客网站。&lt;/p>
&lt;p>但是，仔细一看 Material for MkDocs 的介绍，感觉不太对劲：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Technical documentation that just works&lt;/strong>&lt;/p>
&lt;p>Create a branded static site from a set of Markdown files to host the documentation of
your Open Source or commercial project&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>MkDocs 本来就只是为了搭建项目文档网站设计的，很多功能和设计（如底部导航栏、侧边栏等）更适合文档，而非博客。这导致，MkDocs 并没有将所有文章列在一个地方的功能。没有博客界面，怎么能叫做一个博客呢……&lt;/p>
&lt;p>但是，实在舍不得这么好看的界面，就琢磨着自己魔改一番。正好小学期学了 Python，可以用来练练手。&lt;/p>
&lt;blockquote>
&lt;p>最终成果开源于 &lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">mkdocs-blogging-plugin&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;h2 id="mkdocs-的插件是怎么工作的">MkDocs 的插件是怎么工作的&lt;/h2>
&lt;p>仔细读了读&lt;a href="https://www.mkdocs.org/dev-guide/plugins/">开发者文档&lt;/a>，MkDocs 插件的工作是在 MkDocs 处理文档的整个生命周期的不同阶段&lt;strong>拦截当前结果，并返回处理后的结果&lt;/strong>。比如，对生成的 html 进行修改，是在插件类的 &lt;code>on_page_content&lt;/code> 函数中实现的：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>on_page_content&lt;/strong>&lt;/p>
&lt;p>The &lt;code>page_content&lt;/code> event is called after the Markdown text is rendered to HTML (but before being passed to a template) and can be used to alter the HTML body of the page.&lt;/p>
&lt;p>Parameters:&lt;/p>
&lt;ul>
&lt;li>html: HTML rendered from Markdown source as string&lt;/li>
&lt;li>page: &lt;code>mkdocs.nav.Page&lt;/code> instance&lt;/li>
&lt;li>config: global configuration object&lt;/li>
&lt;li>files: global files collection&lt;/li>
&lt;/ul>
&lt;p>Returns:
HTML rendered from Markdown source as string&lt;/p>
&lt;/blockquote>
&lt;p>我们从函数参数中读取当前处理中的 html 字符串，在对其进行处理后，返回最终的 html 字符串，就完成了插件对页面内容的修改。&lt;/p>
&lt;p>实现这些函数，我们就可以读取需要的信息，并更改某些页面内容。&lt;/p>
&lt;h2 id="设计与实现">设计与实现&lt;/h2>
&lt;p>首先，考虑把博客内容加入什么地方。考虑到用户可能需要在博客列表的前后加上其他的信息，直接生成一个页面不太可行。参考 &lt;a href="https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/tree/master/mkdocs_git_revision_date_localized_plugin">mkdocs-git-revision-date-localized-plugin&lt;/a> 的做法，让用户在需要添加博客内容的地方加上一个指定的占位符 &lt;code>{{ blog_content }}&lt;/code>，然后在处理时用正则表达式替换即可。&lt;/p>
&lt;p>然后，考虑博客的排序。我们选择使用 git log 来获取博文的创建和更新时间，然后向用户提供配置的选项（从新到旧 + 根据创建时间 / 从旧到新 + 根据修改时间 等）。这部分代码可以直接抄 &lt;a href="https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/tree/master/mkdocs_git_revision_date_localized_plugin">mkdocs-git-revision-date-localized-plugin&lt;/a> 的实现，工作量&amp;ndash;。&lt;/p>
&lt;h3 id="插件类与用户配置">插件类与用户配置&lt;/h3>
&lt;p>根据文档，插件类需要继承 &lt;code>BasePlugin&lt;/code>。在 &lt;code>BasePlugin&lt;/code> 中，&lt;code>config_scheme&lt;/code> 为插件的可配置选项。比如，我们需要用户设置一个名叫 &lt;code>foo&lt;/code> 的参数，其类型为 &lt;code>str&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BloggingPlugin&lt;/span>(BasePlugin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> config_scheme &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>, config_options&lt;span style="color:#f92672">.&lt;/span>Type(str, default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">None&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，用户在 &lt;code>mkdocs.yml&lt;/code> 中，可以这样配置 &lt;code>foo&lt;/code>（假设我们的插件名为 &lt;code>blogging&lt;/code>）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">plugins&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">blogging&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">foo&lt;/span>: &lt;span style="color:#ae81ff">bar&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="获取博客文件">获取博客文件&lt;/h3>
&lt;p>我们需要给予用户自行设置哪些文件夹、哪些文件的途径。在 &lt;code>config_scheme&lt;/code> 中，加上一项 &lt;code>dirs&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>config_scheme &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#e6db74">&amp;#34;dirs&amp;#34;&lt;/span>, config_options&lt;span style="color:#f92672">.&lt;/span>Type(list, default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">None&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，在 &lt;code>on_page_content&lt;/code> 函数中，如果 &lt;code>page&lt;/code> 是在用户设置的 &lt;code>dirs&lt;/code> 里面，就将其储存下来：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_post_page&lt;/span>(self, output, page, config):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dir &lt;span style="color:#f92672">in&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>docs_dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>file&lt;span style="color:#f92672">.&lt;/span>src_path[:len(dir)] &lt;span style="color:#f92672">==&lt;/span> dir:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>blog_pages&lt;span style="color:#f92672">.&lt;/span>append(page)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，有些文章在 &lt;code>dirs&lt;/code> 中，但可能并不希望被包括进博客中（如一些 index page）。对于这些文件，我们要求用户在 markdown 的 meta 中加入一项 &lt;code>exclude_from_blog&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>exclude_from_blog: true
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，修改一下上面的判断条件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>file&lt;span style="color:#f92672">.&lt;/span>src_path[:len(dir)] &lt;span style="color:#f92672">==&lt;/span> dir \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">and&lt;/span> (&lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#e6db74">&amp;#34;exclude_from_blog&amp;#34;&lt;/span> &lt;span style="color:#f92672">in&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>meta \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>meta[&lt;span style="color:#e6db74">&amp;#34;exclude_from_blog&amp;#34;&lt;/span>]):
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就可以实现页面的筛选。&lt;/p>
&lt;h3 id="页面生成">页面生成&lt;/h3>
&lt;p>我们需要生成博客的列表界面，最简单的办法就是用 html 模版引擎来为我们动态地生成静态页面，这里使用 MkDocs 使用的 &lt;code>jinja2&lt;/code> 来处理。&lt;/p>
&lt;p>生成博客列表不是难事，写一个 for 循环就可以了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> page &lt;span style="color:#66d9ef">in&lt;/span> pages &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endfor&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>比较棘手的是分页的处理。根据我们的设计，我们最终在&lt;strong>一个页面&lt;/strong>中插入博客内容，但是分页一般是&lt;strong>多个页面&lt;/strong>。&lt;/p>
&lt;p>最终采取了将所有博客信息塞入页面、使用 css 现实当前页面博客信息 / 隐藏其他页面的方法：&lt;/p>
&lt;p>首先，在 pagination 中，将地址设置为 &lt;code>#page{{当前页面}}&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">a&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;#page{{ page_idx + 1 }}&amp;#34;&lt;/span>&amp;gt;{{ page_idx + 1 }}&amp;lt;/&lt;span style="color:#f92672">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 css 中，id 与 &lt;code>#&lt;/code> 后面字符相同的可以用 &lt;code>:target&lt;/code> 选择器选中。比如，当前页面是 &lt;code>/blog/#page1&lt;/code>，那么 &lt;code>:target&lt;/code> 选中的就是 &lt;code>id = &amp;quot;page1&amp;quot;&lt;/code> 的元素。我们只需要将 &lt;code>:target&lt;/code> 设为显示，其他设为隐藏就可以了。&lt;/p>
&lt;p>在 html 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&amp;#34;pages&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> page_idx &lt;span style="color:#66d9ef">in&lt;/span> range&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>&lt;span style="color:#f92672">,&lt;/span> page_num&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;page&amp;#34; id=&amp;#34;page&lt;span style="color:#75715e">{{&lt;/span> page_idx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;!-- 第 page_index + 1 页的内容 --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endfor&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 css 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* 将所有子元素隐藏 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#a6e22e">pages&lt;/span> &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#f92672">*&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">display&lt;/span>: &lt;span style="color:#66d9ef">none&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* 显示 :target */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#a6e22e">pages&lt;/span> &lt;span style="color:#f92672">&amp;gt;&lt;/span> :&lt;span style="color:#a6e22e">target&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">display&lt;/span>: &lt;span style="color:#66d9ef">block&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>由于 css 处于后面的声明会覆盖前面的声明，&lt;code>:target&lt;/code> 可以显示，而其他页面的元素将被隐藏。这样就实现了分页。&lt;/p>
&lt;p>另外，我们还需要 JavaScript 来处理 pagination 的高亮，在这里就不赘述了。&lt;/p>
&lt;blockquote>
&lt;p>这种解决方法其实有个问题：当我们点击链接跳转到 &lt;code>#pageX&lt;/code> 的时候，浏览器会自动跳转到 &lt;code>id = pageX&lt;/code> 的元素处，非常影响体验。如今已经抛弃这种方法，改为在 JavaScript 中利用点击事件获取页面、更改 &lt;code>page&lt;/code> 的 class 来实现页面的显示和隐藏。&lt;/p>
&lt;/blockquote>
&lt;h3 id="页面插入">页面插入&lt;/h3>
&lt;p>在插件的 &lt;code>on_post_page&lt;/code> 函数中，将原来文件中的 &lt;code>{{ placeholder }}&lt;/code> 替换为用 jinja 生成的 html 字符串：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_post_page&lt;/span>(self, output, page, config):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> additional_html &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>template&lt;span style="color:#f92672">.&lt;/span>render(pages&lt;span style="color:#f92672">=&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>blog_pages, &lt;span style="color:#f92672">...&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output &lt;span style="color:#f92672">=&lt;/span> re&lt;span style="color:#f92672">.&lt;/span>sub(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">r&lt;/span>&lt;span style="color:#e6db74">&amp;#34;\{\{\s*blog_content\s*\}\}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> additional_html,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> flags&lt;span style="color:#f92672">=&lt;/span>re&lt;span style="color:#f92672">.&lt;/span>IGNORECASE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就基本上完成了所有的工作。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>上面所述的仅仅是插件开发中一些枝干的问题，实际上，做一个工程，而不仅是自己用的小软件，要考虑的问题远不止于怎么实现功能，不仅要尽可能满足用户的需求，还要处理各种 corner cases（&lt;del>测试工程师进酒吧&lt;/del>）。当然，造轮子虽然难，但是其中的快乐是无穷的。&lt;/p></content></item><item><title>用 Property Wrapper 包装网络请求参数</title><link>https://yeshe.ng/blog/posts/2021/08/property-wrapper-http/</link><pubDate>Sat, 28 Aug 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/08/property-wrapper-http/</guid><description>Swift 中的 HTTP 请求一般需要手动设置请求的参数和 Header，是否有一种优雅的方式将请求的数据与请求本身绑定，从而省去手动赋值的过程？</description><content>&lt;p>偶然看到 &lt;a href="https://square.github.io/retrofit/">Java 语言 Retrofit 库的文档&lt;/a>，对这种标注 HTTP 请求参数的方式十分感兴趣（其实我也没学过 Java）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@FormUrlEncoded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@POST&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;user/edit&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Call&lt;span style="color:#f92672">&amp;lt;&lt;/span>User&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">updateUser&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">@Field&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;first_name&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> String first&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#a6e22e">@Field&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;last_name&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> String last&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>联想一下以前用 Swift 写的网络请求（参考 &lt;a href="https://yeshe.ng/blog/posts/2021/08/protocol-generic/">协议 + 泛型：威力巨大的组合&lt;/a>），都需要手动赋值参数，实在是不太优雅：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> headers: HTTPHeaders = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.token,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Accept&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> parameters: [String : Encodable] = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pid&amp;#34;&lt;/span> : &lt;span style="color:#66d9ef">self&lt;/span>.configuration.postId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;switch&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.switchToAttention ? &lt;span style="color:#ae81ff">1&lt;/span> : &lt;span style="color:#ae81ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Java 的这种语法与 Swift 的 &lt;code>Property Wrapper&lt;/code> 很像（实践证明 Swift 这个语法局限性还是挺大的），&lt;del>而且逼格也非常高&lt;/del> ，恰好也一直想自己写个 Property Wrapper 玩一玩研究一下，于是就动手写一个包装网络请求参数的库。&lt;/p>
&lt;h2 id="理想效果">理想效果&lt;/h2>
&lt;p>理想中，对参数的包装应该达到的效果有：&lt;/p>
&lt;ul>
&lt;li>用 Property Wrapper 包装实际的参数值和对应的 key
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SomeRequest&lt;/span>: Request {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Header(&lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> accessToken
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;page&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> page: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>能够自动找到所有的参数，并将其放入 HTTP 请求中&lt;/li>
&lt;/ul>
&lt;p>目标有了，就开始写吧！&lt;/p>
&lt;h2 id="设计与实现">设计与实现&lt;/h2>
&lt;h3 id="包装参数">包装参数&lt;/h3>
&lt;p>我们的 Property Wrapper 中需要 key 和 value。尝试写一下 &lt;code>@Query&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@propertyWrapper
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.wrappedValue = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@Query&lt;/code> 接受 &lt;code>String&lt;/code> 作为 key，和一个遵循 &lt;code>Encodable&lt;/code> 协议的值作为 value。&lt;/p>
&lt;p>可是，当我们尝试使用这个 Property Wrapper 的时候，却出现了问题：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样写会出现编译错误：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Missing argument for parameter &amp;#39;wrappedValue&amp;#39; in call
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原因在于，&lt;code>@Query(&amp;quot;foo&amp;quot;) var bar: Int&lt;/code> 的声明其实是在调用 &lt;code>Query&amp;lt;Int&amp;gt;&lt;/code> 的 &lt;code>init(wrappedValue: Value, _ key: String)&lt;/code>，而 Swift 不允许存在未初始化的成员变量，所以必须同时初始化 &lt;code>key&lt;/code> 和 &lt;code>wrappedValue&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是，总不能要求调用者每种类型都给出个默认值……这样实在是太丑陋了，也不符合语义—— &lt;code>@Query(&amp;quot;foo&amp;quot;)&lt;/code> 声明 key，value 应该是之后赋值的。而且也不现实，如果 &lt;code>wrappedValue&lt;/code> 是一个自定义类型的话，提供默认值是一件匪夷所思的事情。&lt;/p>
&lt;p>没办法，Property Wrapper 括号中的参数并不是严格意义上静态的 attribute，而是一个函数的参数，是一个变量，这本来就是和我们的需求相背的。&lt;/p>
&lt;p>那么，退而求其次，既然不能要求提供默认值，那我们可以用一种不会产生歧义的、带有默认初始化方法的变量—— &lt;code>Optional&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@propertyWrapper
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value?, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.wrappedValue = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样一来，参数的声明就可以稍微合理一点了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>虽然我们还是要提供一个初始值，但是 &lt;code>nil&lt;/code> 在这里刚好可以表达“未初始化”的意思，在以后解析所有参数的时候，如果这个值没有被设定，我们直接跳过这个请求参数就好了。&lt;/p>
&lt;p>到这里，自然会想到在 &lt;code>init&lt;/code> 中给 &lt;code>wrappedValue&lt;/code> 设定 &lt;code>nil&lt;/code> 的默认值，使得我们不需要手动写 &lt;code>= nil&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value? = &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#66d9ef">_&lt;/span> key: String)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这似乎解决了问题，我们可以这样写了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是又多了一个问题：我们不能直接用 &lt;code>wrappedValue&lt;/code> 初始化含有 &lt;code>@Query&lt;/code> 变量的类型了，如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Foo&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// let foo = Foo(bar: 1)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Cannot convert value of type &amp;#39;Int&amp;#39; to expected argument type &amp;#39;Query&amp;lt;Int&amp;gt;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>用一个例子测试一下 Property Wrapper 初始化的原理：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">A&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> value: Value?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value? {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">get&lt;/span> { value }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">set&lt;/span> { value = newValue; print(newValue) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value?, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.value = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Initializing with wrappedValue &lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>wrappedValue&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">, key &lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>key&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Test&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @A(&lt;span style="color:#e6db74">&amp;#34;hello&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> a: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> foo = Test()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> bar = Test(a: &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Initializing with wrappedValue nil, key hello
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Initializing with wrappedValue Optional(1), key hello
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原来 &lt;code>@A&lt;/code> 变量在 &lt;code>Test&lt;/code> 的构造函数中无论是否赋值，都只会调用一次 &lt;code>@A&lt;/code> 的 &lt;code>init&lt;/code>。当我们不指定 &lt;code>a&lt;/code> 的值的时候，使用我们提供的默认值 &lt;code>nil&lt;/code> 进行初始化；当我们指定 &lt;code>a&lt;/code> 的值的时候，使用我们指定的值。&lt;/p>
&lt;p>所以，&lt;code>@Query(&amp;quot;foo&amp;quot;) var bar: Int? = nil&lt;/code> 中 &lt;code>= nil&lt;/code> 其实就相当于 &lt;code>init&lt;/code> 的一个默认参数。这也解释了为何我们直接在 &lt;code>init&lt;/code> 中设参数默认值使得无法用 &lt;code>wrappedValue&lt;/code> 初始化。&lt;/p>
&lt;/blockquote>
&lt;p>所以，还是手动把 &lt;code>= nil&lt;/code> 加上为妙。&lt;/p>
&lt;h3 id="获取信息">获取信息&lt;/h3>
&lt;p>包装好了，怎么获取这些变量是另一个大问题。要知道，我们根本不知道传进来的对象是什么。例如，我们的 &lt;code>SomeRequest&lt;/code> 中有这些参数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SomeRequest&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;page&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> page
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;bar&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> foo: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们当然不知道 &lt;code>SomeRequest&lt;/code> 中有 &lt;code>page&lt;/code> 和 &lt;code>foo&lt;/code> 两个成员。&lt;/p>
&lt;p>幸好，我们有 &lt;code>Mirror&lt;/code> ——利用对象的元数据，能够向我们提供对象成员变量的信息。&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>用 &lt;code>Mirror&lt;/code> 尝试一下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> mirror = Mirror(reflecting: request) &lt;span style="color:#75715e">// request is `SomeRequest`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">_&lt;/span>, property) &lt;span style="color:#66d9ef">in&lt;/span> mirror.children {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// property 是 request 的成员变量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们可以通过这种方法访问 &lt;code>request&lt;/code> 对象的所有成员变量。接下来的问题是，我们怎么识别是不是 &lt;code>@Query&lt;/code>，以及怎么把 &lt;code>Query&lt;/code> 里面的 &lt;code>key&lt;/code> 和 &lt;code>wrappedValue&lt;/code> 取出来。&lt;/p>
&lt;p>一种很朴素，又很错误的想法是这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> property = property &lt;span style="color:#66d9ef">as&lt;/span>? Query {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样当然通过不了编译，&lt;code>Query&lt;/code> 只是个模版，不是具体的类。我们需要一种与 &lt;code>Query&lt;/code> 模版参数无关的方法。&lt;/p>
&lt;p>我们喜欢的 &lt;code>protocol&lt;/code> 这时可以发挥作用了。我们不需要获取“&lt;code>property&lt;/code> 具体的类型”这么强的信息，只需要这个 &lt;code>property&lt;/code> 能够向我们提供我们需要的 URL 编码的 &lt;code>value&lt;/code> 和 &lt;code>key&lt;/code> 就行了。马上可以写一个协议：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">QueryProtocol&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedKey&lt;/span>() -&amp;gt; String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedValue&lt;/span>() -&amp;gt; String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们给 &lt;code>Query&lt;/code> 加上 &lt;code>QueryProtocol&lt;/code> 的协议：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>: QueryProtocol {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedKey&lt;/span>() -&amp;gt; String {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> SomeURLStringEncoder().encode(key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedValue&lt;/span>() -&amp;gt; String {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> SomeURLStringEncoder().encode(wrappedValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们就可以用 &lt;code>Mirror&lt;/code> 访问 key 和 value 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> mirror = Mirror(reflecting: request) &lt;span style="color:#75715e">// request is `SomeRequest`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">_&lt;/span>, property) &lt;span style="color:#66d9ef">in&lt;/span> mirror.children {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> query = property &lt;span style="color:#66d9ef">as&lt;/span>? QueryProtocol {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> key = query.urlEncodedKey()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> value = query.urlEncodedValue()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// &lt;/span>&lt;span style="color:#75715e">TODO:&lt;/span>&lt;span style="color:#75715e"> Add key, value to request URL.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，最后一个障碍——读取参数信息也扫清了。我们可以获取到一个类里面所有的 &lt;code>@Query&lt;/code> 参数了，将所有的 &lt;code>key&lt;/code> 和 &lt;code>value&lt;/code> 加到请求的 URL 即可。&lt;/p>
&lt;blockquote>
&lt;p>实际上的处理要稍微复杂一点，因为 &lt;code>Value&lt;/code> 可能是一个数组，这时候就不仅仅是一个 key 和 value 对了。&lt;/p>
&lt;/blockquote>
&lt;h2 id="后续">后续&lt;/h2>
&lt;p>根据相似的思路，我们可以为 Header、body 参数等定义类似的 Property Wrapper：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Property Wrapper&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Wrapped Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>@Header&lt;/code>&lt;/td>
&lt;td>HTTP Header&lt;/td>
&lt;td>&lt;code>String?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@HeaderDict&lt;/code>&lt;/td>
&lt;td>An dictionary of headers&lt;/td>
&lt;td>&lt;code>[String : String]?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@Query&lt;/code>&lt;/td>
&lt;td>Query parameters&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@QueryDict&lt;/code>&lt;/td>
&lt;td>An dictionary of query parameters&lt;/td>
&lt;td>&lt;code>[String : String]?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@KeyQuery&lt;/code>&lt;/td>
&lt;td>Query parameters with no value associated&lt;/td>
&lt;td>&lt;code>Bool&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@JSON&lt;/code>&lt;/td>
&lt;td>Body parameters with JSON encoding&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@Field&lt;/code>&lt;/td>
&lt;td>Form URL Encoded parameters&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>我们可以在一个类声明中定义所有的请求参数了。最后的实现效果如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SetPushRequest&lt;/span>: JSONDecodableRequest, RequestConfiguration {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Header(&lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> accessToken = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_system_msg&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushSystemMessage: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_reply_me&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushReplyMe: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_favorited&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushFavourite: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Response&lt;/span>: Codable {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> base: URL = URL(string: &lt;span style="color:#e6db74">&amp;#34;https://dev-api.thuhole.com&amp;#34;&lt;/span>)&lt;span style="color:#f92672">!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> path: String = &lt;span style="color:#e6db74">&amp;#34;v3/config/set_push&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> method: HTTPMethod = .post
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: &lt;span style="color:#66d9ef">Self&lt;/span> { &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">self&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> request = SetPushRequest(accessToken: token, pushSystemMessage: &lt;span style="color:#66d9ef">true&lt;/span>, pushReplyMe: &lt;span style="color:#66d9ef">true&lt;/span>, pushFavourite: &lt;span style="color:#66d9ef">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>request.perform { result &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> result {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .success(&lt;span style="color:#66d9ef">let&lt;/span> response):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(response)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .failure(&lt;span style="color:#66d9ef">let&lt;/span> error):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>SetPushRequest&lt;/code> 的定义不计空行只用了 14 行。在 &lt;a href="https://github.com/liang2kl/HollowCore/blob/main/Sources/HollowCore/Network/Config/SetPushRequest.swift">原来树洞的代码&lt;/a> 中，完成上述功能写了 31 行（不包括上面代码不涉及的功能）。&lt;/p>
&lt;blockquote>
&lt;p>项目源代码参见 &lt;a href="https://github.com/liang2kl/APIKit">liang2kl/APIKit&lt;/a>（&lt;del>虽然还没写完&lt;/del>）。&lt;/p>
&lt;/blockquote>
&lt;p>利用 Property Wrapper 包装参数，加上一些基本信息，就可以定义一个 HTTP 请求了。最重要的是，我们再也不需要手动设置 parameters 了。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://developer.apple.com/documentation/swift/mirror">Mirror | Apple Developer Documentation&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content></item><item><title>协议 + 泛型：威力巨大的组合</title><link>https://yeshe.ng/blog/posts/2021/08/protocol-generic/</link><pubDate>Thu, 26 Aug 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/08/protocol-generic/</guid><description>在 Swift 的 protocol 中使用泛型，进行愉快的 Protocol-oriented Programming</description><content>&lt;p>写树洞客户端时，和一个 PKU 的同学一起写，他负责写调用树洞 HTTP API 获取数据的部分。既然涉及到协作，就得先规定下接口了。&lt;/p>
&lt;p>对于 Swift 来说，规定接口的最好的办法自然是 &lt;code>protocol&lt;/code> ——规定并强制实现所有的接口。于是，我着手写这个协议的声明。&lt;/p>
&lt;blockquote>
&lt;p>源代码参见 &lt;a href="https://github.com/liang2kl/HollowCore">liang2kl/HollowCore&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;h2 id="协议的设计">协议的设计&lt;/h2>
&lt;h3 id="目标">目标&lt;/h3>
&lt;p>协议的设计有两个目标：&lt;/p>
&lt;ul>
&lt;li>所有的请求均提供统一的接口，方便调用&lt;/li>
&lt;li>尽可能方便不同的请求类复用代码&lt;/li>
&lt;/ul>
&lt;h3 id="v10最初尝试">v1.0：最初尝试&lt;/h3>
&lt;p>接口无非是 &lt;code>输入&lt;/code> + &lt;code>输出&lt;/code>，对于网络请求，输入即是请求的参数，输出是得到的 response。很容易想到这样一个声明：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 用网络请求参数初始化请求&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(parameters: [String : String])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 开始请求&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (String?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>init&lt;/code> 接受一个字典作为输入，&lt;code>performRequest&lt;/code> 接受一个回调函数，异步处理数据。&lt;/p>
&lt;p>乍看之下，似乎没什么问题——调用者只需要将参数的字典传进去，然后接收 JSON 格式的 &lt;code>String&lt;/code> 字符串就行了。不过，这个设计真的合理吗？&lt;/p>
&lt;p>仔细分析，这个版本的协议存在这些问题：&lt;/p>
&lt;ol>
&lt;li>调用者的输入 &lt;code>parameters: [String : String]&lt;/code> 没有受到任何限制，导致：
&lt;ul>
&lt;li>调用者在每次调用时都需要查看文档以确定需要传哪些参数&lt;/li>
&lt;li>被调用者需要繁琐地一一检查字典的 key 和 value 是否符合要求&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>直接返回原始的 JSON 字符串，每次调用后均需要解析&lt;/li>
&lt;/ol>
&lt;p>问题的根本在于，完成网络请求的类并不知道任何关于请求本身的具体信息。我们需要向类提供这些信息，使得类只接受&lt;strong>符合规定的输入&lt;/strong>，并提供&lt;strong>解析好的、符合规定的输出&lt;/strong>。而且，完成这个目标最好是依赖编译期进行静态的检查，避免可能出现的失误。朝着这个目标，我们来写 &lt;code>2.0&lt;/code> 版本。&lt;/p>
&lt;h3 id="v20添加请求信息">v2.0：添加请求信息&lt;/h3>
&lt;p>我们需要向 &lt;code>Request&lt;/code> 协议内添加参数、输出的信息。&lt;/p>
&lt;p>首先考虑传递参数的问题。一个朴素的想法是在 &lt;code>init&lt;/code> 中分别列出参数，如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(token: String, page: Int, ...)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是这就违背了我们**「提供一个统一的接口」**的宗旨。理想情况下，我们应该提供这样一个接口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>Configuration&lt;/code> 包装了所有需要的参数，这通常使用 &lt;code>struct&lt;/code> 完成。&lt;/p>
&lt;p>但是，不同请求的 &lt;code>Configuration&lt;/code> 是不一样的，该怎么办？&lt;/p>
&lt;p>幸运的是，&lt;code>protocol&lt;/code> 支持“泛型”，我们只需要针对不同的请求定义不同的 &lt;code>Configuration&lt;/code> 类就可以了。同理，每个请求应该有对应的 &lt;code>Response&lt;/code> 类型。另外，为了方便处理错误，添加一个 &lt;code>Error&lt;/code> 类型，其遵循 Swift 中 &lt;code>Error&lt;/code> 协议，用来指定具体的错误。新的设计如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Error: Swift.Error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 储存参数信息&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: Configuration { &lt;span style="color:#66d9ef">get&lt;/span> &lt;span style="color:#66d9ef">set&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (Response?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们可以开始实现具体的请求了。比如，对于编辑收藏的请求：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">EditAttentionRequest&lt;/span>: Request {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Configuration&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> token: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> postId: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> switchToAttention: Bool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Response&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 暂时省略&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">enum&lt;/span> &lt;span style="color:#a6e22e">Error&lt;/span>: Swift.Error {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> tokenExpired,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.configuration = configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (Response?, Error?) -&amp;gt; Void) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> urlPath = &lt;span style="color:#e6db74">&amp;#34;v3/edit/attention&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，对于调用者来说就轻松很多了，不再需要翻文档来传参数，也不需要自己来解析 JSON 字符串，只需要根据 &lt;code>Configuration&lt;/code> 的构造函数传递参数，并利用得到的 &lt;code>Response&lt;/code> 即可：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> configuration = EditAttentionRequest.Configuration(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> request = EditAttentionRequest(configuration: configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>request.performRequest { response, error &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个版本的 &lt;code>Request&lt;/code> 基本上实现了我们设计一个统一接口的目标。&lt;/p>
&lt;h3 id="v21response-的界定">v2.1：Response 的界定&lt;/h3>
&lt;p>注意到上面的 &lt;code>EditAttentionRequest&lt;/code> 中省略了 &lt;code>Response&lt;/code> 的定义。我们需要明确 &lt;code>Response&lt;/code> 是什么。&lt;/p>
&lt;p>从调用者的角度，&lt;code>Response&lt;/code> 应该是可以直接利用，而不需要再进行处理的数据；而从请求本身的角度，&lt;code>Response&lt;/code> 指的是服务器返回的 JSON 字符串解析出来的对象。但是，这两个概念并不总是一样的：服务器返回的结果往往需要进一步处理，比如指示请求状态的 &lt;code>code&lt;/code>，错误信息 &lt;code>msg&lt;/code> 等。&lt;/p>
&lt;p>于是，&lt;code>Response&lt;/code> 的概念需要分成两部分：服务器返回的结果 &lt;code>Result&lt;/code>，和处理后最终返回的可以直接使用的数据 &lt;code>ResultData&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Result
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype ResultData
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Error: Swift.Error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (ResultData?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>相对应地，&lt;code>EditAttentionRequest&lt;/code> 中 &lt;code>Result&lt;/code> 和 &lt;code>ResultData&lt;/code> 分别定义为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Result&lt;/span>: DefaultRequestResult {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> data: Post?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">typealias&lt;/span> ResultData = Post
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在编辑完关注状态后，服务器会返回完整的树洞信息 &lt;code>Post&lt;/code>，以及 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>。我们不需要 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>（错误已经在请求内部处理），于是 &lt;code>Post&lt;/code> 作为 &lt;code>ResultData&lt;/code> 就可以了。&lt;/p>
&lt;blockquote>
&lt;p>定义 &lt;code>Result&lt;/code> 类型是很有必要的：我们一般直接使用 &lt;code>JSONDecoder&lt;/code> 将 JSON 反序列化为对象，这需要我们提供解析的模版，即 &lt;code>Result&lt;/code>。&lt;/p>
&lt;/blockquote>
&lt;p>至此，我们的协议声明已经基本上完成了。所有遵循 &lt;code>Request&lt;/code> 协议的类型都对外提供了统一的接口，并且利用泛型实现了对参数输入、服务器返回数据、最终输出和错误的严格规定。&lt;/p>
&lt;h2 id="代码复用与接口扩展">代码复用与接口扩展&lt;/h2>
&lt;p>上面我们花了大力气来规定 &lt;code>Request&lt;/code> 的各种类型，但除了统一了接口、约束输入输出外并没有什么特别的作用。实际上，协议 + 泛型的巨大威力体现在&lt;strong>代码复用&lt;/strong>和&lt;strong>接口扩展&lt;/strong>上。&lt;/p>
&lt;h3 id="代码复用">代码复用&lt;/h3>
&lt;p>上面的 &lt;code>Request&lt;/code> 只是规定了接口，我们还要逐一实现每个请求的 &lt;code>performRequest&lt;/code>。如果分别独立写 HTTP 请求代码，将十分繁琐，而且会大量重复相同代码。为此，我们需要进行一些默认实现。&lt;/p>
&lt;p>大多数请求满足两个共性：&lt;/p>
&lt;ul>
&lt;li>从 JSON 反序列化为对象的要求很简单：&lt;code>Result&lt;/code> 类型遵循 &lt;code>Decodable&lt;/code> 即可。对于这些请求，&lt;strong>由获得的 JSON 生成 &lt;code>Result&lt;/code> 的方法是固定的&lt;/strong>。&lt;/li>
&lt;li>返回包括 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>，用来传递查询结果和错误信息。因此，&lt;strong>从 &lt;code>Result&lt;/code> 生成 &lt;code>Error&lt;/code> 的方法是固定的&lt;/strong>。而大多数的 &lt;code>Error&lt;/code> 类型也是相同的。&lt;/li>
&lt;/ul>
&lt;p>利用这两点，我们可以在 &lt;code>Request&lt;/code> 的基础上，针对这些请求进行 &lt;code>performRequest&lt;/code> 的默认实现。对这些请求，我们定义一个新的协议 &lt;code>DefaultRequest&lt;/code>，遵循 &lt;code>Request&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">DefaultRequest&lt;/span>: Request
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">where&lt;/span> Error == DefaultRequestError, Result: DefaultRequestResult { }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Error&lt;/code> 为 &lt;code>DefaultRequestError&lt;/code>，是一个涵盖了所有错误类型的 &lt;code>enum&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">enum&lt;/span> &lt;span style="color:#a6e22e">DefaultRequestError&lt;/span>: Swift.Error, LocalizedError {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> decodeFailed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> tokenExpiredError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> fileTooLarge
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> unknown
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> noSuchPost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> other(description: String)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(errorCode: Int, description: String?) {...}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Result&lt;/code> 遵循 &lt;code>DefaultRequestResult&lt;/code>，其定义如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">DefaultRequestResult&lt;/span>: Codable {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int { &lt;span style="color:#66d9ef">get&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String? { &lt;span style="color:#66d9ef">get&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分别对应上述两个共同点。&lt;/p>
&lt;p>于是，我们就可以进行 &lt;code>performRequest&lt;/code> 的默认实现了。在 &lt;code>DefaultRequest&lt;/code> 的 &lt;code>extension&lt;/code>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">DefaultRequest&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: [String : Any]? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: HTTPHeaders? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: HTTPMethod,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: @escaping (Result) -&amp;gt; ResultData?,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: @escaping (ResultData?, Error?) -&amp;gt; Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) {...}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在函数内，我们需要实现：&lt;/p>
&lt;ul>
&lt;li>将网络请求得到的 JSON 反序列化为对象，由 &lt;code>DefaultRequestResult&lt;/code> 遵循 &lt;code>Codable&lt;/code> 满足
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> jsonDecoder = JSONDecoder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> result = &lt;span style="color:#66d9ef">try&lt;/span> jsonDecoder.decode(Result.&lt;span style="color:#66d9ef">self&lt;/span>, from: data)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>将 &lt;code>Result&lt;/code> 中包含的错误信息转化为 &lt;code>Error&lt;/code> 返回，由 &lt;code>Error&lt;/code> 类型已知以及 &lt;code>DefaultRequestResult&lt;/code> 中要求 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code> 满足
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> error = DefaultRequestError(errorCode: result.code, description: result.msg)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>将 &lt;code>Result&lt;/code> 转化为 &lt;code>ResultData&lt;/code>，由传入的参数 &lt;code>transformer&lt;/code> 提供
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = transformer(result) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(data, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>我们在对 &lt;code>protocol&lt;/code> 的泛型作了一定的约束后，获得了更多的信息，使得我们可以在不知道请求的具体类型的情况下进行默认实现&lt;/strong>。&lt;/p>
&lt;p>完整代码如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: [String : Any]? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: HTTPHeaders? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: HTTPMethod,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: @escaping (Result) -&amp;gt; ResultData?,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: @escaping (ResultData?, Error?) -&amp;gt; Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AF.request(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot &lt;span style="color:#f92672">+&lt;/span> urlPath,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: method,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: parameters,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> encoding: URLEncoding.&lt;span style="color:#66d9ef">default&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: headers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .validate()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .responseJSON { response &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> response.result {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .success:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> jsonDecoder = JSONDecoder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">do&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">guard&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = response.data &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .unknown)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> result = &lt;span style="color:#66d9ef">try&lt;/span> jsonDecoder.decode(Result.&lt;span style="color:#66d9ef">self&lt;/span>, from: data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> result.code &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// result code &amp;gt;= 0 valid!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = transformer(result) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(data, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .unknown)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> error = DefaultRequestError(errorCode: result.code, description: result.msg)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">catch&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .decodeFailed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> .failure(error):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> errorCode = error.responseCode, errorCode == &lt;span style="color:#ae81ff">413&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .fileTooLarge)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .other(description: error.localizedDescription))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，我们可以轻松实现具体类型的 &lt;code>performRequest&lt;/code> 了，如 &lt;code>EditAttentionRequest&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (ResultData?, Error?) -&amp;gt; Void) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> urlPath = &lt;span style="color:#e6db74">&amp;#34;v3/edit/attention&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> Constants.urlSuffix
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> headers: HTTPHeaders = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.token,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Accept&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> parameters: [String : Encodable] = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pid&amp;#34;&lt;/span> : &lt;span style="color:#66d9ef">self&lt;/span>.configuration.postId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;switch&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.switchToAttention ? &lt;span style="color:#ae81ff">1&lt;/span> : &lt;span style="color:#ae81ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> performRequest(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.apiRoot,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: urlPath,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: parameters,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: headers,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: .post,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: { $0.data },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: completion
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们只需要将 &lt;code>Configuration&lt;/code> 转换为字典就可以了。在实际项目中，几乎所有的请求都可以遵循 &lt;code>DefaultRequest&lt;/code>，只需要写一次网络请求的代码，就可以实现所有的网络请求。这就是 &lt;strong>&lt;code>protocol&lt;/code> + 泛型 + 限制条件&lt;/strong>的魅力。&lt;/p>
&lt;h3 id="接口扩展">接口扩展&lt;/h3>
&lt;p>利用 &lt;code>protocol&lt;/code> 的泛型，还有个好处在于扩展接口十分方便。例如，针对 &lt;code>Swift 5.5&lt;/code> 的 &lt;code>async/await&lt;/code>，我们很容易为 &lt;code>Request&lt;/code> 添加异步接口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">result&lt;/span>() async -&amp;gt; (ResultData?, Error?) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> await withCheckedContinuation { continuation &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> performRequest { result &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> continuation.resume(returning: result)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>几乎不需要写任何代码。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>Swift 的 &lt;code>protocol&lt;/code> 相当于 C++ 的抽象类，不过相比 C++ 更加严谨、灵活。&lt;code>protocol&lt;/code> 本身有规定接口的功能，再加上泛型，能够很好地实现多态，实现高效的代码复用，同时保持良好的可维护性和可扩展性。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID521">Protocols — The Swift Programming Language (Swift 5.5)&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content></item></channel></rss>