-
Notifications
You must be signed in to change notification settings - Fork 5
/
feed.xml
4328 lines (3191 loc) · 560 KB
/
feed.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?><feed xmlns="https://www.w3.org/2005/Atom" xml:lang="zh_CN"><generator uri="https://jekyllrb.com/" version="4.2.1">Jekyll</generator><link href="https://www.desgard.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.desgard.com/" rel="alternate" type="text/html" hreflang="zh_CN" /><updated>2023-09-14T20:18:23-08:00</updated><id>https://www.desgard.com/feed.xml</id><title type="html">一片瓜田</title><subtitle>I write more code to write less.
</subtitle><author><name>冬瓜</name><email>[email protected]</email></author><entry><title type="html">WWDC23 - What’s new in Swift</title><link href="https://www.desgard.com/2023/06/21/what's_new_in_swfit_2023.html" rel="alternate" type="text/html" title="WWDC23 - What’s new in Swift" /><published>2023-06-21T00:00:00-08:00</published><updated>2023-06-21T00:00:00-08:00</updated><id>https://www.desgard.com/2023/06/21/what's_new_in_swfit_2023</id><content type="html" xml:base="https://www.desgard.com/2023/06/21/what's_new_in_swfit_2023.html"><![CDATA[<blockquote>
<p>摘要:这个 Session 涉及了 Swift 的新语法特性和 Swift Macro 的话题,这些功能对于编写更加灵活和健壮的 API 以及高质量代码起到了很大的帮助。此外,也深入探讨了在受限环境下使用 Swift 的优势,并讨论了 Swift 在适配多种平台设备和语言方面的灵活性。</p>
</blockquote>
<p>本文基于 <a href="https://developer.apple.com/videos/play/wwdc2023/10164/">Session 10164</a> 梳理。</p>
<blockquote>
<p>审核:</p>
<ul>
<li>
<p>stevapple:学生,Swift/Vapor 社区贡献者</p>
</li>
<li>
<p>kemchenj:老司机技术核心成员 / 开源爱好者 / 最近在做 3D 重建相关的开发工作</p>
</li>
</ul>
</blockquote>
<p>今年是 Swift 的一次重大更新,这个 Session 将和大家一起聊聊 Swift 语言今年的一些改进、简洁的语法糖以及一些强大的新 API 的使用。一年前,Swift 项目的核心团队宣布了成立 Language Steering Group ,主要负责监督 Swift 语言和标准库的修改。从这之后,Language Steering Group 已经审核了 40 多个新的提案,这些会覆盖到我们今天所要讨论的几个。</p>
<p>有很多社区提案都有相同的思路和看法,所以我们会将这些类似的提案进行合并。而在这之中,Swift Macro(在 C 的中文教材中,一般称作<strong>宏</strong>,下文继续使用 Macro 进行代替)是提及最多的一个提案,所以我们后文也会具体讨论 Swift 5.9 中关于 Swift Macro 的新特性。</p>
<p>当然,语言的演进只是 Swift 社区工作的一部分,一门成功的语言需要的远不止这些,它还需要配合优秀的工具链、多平台的强大支持以及丰富的文档。为了全方位的监控进展,核心团队也正在组建一个 Ecosystem Steering Group ,这个新的团队也在 swift.org 的博客中有所提及,我们可以一起期待一下这个团队的进一步公告。</p>
<p>现在我们进入正题,来讨论一下今年 Swift 语言的更新。</p>
<h2 id="ifelse-和-switch-语句表达式"><code class="language-plaintext highlighter-rouge">if/else</code> 和 <code class="language-plaintext highlighter-rouge">switch</code> 语句表达式</h2>
<p>Swift 5.9 中允许 <code class="language-plaintext highlighter-rouge">if/else</code> 和 <code class="language-plaintext highlighter-rouge">switch</code> 作为表达式,从而输出返回值。这个特性将会为你的代码提供一种优雅的写法。例如,你如果有一个 <code class="language-plaintext highlighter-rouge">let</code> 变量,其赋值语句是一个非常复杂的三元表达式:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">bullet</span> <span class="o">=</span>
<span class="n">isRoot</span> <span class="o">&&</span> <span class="p">(</span><span class="n">count</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="o">!</span><span class="n">willExpand</span><span class="p">)</span> <span class="p">?</span> <span class="s">""</span>
<span class="p">:</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">?</span> <span class="s">"- "</span>
<span class="p">:</span> <span class="n">maxDepth</span> <span class="o"><=</span> <span class="mi">0</span> <span class="p">?</span> <span class="s">"▹ "</span> <span class="p">:</span> <span class="s">"▿ "</span>
</code></pre></div></div>
<p>对于 <code class="language-plaintext highlighter-rouge">bullet</code> 变量的赋值条件,你可能会觉得可读性十分差。而现在,我们可以直接使用 <code class="language-plaintext highlighter-rouge">if/else</code> 表达式来改善可读性:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">bullet</span> <span class="o">=</span>
<span class="k">if</span> <span class="n">isRoot</span> <span class="o">&&</span> <span class="p">(</span><span class="n">count</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="o">!</span><span class="n">willExpand</span><span class="p">)</span> <span class="p">{</span> <span class="s">""</span> <span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span> <span class="s">"- "</span> <span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="n">maxDepth</span> <span class="o"><=</span> <span class="mi">0</span> <span class="p">{</span> <span class="s">"▹ "</span> <span class="p">}</span>
<span class="k">else</span> <span class="p">{</span> <span class="s">"▿ "</span> <span class="p">}</span>
</code></pre></div></div>
<p>如此修改后,我们的代码会让大家一目了然。</p>
<p>另外,在声明一个全局变量的时候,这种特性会十分友好。之前,你需要将它放在一个 closure 中,写起来是十分繁琐。例如以下代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">attributedName</span> <span class="o">=</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">displayName</span><span class="p">,</span> <span class="o">!</span><span class="n">displayName</span><span class="o">.</span><span class="n">isEmpty</span> <span class="p">{</span>
<span class="kt">AttributedString</span><span class="p">(</span><span class="nv">markdown</span><span class="p">:</span> <span class="n">displayName</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="s">"Untitled"</span>
<span class="p">}</span>
<span class="p">}()</span>
</code></pre></div></div>
<p>但是当我们使用这个新特性,我们可以直接去掉累赘的 closure 写法,将其简化成以下代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">attributedName</span> <span class="o">=</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">displayName</span><span class="p">,</span> <span class="o">!</span><span class="n">displayName</span><span class="o">.</span><span class="n">isEmpty</span> <span class="p">{</span>
<span class="kt">AttributedString</span><span class="p">(</span><span class="nv">markdown</span><span class="p">:</span> <span class="n">displayName</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="s">"Untitled"</span>
<span class="p">}</span>
</code></pre></div></div>
<p>因为 <code class="language-plaintext highlighter-rouge">if/else</code>是一个带有返回值的表达式,所以这个特性可以避免之前啰嗦的写法,让代码更简洁。</p>
<h2 id="result-builder-相关工具链优化">Result Builder 相关工具链优化</h2>
<p>Result Builder (结果生成器)是驱动 SwiftUI 声明式语法的 Swift 特性之一。在前几个版本中,Swift 编译器需要花费很长的时间来确定错误,因为类型检查器搜索了大量无效路径。</p>
<p>从 Swift 5.8 开始,错误代码的类型检查速度将大幅度提升,对错误代码的错误信息现在也更加准确。</p>
<p>例如,我们来看下以下代码:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_01.png" alt="" /></p>
<p>在这个代码中,核心的问题就是 <code class="language-plaintext highlighter-rouge">NavigationLink(value: .one)</code> 中,<code class="language-plaintext highlighter-rouge">.one</code> 是一个类型错误的参数。但是在 Swift 5.7 旧版本中,会报出如图中展示的错误。Swift 5.8 对 Result Builder 诊断做了优化,不仅提高了诊断的准确性,而且也大幅度优化了时间开销。在 Swift 5.8 及之后的版本中,你将会立即查看到正确的语义诊断错误提示,例如下图:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_02.png" alt="" /></p>
<h2 id="repeat-关键字和-type-parameter-pack"><code class="language-plaintext highlighter-rouge">repeat</code> 关键字和 Type Parameter Pack</h2>
<p>在日常使用 Swift 语言中,我们会经常使用 Array ,并结合泛型特性来提供一个存储任何类型的数组。由于 Swift 具有强大的类型推断能力,使用时只需要提供其中的元素,Swift 编译器将会自动推断出来这个数组的类型。</p>
<p>但是在实际使用中,这个场景其实具有局限性。例如我们有一组数据需要处理,且它们不仅仅是单一类型的 <code class="language-plaintext highlighter-rouge">Result</code>,而是多个类型的 <code class="language-plaintext highlighter-rouge">Result</code> 入参。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Request</span><span class="o"><</span><span class="kt">Result</span><span class="o">></span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span>
<span class="kd">struct</span> <span class="kt">RequestEvaluator</span> <span class="p">{</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">Result</span><span class="o">></span><span class="p">(</span><span class="n">_</span> <span class="nv">request</span><span class="p">:</span> <span class="kt">Request</span><span class="o"><</span><span class="kt">Result</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="kt">Result</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">evaluate</span><span class="p">(</span><span class="n">_</span> <span class="nv">request</span><span class="p">:</span> <span class="kt">Request</span><span class="o"><</span><span class="kt">Bool</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="kt">Bool</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">RequestEvaluator</span><span class="p">()</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这里的 <code class="language-plaintext highlighter-rouge">evaluate</code> 方法只是一个实例,因为在实际使用过程中,我们会接收多个参数,就像下面这样:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">value</span> <span class="o">=</span> <span class="kt">RequestEvaluator</span><span class="p">()</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">let</span> <span class="p">(</span><span class="nv">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">)</span> <span class="o">=</span> <span class="kt">RequestEvaluator</span><span class="p">()</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="n">r1</span><span class="p">,</span> <span class="n">r2</span><span class="p">)</span>
<span class="k">let</span> <span class="p">(</span><span class="nv">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">,</span> <span class="nv">z</span><span class="p">)</span> <span class="o">=</span> <span class="kt">RequestEvaluator</span><span class="p">()</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="n">r1</span><span class="p">,</span> <span class="n">r2</span><span class="p">,</span> <span class="n">r3</span><span class="p">)</span>
</code></pre></div></div>
<p>所以在实现的时候,我们还需要实现下面的这些多入参泛型方法:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">Result</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:)</span> <span class="o">-></span> <span class="p">(</span><span class="kt">Result</span><span class="p">)</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:)</span> <span class="o">-></span> <span class="p">(</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">)</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:)</span> <span class="o">-></span> <span class="p">(</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">)</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:)</span><span class="o">-></span> <span class="p">(</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="p">)</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="p">,</span> <span class="kt">R5</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:)</span> <span class="o">-></span> <span class="p">(</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="p">,</span> <span class="kt">R5</span><span class="p">)</span>
<span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="p">,</span> <span class="kt">R5</span><span class="p">,</span> <span class="kt">R6</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:</span><span class="nv">_</span><span class="p">:)</span> <span class="o">-></span> <span class="p">(</span><span class="kt">R1</span><span class="p">,</span> <span class="kt">R2</span><span class="p">,</span> <span class="kt">R3</span><span class="p">,</span> <span class="kt">R4</span><span class="p">,</span> <span class="kt">R5</span><span class="p">,</span> <span class="kt">R6</span><span class="p">)</span>
</code></pre></div></div>
<p>如此实现之后,我们就可以接收 1 - 6 个参数。但是好巧不巧,如果需要传入 7 个参数:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">results</span> <span class="o">=</span> <span class="n">evaluator</span><span class="o">.</span><span class="nf">evaluate</span><span class="p">(</span><span class="n">r1</span><span class="p">,</span> <span class="n">r2</span><span class="p">,</span> <span class="n">r3</span><span class="p">,</span> <span class="n">r4</span><span class="p">,</span> <span class="n">r5</span><span class="p">,</span> <span class="n">r6</span><span class="p">,</span> <span class="n">r7</span><span class="p">)</span>
</code></pre></div></div>
<p>对于这种尴尬的场景,在旧版本的 Swift 中就需要继续增加参数定义,从而兼容 7 个入参的场景。但是 Swift 5.9 将会简化这个流程,我们引入 <strong>Type Parameter Pack</strong> 这个概念。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">evaluate</span><span class="o"><</span><span class="n">each</span> <span class="kt">Result</span><span class="o">></span><span class="p">(</span><span class="nv">_</span><span class="p">:</span> <span class="k">repeat</span> <span class="kt">Request</span><span class="o"><</span><span class="n">each</span> <span class="kt">Result</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="p">(</span><span class="k">repeat</span> <span class="n">each</span> <span class="kt">Result</span><span class="p">)</span>
</code></pre></div></div>
<p>我们来看引入 Type Parameter Pack 概念之后我们将如何修改这个场景。</p>
<ol>
<li><code class="language-plaintext highlighter-rouge"><each Result></code> - 这里代表我将创建一个名字叫 <code class="language-plaintext highlighter-rouge">Result</code> 的 Type Parameter Pack;</li>
<li><code class="language-plaintext highlighter-rouge">repeat each Result</code> - 这是一个 Pack Expansion,它将 Type Parameter Pack 作为实际上表示的类型。其实这里你可以理解成<code class="language-plaintext highlighter-rouge">(each Result)...</code>,即 Type Parameter Pack 类型的不定参数。所以 <code class="language-plaintext highlighter-rouge">repeat</code>关键字更像是一个运算符(Operator);</li>
</ol>
<p>通过这样定义,我们就可以传入不受限制个数的参数,并且可以保证每一个入参都是独立泛型类型。</p>
<p>当然这个特性,最大的受益场景就是 SwiftUI ,因为当一个 <code class="language-plaintext highlighter-rouge">View</code> 内嵌多个 <code class="language-plaintext highlighter-rouge">View</code> 的时候,SwiftUI 官方的方法就是通过泛型以及 Result Builder 进行设计的,并且最大的子 <code class="language-plaintext highlighter-rouge">View</code>有 10 个为上限的数量限制。当引入了 Type Parameter Pack 这个特性之后,限制将被突破,API 设计也更加简洁和易读。</p>
<blockquote>
<p>Stevapple: type parameter pack 是 variadic generics(可变泛型)系列 feature 的一部分,更大的范畴上是增强 Swift 泛型系统的一部分。Variadic generics 的概念其实可以这么理解:generics 是对类型进行抽象,而 variadic generics 希望在此基础上增加对参数数量的抽象。具体的提案可以查看<a href="https://github.com/hborla/swift-evolution/blob/variadic-generics-vision/vision-documents/variadic-generics.md">这里</a>。</p>
</blockquote>
<h2 id="macro">Macro</h2>
<p>在 Swift 5.9 中,对于 Macro 的支持是重大的更新。通过 Macro ,你可以扩展语言本身的能力,消除繁琐的样板代码,并解锁 Swift 更多的表达能力。</p>
<p>我们先来看断言(assert)方法,它是用于检查一个条件是否为 <code class="language-plaintext highlighter-rouge">true</code>。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">assert</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">==</span> <span class="n">c</span><span class="p">)</span>
</code></pre></div></div>
<p>如果是 <code class="language-plaintext highlighter-rouge">false</code> ,断言将停止程序运行。在通常情况下,你获得到错误信息很少,因为你需要修改每一处断言调用,增加 message 信息,这样才能有效定位很多边界情况问题。</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_03.png" alt="" /></p>
<p>其实 <code class="language-plaintext highlighter-rouge">XCTest</code> 也提供了一个 <code class="language-plaintext highlighter-rouge">XCAssertEqual</code> 方法,可以展示更多的错误信息。但我们实际操作后发现,即使我们知道了两边的值,也无法定位到到底是 <code class="language-plaintext highlighter-rouge">max</code> 方法的错误,还是右边 <code class="language-plaintext highlighter-rouge">c</code> 变量的问题。</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_04.png" alt="" /></p>
<p>所以我们应该怎么扩展错误信息呢?在 Macro 特性推出之前,也许我们没有更好的方法。下面我们来看看如何用 Macro 特性来改善这一痛点。首先我们使用新的 hash(<code class="language-plaintext highlighter-rouge">#</code>) 的关键字来重写这段代码。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#assert(max(a, b) == c)</span>
</code></pre></div></div>
<p>或许你会觉得这个写法很熟悉,因为 Swift 中已经有很多类似的关键字,例如 <code class="language-plaintext highlighter-rouge">#file</code>、<code class="language-plaintext highlighter-rouge">#selector(...)</code> 、<code class="language-plaintext highlighter-rouge">#warning</code> 的语法。当我们断言失败后,则会使用文本符号来展示一个更加详尽的图,例如这样:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_05.png" alt="" /></p>
<p>如果我们想自己定义一个这种带有 <code class="language-plaintext highlighter-rouge">#</code> 符号的 Macro 要如何定义呢?</p>
<p>我们可以使用 <code class="language-plaintext highlighter-rouge">macro</code> 关键字来声明一个 Macro:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@freestanding</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span>
<span class="kd">public</span> <span class="n">macro</span> <span class="nf">assert</span><span class="p">(</span><span class="n">_</span> <span class="nv">condition</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="o">=</span> <span class="err">#</span><span class="nf">externalMacro</span><span class="p">(</span>
<span class="nv">module</span><span class="p">:</span> <span class="s">"PowerAssertPlugin"</span><span class="p">,</span>
<span class="nv">type</span><span class="p">:</span> <span class="s">"PowerAssertMacro"</span>
<span class="p">)</span>
</code></pre></div></div>
<p>大多数的 Macro 被定义为 External Macro,这些 External Macro 会在编译器插件的独立程序中被定义。Swift 编译器将使用 Macro 的代码传递给插件,插件产生替换后的代码,并整合回 Swift 程序中。此处,Macro 将 <code class="language-plaintext highlighter-rouge">#assert</code> 扩展成可以展示每个值的代码,其工作流程如下图:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_06.png" alt="" /></p>
<h3 id="expression-macro">Expression Macro</h3>
<p>以上介绍的 <code class="language-plaintext highlighter-rouge">#assert</code> 这个 Macro 还是一个独立表达式。我们之所以称之为“独立”,是因为它使用的是 <code class="language-plaintext highlighter-rouge">#</code> 这个关键字作为前缀调用的一种类型。这种类型是使用 <code class="language-plaintext highlighter-rouge">@freestanding(expression)</code> 进行声明的。这种表达式 Macro ,可以作为一个指定类型的 Value,放在对应的位置,编译器也会更加友好地检测它的类型。</p>
<p>在最新版本的 Foundation 中,<code class="language-plaintext highlighter-rouge">Predicate</code> API 提供了一个表达式 Macro 的极好的例子。Predicate Macro 允许你使用闭包的方式,并且会对类型安全进行校验。这种可读性很强,我们看下面的示例代码,一探便知:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">pred</span> <span class="o">=</span> <span class="err">#</span><span class="kt">Predicate</span><span class="o"><</span><span class="kt">Person</span><span class="o">></span> <span class="p">{</span>
<span class="nv">$0</span><span class="o">.</span><span class="n">favoriteColor</span> <span class="o">==</span> <span class="o">.</span><span class="n">blue</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">blueLovers</span> <span class="o">=</span> <span class="n">people</span><span class="o">.</span><span class="nf">filter</span><span class="p">(</span><span class="n">pred</span><span class="p">)</span>
</code></pre></div></div>
<p>在 Predicate Macro 中,输入的类型是一个 Type Parameter Pack Expansion,这也就是它为什么能接受任意类型、任意数量的闭包入参。而返回值是一个 <code class="language-plaintext highlighter-rouge">Bool</code>类型的值。在使用上,我们发现它仍旧使用 <code class="language-plaintext highlighter-rouge">#</code> 符号的 Hashtag 方式,所以依旧是一个表达式 Macro。我们可以来看看它的定义代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@freestanding</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span>
<span class="kd">public</span> <span class="n">macro</span> <span class="kt">Predicate</span><span class="o"><</span><span class="n">each</span> <span class="kt">Input</span><span class="o">></span><span class="p">(</span>
<span class="n">_</span> <span class="nv">body</span><span class="p">:</span> <span class="p">(</span><span class="k">repeat</span> <span class="n">each</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Bool</span>
<span class="p">)</span> <span class="o">-></span> <span class="kt">Predicate</span><span class="o"><</span><span class="k">repeat</span> <span class="n">each</span> <span class="kt">Input</span><span class="o">></span>
</code></pre></div></div>
<p>从定义上我们可以看到,这里很巧妙地用到了 Type Parameter Pack 这个特性,并且其返回值是一个 <code class="language-plaintext highlighter-rouge">Predicate</code> 类型的实例,从而可以实现多处复用的 Predicate 范式定义。</p>
<h3 id="attached-macro">Attached Macro</h3>
<p>除了这种表达式 Macro,我们还有大量的代码,需要我们对输入的参数,做出自定义的修改或者增加条件限制,显然没有输入参数的表达式 Macro 无法应对这种场景。此时,我们引入其他的 Macro,来帮助我们按照要求生成模版代码。</p>
<p>我们举个例子,例如在开发中,我们需要使用 Path 枚举类型,它可以标记一个路径是绝对路径还是相对路径。此时我们可能会遇到一个需求:<strong>从一个集合中,筛选出所有的绝对路径</strong>。例如如下代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">case</span> <span class="nf">relative</span><span class="p">(</span><span class="kt">String</span><span class="p">)</span>
<span class="k">case</span> <span class="nf">absolute</span><span class="p">(</span><span class="kt">String</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">absPaths</span> <span class="o">=</span> <span class="n">paths</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">isAbsolute</span> <span class="p">}</span>
<span class="kd">extension</span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">isAbsolute</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">case</span> <span class="o">.</span><span class="n">absolute</span> <span class="o">=</span> <span class="k">self</span> <span class="p">{</span> <span class="kc">true</span> <span class="p">}</span>
<span class="k">else</span> <span class="p">{</span> <span class="kc">false</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">var</span> <span class="nv">isRelative</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">case</span> <span class="o">.</span><span class="n">relative</span> <span class="o">=</span> <span class="k">self</span> <span class="p">{</span> <span class="kc">true</span> <span class="p">}</span>
<span class="k">else</span> <span class="p">{</span> <span class="kc">false</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>我们会发现,对于 <code class="language-plaintext highlighter-rouge">enum</code> 中的每一个 <code class="language-plaintext highlighter-rouge">case</code> 我们都需要写一个这种 <code class="language-plaintext highlighter-rouge">isCaseX</code> 的方法,从而增加一个过滤方法。此处是 <code class="language-plaintext highlighter-rouge">Path</code> 只有两种类型,但是如果在 case 更多的场景,这是一个相当繁琐的工作,而且一旦新增会带来很大的代码改动。</p>
<p>此处,我们可以引入 Attached Macro ,来生成繁琐的重复代码。使用后的代码如下:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@CaseDetection</span>
<span class="kd">enum</span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">case</span> <span class="nf">relative</span><span class="p">(</span><span class="kt">String</span><span class="p">)</span>
<span class="k">case</span> <span class="nf">absolute</span><span class="p">(</span><span class="kt">String</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">absPaths</span> <span class="o">=</span> <span class="n">paths</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="n">isAbsolute</span> <span class="p">}</span>
</code></pre></div></div>
<p>此处的 <code class="language-plaintext highlighter-rouge">@CaseDetection</code> 是我们自定义的一个 Attached Macro,这个 Macro 的标志是使用和 Property Wrappers 特性中相同的 <code class="language-plaintext highlighter-rouge">@</code> 符号作为语法关键字,然后将其加到你需要添加的代码前方进行使用。它会根据你对于 Attached Macro 的实现,来生成代码,从而满足需求。</p>
<p>在上面这个例子中,我们的作用范围是 <code class="language-plaintext highlighter-rouge">Path</code> 这个 enum 类型的每一个 Member ,所以在使用的时候,表达是 <code class="language-plaintext highlighter-rouge">@attached(member)</code>。Attached Macro 提供 5 种作用范围给开发者使用:</p>
<table>
<thead>
<tr>
<th>Attached Macro 类型</th>
<th>作用范围</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#member-macros"><code class="language-plaintext highlighter-rouge">@attached(member)</code></a></td>
<td>为类型/扩展添加声明(Declaration)</td>
</tr>
<tr>
<td><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#peer-macros"><code class="language-plaintext highlighter-rouge">@attached(peer)</code></a></td>
<td>为声明添加新的声明</td>
</tr>
<tr>
<td><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#accessor-macros"><code class="language-plaintext highlighter-rouge">@attached(accessor)</code></a></td>
<td>为存储属性添加访问方法(<code class="language-plaintext highlighter-rouge">set</code>/<code class="language-plaintext highlighter-rouge">get</code>/<code class="language-plaintext highlighter-rouge">willSet</code>/<code class="language-plaintext highlighter-rouge">didSet</code>)</td>
</tr>
<tr>
<td><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#member-attribute-macros"><code class="language-plaintext highlighter-rouge">@attached(memberAttribute)</code></a></td>
<td>为类型/扩展添加注解(Attributes)声明</td>
</tr>
<tr>
<td><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0389-attached-macros.md#conformance-macros"><code class="language-plaintext highlighter-rouge">@attached(conformance)</code></a></td>
<td>为类型/扩展添加遵循的协议</td>
</tr>
</tbody>
</table>
<p>通过这些 Attached Macro 的组合使用,可以达到很好的效果。这里最重要的例子就是我们在 SwiftUI 中经常使用到的 observation 特性。</p>
<p>通过 observation 特性,我们可以观察到 class 中的 Property 的变化。想要使用这个功能,只需要让类型遵循 <code class="language-plaintext highlighter-rouge">ObservableObject</code> 协议,将每个属性标记为 <code class="language-plaintext highlighter-rouge">@Published</code> ,最后在 View 中使用 <code class="language-plaintext highlighter-rouge">ObservedObject</code> 的 Property Wrapper 即可。我们来写个示例代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Observation in SwiftUI</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
<span class="kd">@Published</span> <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
<span class="kd">@Published</span> <span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">@Published</span> <span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">person</span><span class="p">:</span> <span class="kt">Person</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Hello, </span><span class="se">\(</span><span class="n">person</span><span class="o">.</span><span class="n">name</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>我们会发现以上的 3 步,会有很多繁琐且复杂的工作需要我们手工处理。如果我们忘记了一步,就无法完成观察 Property 变化、自动触发 UI 刷新这样的需求。</p>
<p>当我们有了 Attached Macro 之后,我们就可以简化声明过程。例如我们有一个 <code class="language-plaintext highlighter-rouge">@Observable</code> 的 Macro ,可以完成以上操作。则我们的代码就可以精简成这样:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Observation in SwiftUI</span>
<span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
<span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">person</span><span class="p">:</span> <span class="kt">Person</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Hello, </span><span class="se">\(</span><span class="n">person</span><span class="o">.</span><span class="n">name</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>我们发现我们的代码得到了极大的精简,这得益于我们组合使用 Attached Macro 的效果。在 Macro 声明部分的代码如下:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@attached</span><span class="p">(</span><span class="n">member</span><span class="p">,</span> <span class="nv">names</span><span class="p">:</span> <span class="o">...</span><span class="p">)</span>
<span class="kd">@attached</span><span class="p">(</span><span class="n">memberAttribute</span><span class="p">)</span>
<span class="kd">@attached</span><span class="p">(</span><span class="n">conformance</span><span class="p">)</span>
<span class="kd">public</span> <span class="n">macro</span> <span class="kt">Observable</span><span class="p">()</span> <span class="o">=</span> <span class="err">#</span><span class="nf">externalMacro</span><span class="p">(</span><span class="o">...</span><span class="p">)</span><span class="o">.</span>
</code></pre></div></div>
<p>下面我们来深入探究一下每一个 Attached Macro 的作用。</p>
<p>以下是关键代码:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
<span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span>
<span class="p">}</span>
</code></pre></div></div>
<p>首先,Member Macro 会引入新的属性和方法,编译器将 Macro 代码替换后将变成这样:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
<span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span>
<span class="c1">// 这里定义了一个 `ObservationRegistrar` 实例,</span>
<span class="c1">// 用于管理观察事件的实例,当属性发生变化将通知所有 Observer</span>
<span class="kd">internal</span> <span class="k">let</span> <span class="nv">_</span><span class="err">$</span><span class="n">observationRegistrar</span> <span class="o">=</span> <span class="kt">ObservationRegistrar</span><span class="o"><</span><span class="kt">Person</span><span class="o">></span><span class="p">()</span>
<span class="c1">// access 方法会在属性被访问时调用,通过 ObservationRegistrar 的 access</span>
<span class="c1">// 方法,并传入被访问的属性 keyPath,来触发事件</span>
<span class="kd">internal</span> <span class="kd">func</span> <span class="n">access</span><span class="o"><</span><span class="kt">Member</span><span class="o">></span><span class="p">(</span>
<span class="nv">keyPath</span><span class="p">:</span> <span class="kt">KeyPath</span><span class="o"><</span><span class="kt">Person</span><span class="p">,</span> <span class="kt">Member</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span>
<span class="n">_</span><span class="err">$</span><span class="n">observationRegistrar</span><span class="o">.</span><span class="nf">access</span><span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="nv">keyPath</span><span class="p">:</span> <span class="n">keyPath</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// withMutation 方法用于在修改属性时触发观察方法。在修改属性之前和之后分别触发观察事件,</span>
<span class="c1">// 以便于观察者可以检测到属性的变化。这个方法通过 `ObservationRegistrar` 的 `withMutation`</span>
<span class="c1">// 方法,传入被修改的属性的 keyPath 和一个闭包,这个闭包包含了对属性的修改操作</span>
<span class="kd">internal</span> <span class="kd">func</span> <span class="n">withMutation</span><span class="o"><</span><span class="kt">Member</span><span class="p">,</span> <span class="kt">T</span><span class="o">></span><span class="p">(</span>
<span class="nv">keyPath</span><span class="p">:</span> <span class="kt">KeyPath</span><span class="o"><</span><span class="kt">Person</span><span class="p">,</span> <span class="kt">Member</span><span class="o">></span><span class="p">,</span>
<span class="n">_</span> <span class="nv">mutation</span><span class="p">:</span> <span class="p">()</span> <span class="k">throws</span> <span class="o">-></span> <span class="kt">T</span>
<span class="p">)</span> <span class="k">rethrows</span> <span class="o">-></span> <span class="kt">T</span> <span class="p">{</span>
<span class="k">try</span> <span class="n">_</span><span class="err">$</span><span class="n">observationRegistrar</span><span class="o">.</span><span class="nf">withMutation</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="k">self</span><span class="p">,</span> <span class="nv">keyPath</span><span class="p">:</span> <span class="n">keyPath</span><span class="p">,</span> <span class="n">mutation</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>其次,Member Attribute Macro 会为所有的属性添加 <code class="language-plaintext highlighter-rouge">@ObservationTracked</code> ,这个 Property Wrapper 会为属性添加 <code class="language-plaintext highlighter-rouge">get</code> 和 <code class="language-plaintext highlighter-rouge">set</code> 方法。</p>
<p>最后,Conformance Macro 会让 <code class="language-plaintext highlighter-rouge">Person</code> 这个 class 遵循 <code class="language-plaintext highlighter-rouge">Observable</code>协议。</p>
<p>通过这三个宏的改造,编译器将代码进行展开后,我们的真实代码类似以下:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
<span class="kd">@ObservationTracked</span> <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="k">get</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="k">set</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="p">}</span>
<span class="kd">@ObservationTracked</span> <span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span> <span class="p">{</span> <span class="k">get</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="k">set</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="p">}</span>
<span class="kd">@ObservationTracked</span> <span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span> <span class="k">get</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="k">set</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span> <span class="p">}</span>
<span class="kd">internal</span> <span class="k">let</span> <span class="nv">_</span><span class="err">$</span><span class="n">observationRegistrar</span> <span class="o">=</span> <span class="kt">ObservationRegistrar</span><span class="o"><</span><span class="kt">Person</span><span class="o">></span><span class="p">()</span>
<span class="kd">internal</span> <span class="kd">func</span> <span class="n">access</span><span class="o"><</span><span class="kt">Member</span><span class="o">></span><span class="p">(</span>
<span class="nv">keyPath</span><span class="p">:</span> <span class="kt">KeyPath</span><span class="o"><</span><span class="kt">Person</span><span class="p">,</span> <span class="kt">Member</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span>
<span class="n">_</span><span class="err">$</span><span class="n">observationRegistrar</span><span class="o">.</span><span class="nf">access</span><span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="nv">keyPath</span><span class="p">:</span> <span class="n">keyPath</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">internal</span> <span class="kd">func</span> <span class="n">withMutation</span><span class="o"><</span><span class="kt">Member</span><span class="p">,</span> <span class="kt">T</span><span class="o">></span><span class="p">(</span>
<span class="nv">keyPath</span><span class="p">:</span> <span class="kt">KeyPath</span><span class="o"><</span><span class="kt">Person</span><span class="p">,</span> <span class="kt">Member</span><span class="o">></span><span class="p">,</span>
<span class="n">_</span> <span class="nv">mutation</span><span class="p">:</span> <span class="p">()</span> <span class="k">throws</span> <span class="o">-></span> <span class="kt">T</span>
<span class="p">)</span> <span class="k">rethrows</span> <span class="o">-></span> <span class="kt">T</span> <span class="p">{</span>
<span class="k">try</span> <span class="n">_</span><span class="err">$</span><span class="n">observationRegistrar</span><span class="o">.</span><span class="nf">withMutation</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="k">self</span><span class="p">,</span> <span class="nv">keyPath</span><span class="p">:</span> <span class="n">keyPath</span><span class="p">,</span> <span class="n">mutation</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>虽然展开后的代码十分复杂,但是绝大多数代码都被 <code class="language-plaintext highlighter-rouge">@Observable</code> Macro 封装起来了,我们只需要输入以下的简洁版本即可实现。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Person</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
<span class="k">var</span> <span class="nv">age</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">isFavorite</span><span class="p">:</span> <span class="kt">Bool</span>
<span class="p">}</span>
</code></pre></div></div>
<p>当你需要对 Macro 展开,希望更好地理解程序代码时,你可以使用 Xcode 的 Expand Macro 功能对 Macro 进行源代码展开。任何在 Macro 生成的代码中出现的错误消息都会引导你去展开代码,从而快速地发现问题。</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_07.png" alt="" /></p>
<p>Swift Macro 为 Swift 实现更具可读性的 API 、构建更简洁的实现语法提供了一种新的模式。Macro 可以根据我们的需求,批量生成 Swift 代码,并在程序中使用 <code class="language-plaintext highlighter-rouge">@</code> 或者 <code class="language-plaintext highlighter-rouge">#</code> 语法快速引入,简化了业务代码,也提高了可读性。</p>
<p>本文只是对 Swift Macro 进行一个初步的介绍以便于你从零到一认识。如果想学习更多,可以查看<a href="https://developer.apple.com/videos/play/wwdc2023/10166/">《Write Swift Macro》</a>这个 Session ,通过实践来编写自己的 Swift Macro。另外如果想对 Swift Macro 的设计理念有更深刻的了解,可以学习<a href="https://developer.apple.com/videos/play/wwdc2023/10167/">《Expand on Swift》</a> 这个 Session。</p>
<h2 id="swift-foundation-升级">Swift Foundation 升级</h2>
<p>Swift 这门编程语言的定位是一种可扩展的语言,其设计理念是使用清晰、简洁的代码,方便阅读和编写。其中的一些强大的特性,例如泛型、async/await 支持等等,可以支撑像 SwiftUI 或 SwiftData 这种更贴近自然描述的框架,从而允许开发者将关注点聚焦在业务上。</p>
<p>为了实现更加宽泛的可扩展性,我们需要将 Swift 适配较于 Objective-C 更多的领域,其中也包括更底层的操作系统领域,这一部分之前是由 C 或 C++ 进行编写的。</p>
<p>最近我们在 Swift 社区中,开源了 Foundation 的 Swift 重构版本,这个措施将使 Apple 以外的平台也可以享受到 Swift 带来的更加高效的性能和开发优势。但是也以为着我们需要用 Swift 对大量的 Objective-C 和 C 代码进行重构。</p>
<p>截止至 macOS Sonoma 和 iOS 17,一些基本类型已经完成了 Swift 重构版本,例如 <code class="language-plaintext highlighter-rouge">Date</code>、<code class="language-plaintext highlighter-rouge">Calendar</code>、<code class="language-plaintext highlighter-rouge">Locale</code> 和 <code class="language-plaintext highlighter-rouge">AttributedString</code>。另外 Swift 实现的 Encoder 和 Decoder 性能相较旧版本也有所提升。</p>
<p>下图是我们通过跑这些类库 Benchmark 测试用例,所得出的 Swift 重构版本的性能提升数据:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_08.png" alt="" /></p>
<p>这些性能提升除了得益于 Swift 整体工具链的性能,另外还有一个原因就是 macOS Sonoma 系统上,我们避免了语言调用时的<strong>桥接成本(Bridging Cost)</strong>,不需要再调用 Objective-C 的代码。我们从 <code class="language-plaintext highlighter-rouge">enumerateDates</code> 这个方法的调用数据统计中可以看到这个变化:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_09.png" alt="" /></p>
<h2 id="ownership--non-copyable-type">Ownership & Non-copyable Type</h2>
<p>在对 Foundation 进行重构时,有时在操作系统的底层操作中,为了达到更高的性能水平,需要更细粒度的掌控。Swift 5.9 引入了“所有权”(Ownership)的概念,用来描述在你的代码中,当值在传递时,是哪段代码在“拥有”该值。</p>
<p>也许这个 Ownership 用文字描述起来有一些抽象,我们来看下示例:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">FileDescriptor</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">fd</span><span class="p">:</span> <span class="kt">CInt</span>
<span class="c1">// 初始化方法,接受文件描述符作为参数</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">descriptor</span><span class="p">:</span> <span class="kt">CInt</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">fd</span> <span class="o">=</span> <span class="n">descriptor</span>
<span class="p">}</span>
<span class="c1">// 写入方法,接受一个 UInt8 数组作为缓冲区,并抛出可能的错误</span>
<span class="kd">func</span> <span class="nf">write</span><span class="p">(</span><span class="nv">buffer</span><span class="p">:</span> <span class="p">[</span><span class="kt">UInt8</span><span class="p">])</span> <span class="k">throws</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">written</span> <span class="o">=</span> <span class="n">buffer</span><span class="o">.</span><span class="n">withUnsafeBufferPointer</span> <span class="p">{</span>
<span class="c1">// 使用 Darwin.write 函数将缓冲区的内容写入文件描述符,并返回写入的字节数</span>
<span class="kt">Darwin</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="nv">$0</span><span class="o">.</span><span class="n">baseAddress</span><span class="p">,</span> <span class="nv">$0</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="c1">// 关闭方法,关闭文件描述符</span>
<span class="kd">func</span> <span class="nf">close</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">Darwin</span><span class="o">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这是一段 FileDescriptor 的封装代码,我们可以用它更加方便的进行写文件操作。但是在使用的时候我们会经常犯一些错误,例如:你可能会在调用 <code class="language-plaintext highlighter-rouge">close()</code> 方法后,再执行 <code class="language-plaintext highlighter-rouge">write(buffer:)</code>方法。再比如:你可能会在 <code class="language-plaintext highlighter-rouge">write(buffer:)</code> 方法后,忘记调用 <code class="language-plaintext highlighter-rouge">close()</code>方法。</p>
<p>对于上面说的第二个场景,我们可以将 <code class="language-plaintext highlighter-rouge">struct</code> 修改成 <code class="language-plaintext highlighter-rouge">class</code>,通过在 <code class="language-plaintext highlighter-rouge">deinit</code> 方法中,调用 <code class="language-plaintext highlighter-rouge">close()</code> 方法,以便于在示例释放的时候,自动关闭。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">FileDescriptor</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="kd">deinit</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>但是这种做法也有它的缺点,例如它会造成额外的内存分配。虽然在通常情况下并不是大问题,但是在操作系统代码中某些受限的场景下是一个问题。</p>
<p>另外,<code class="language-plaintext highlighter-rouge">class</code> 构造出的示例,传递的都是引用。如果这个 <code class="language-plaintext highlighter-rouge">FileDescriptor</code> 在多个线程之间得到访问,导致竞态条件,或者持久化了这个实例导致引用计数始终大于 0 ,内存无法释放,进而引发内存泄漏。</p>
<p>再让我们重新回顾一下之前的 struct 版本。其实这个 struct 的行为也类似于引用类型。它会持有一个 <code class="language-plaintext highlighter-rouge">fd</code> 的整数,这个整数就好比引用了一个文件状态值,我们可以理解成打开了一个文件。如果我们复制了一个实例,相当于我们延长了这个文件的打开状态,如果后续代码中无意对其操作,这是不符合我们预期的。</p>
<p>Swift 的类型,无论是 <code class="language-plaintext highlighter-rouge">struct</code> 还是 <code class="language-plaintext highlighter-rouge">class</code> ,默认都是 Copyable 的。在大多数情况下,不会产生任何问题。但是有的时候,隐式复制的编译器行为,并不是我们希望的结果,尤其是在受限场景下,内存分配是我们要重点关注的问题。在 Swift 5.9 中,可以使用新的语法来强制声明禁止对类型进行隐式复制。当类型不能复制时,则可以像 <code class="language-plaintext highlighter-rouge">class</code> 一样提供一个 <code class="language-plaintext highlighter-rouge">deinit</code> 方法,在类型的值超出作用域时执行该方法。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">FileDescriptor</span><span class="p">:</span> <span class="o">~</span><span class="kt">Copyable</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">fd</span><span class="p">:</span> <span class="kt">CInt</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">descriptor</span><span class="p">:</span> <span class="kt">CInt</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">fd</span> <span class="o">=</span> <span class="n">descriptor</span> <span class="p">}</span>
<span class="kd">func</span> <span class="nf">write</span><span class="p">(</span><span class="nv">buffer</span><span class="p">:</span> <span class="p">[</span><span class="kt">UInt8</span><span class="p">])</span> <span class="k">throws</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">written</span> <span class="o">=</span> <span class="n">buffer</span><span class="o">.</span><span class="n">withUnsafeBufferPointer</span> <span class="p">{</span>
<span class="kt">Darwin</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="nv">$0</span><span class="o">.</span><span class="n">baseAddress</span><span class="p">,</span> <span class="nv">$0</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="n">consuming</span> <span class="kd">func</span> <span class="nf">close</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">Darwin</span><span class="o">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">deinit</span> <span class="p">{</span>
<span class="kt">Darwin</span><span class="o">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>像 <code class="language-plaintext highlighter-rouge">FileDescriptor</code> 这样被声明为 <code class="language-plaintext highlighter-rouge">~Copyable</code> 的类型,我们称之为 Non-copyable types 。我们通过这样声明可以解决之前提出的第一个场景。</p>
<p>这里的 <code class="language-plaintext highlighter-rouge">close</code> 操作,其实就相当于上下文已经放弃了这个实例的 Ownership,这也就是上面代码中 <code class="language-plaintext highlighter-rouge">consuming</code> 关键字的含义。当我们将方法标注为 <code class="language-plaintext highlighter-rouge">consuming</code> 后,就同时声明了 Ownership 的放弃操作,也就意味着在调用上下文中,后文将无法使用该值。</p>
<p>当我们按照这个写法,在实际业务代码中使用的时候,我们会按照这样的执行顺序进行操作:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">file</span> <span class="o">=</span> <span class="kt">FileDescriptor</span><span class="p">(</span><span class="nv">fd</span><span class="p">:</span> <span class="n">descriptor</span><span class="p">)</span>
<span class="n">file</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="nv">buffer</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span>
<span class="n">file</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span>
</code></pre></div></div>
<p>因为 <code class="language-plaintext highlighter-rouge">close</code> 操作被我们标记了是 <code class="language-plaintext highlighter-rouge">consuming</code> 方法,则它必须在最后调用,以确保在此之前上下文代码具有该实例的 Ownership。如果我们写出了错误的调用顺序,编译器将会报错,并提示我们已经放弃了 Ownership ,无法继续调用其他方法。</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_10.png" alt="" /></p>
<p>Non-copyable Type 是 Swift 针对系统编程领域的一项强大的新功能,但目前仍处在早期阶段,后续版本将会不断迭代和扩展 Non-copyable Type 的功能。</p>
<h2 id="与-c-的互操作性">与 C++ 的互操作性</h2>
<h3 id="混编代码">混编代码</h3>
<p>Swift 的推广普及其中一个重要的原因就是和 Objective-C 的互操作性。从一开始,开发者就可以使用 Swift 和 Objective-C 混编的方式,在项目中逐渐将代码替换成 Swift。</p>
<p>但是我们了解到,在很多项目中,不仅用到了 Objective-C,而且还用到了 C++ 来编写核心业务,互操作接口的编写比较麻烦。通常情况下,需要手动添加 bridge 层,Swift 经过 Objective-C ,再调用 C++ ,得到返回值后,再反向传出,这是一个十分繁琐的过程。</p>
<p>Swift 5.9 引入了 Swift 与 C++ 的互操作能力特性,Swift 会将 C++ 的 API 映射成 Swift API ,从而方便调用和获得返回值。</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/session_10164_11.png" alt="" /></p>
<p>C++ 是一个功能强大的语言,具有自己的类、方法、容器等诸多概念。Swift 编译器能够识别 C++ 常见的习惯用法,因此大多数类型可以直接使用。例如下面这个 <code class="language-plaintext highlighter-rouge">Person</code> 类型,定义了 C++ 类型中常见的五个成员函数:拷贝构造函数、转移(有些中文教材也叫做<strong>移动</strong>)构造函数、(两个)赋值重载运算符、析构函数。</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Person.h</span>
<span class="k">struct</span> <span class="nc">Person</span> <span class="p">{</span>
<span class="c1">// 拷贝构造函数: 通过从另一个Person对象进行拷贝来构造新的Person对象</span>
<span class="n">Person</span><span class="p">(</span><span class="k">const</span> <span class="n">Person</span> <span class="o">&</span><span class="p">);</span>
<span class="c1">// 转移构造函数: 通过从另一个Person对象进行移动来构造新的Person对象</span>
<span class="n">Person</span><span class="p">(</span><span class="n">Person</span> <span class="o">&&</span><span class="p">);</span>
<span class="c1">// 拷贝赋值重载运算符: 将另一个Person对象的值赋给当前对象</span>
<span class="n">Person</span> <span class="o">&</span><span class="k">operator</span><span class="o">=</span><span class="p">(</span><span class="k">const</span> <span class="n">Person</span> <span class="o">&</span><span class="p">);</span>
<span class="c1">// 转移赋值重载运算符: 通过移动另一个Person对象的值来赋给当前对象</span>
<span class="n">Person</span> <span class="o">&</span><span class="k">operator</span><span class="o">=</span><span class="p">(</span><span class="n">Person</span> <span class="o">&&</span><span class="p">);</span>
<span class="c1">// 析构函数: 清理Person对象所持有的资源</span>
<span class="o">~</span><span class="n">Person</span><span class="p">();</span>
<span class="c1">// string 类型,存储人员姓名</span>
<span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">name</span><span class="p">;</span>
<span class="c1">// const 代表只读,用于返回人员年龄</span>
<span class="kt">unsigned</span> <span class="n">getAge</span><span class="p">()</span> <span class="k">const</span><span class="p">;</span>
<span class="p">};</span>
<span class="c1">// 函数声明,返回一个 Person 对象的 vector 容器</span>
<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o"><</span><span class="n">Person</span><span class="o">></span> <span class="n">everyone</span><span class="p">();</span>
</code></pre></div></div>
<p>我们通过 Swift 可以直接调用这个 C++ 的 <code class="language-plaintext highlighter-rouge">struct</code> ,也可以直接使用上面定义的 <code class="language-plaintext highlighter-rouge">vector<Person></code> 。补充一句:C++ 的常规容器,例如:<code class="language-plaintext highlighter-rouge">vector</code>、<code class="language-plaintext highlighter-rouge">map</code> 等,Swift 均是可以直接访问的。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Client.swift</span>
<span class="kd">func</span> <span class="nf">greetAdults</span><span class="p">()</span> <span class="p">{</span>
<span class="k">for</span> <span class="n">person</span> <span class="k">in</span> <span class="nf">everyone</span><span class="p">()</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span><span class="o">.</span><span class="nf">getAge</span><span class="p">()</span> <span class="o">>=</span> <span class="mi">18</span> <span class="p">}</span> <span class="p">{</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"Hello, </span><span class="se">\(</span><span class="n">person</span><span class="o">.</span><span class="n">name</span><span class="se">)</span><span class="s">!"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>正如 <code class="language-plaintext highlighter-rouge">greetAdults()</code> 方法描述的这样,我们在 Swift 中可以直接调用 C++ 定义的类型,从而达到和 C++ 的优秀交互能力。</p>
<p>下面来说说“反向”用 C++ 调用 Swift 的场景。C++ 中使用 Swift 的代码基于与 Objective-C 相同的机制,即编译器会自动生成一个 Header 文件,我们可以在 Xcode 中找到生成后的 C++ header。然而与 Objective-C 不同的是,你不需要使用 <code class="language-plaintext highlighter-rouge">@objc</code> 这个注释对方法进行标注。C++ 大多数情况下是可以使用 Swift 完整的 API,包括属性、方法和初始化方法,无需任何桥接成本。</p>
<p>举个例子:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Geometry.swift</span>
<span class="kd">struct</span> <span class="kt">LabeledPoint</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">var</span> <span class="nv">label</span><span class="p">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">"origin"</span>
<span class="k">mutating</span> <span class="kd">func</span> <span class="nf">moveBy</span><span class="p">(</span><span class="n">x</span> <span class="nv">deltaX</span><span class="p">:</span> <span class="kt">Double</span><span class="p">,</span> <span class="n">y</span> <span class="nv">deltaY</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">magnitude</span><span class="p">:</span> <span class="kt">Double</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这是一个 Swift 定义的 <code class="language-plaintext highlighter-rouge">struct</code> ,下面我们在 C++ 文件中来使用它:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// C++ client</span>
<span class="cp">#include</span> <span class="cpf"><Geometry-Swift.h></span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">test</span><span class="p">()</span> <span class="p">{</span>
<span class="n">Point</span> <span class="n">origin</span> <span class="o">=</span> <span class="n">Point</span><span class="p">()</span>
<span class="n">Point</span> <span class="n">unit</span> <span class="o">=</span> <span class="n">Point</span><span class="o">::</span><span class="n">init</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="s">"unit"</span><span class="p">)</span>
<span class="n">unit</span><span class="p">.</span><span class="n">moveBy</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">2</span><span class="p">)</span>
<span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o"><<</span> <span class="n">unit</span><span class="p">.</span><span class="n">label</span> <span class="o"><<</span> <span class="s">" moved to "</span> <span class="o"><<</span> <span class="n">unit</span><span class="p">.</span><span class="n">magnitude</span><span class="p">()</span> <span class="o"><<</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>我们可以看到,在遵循 C++ 语法习惯的前提下,所有的方法名都没有发生变化,无需对 Swift 代码进行定制化修改即可完成调用。</p>
<p>Swift 的 C++ 交互让大家在业务开发中更加容易。许多 C++ 习惯用法可以直接在 Swift 中表达,通常是自动完成的,但偶尔需要一些标记(annotation)来指示所需的语义。而且,可以直接从 C++ 访问 Swift API,无需注释或更改代码,提升了开发效率,也降低了迁移成本。</p>
<blockquote>
<p>有些地方会将 annotation 翻译成“注释”,但是校对者 stevapple 在此处建议使用标记进行翻译,因为是用作编译器来声明额外的语义操作。个人也比较采纳。</p>
</blockquote>
<p>C++ 的交互也是一个不断迭代的 feature,如果想了解更多,可以参看《Mix Swift and C++》这个 session。</p>
<h3 id="构建系统">构建系统</h3>
<p>与 C++ 的交互在语言层面上十分重要,但是我们也不能忽视构建系统的适配。因为使用 Xcode 和 Swift Package Manager 来替换 C++ 的整套构建系统,也是开发者的一个障碍。</p>
<p>这也就是为什么我们要将这个 topic 单独拿出来讨论。Swift 与 CMake 开发社区合作改进了 CMake 对 Swift 的支持。你可以将 Swift 声明为项目使用的一种语言,并将 Swift 源文件加入 target 中,从而将 Swift 代码集成到 CMake 构建中。</p>
<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># CMake</span>
<span class="nb">project</span><span class="p">(</span>PingPong LANGUAGES CXX Swift<span class="p">)</span>
<span class="nb">add_library</span><span class="p">(</span>PingPong
Ping.swift,
Pong.swift,
TableTennisUtils.cpp
<span class="p">)</span>
</code></pre></div></div>
<p>值得一提的是,你也可以在单个 Target 中混合使用 C++ 和 Swift ,CMake 将确保分别编译每个语言,并链接适用于两种语言适当的系统库和 Runtime 库。这也就意味着,你可以使用 Swift 来逐步取代跨平台的 C++ 项目。另外,Swift 社区还提供了一个包含 Swift 和混合 C++/Swift Target 的 CMake 实例存储库,其中包括使用桥接和生成的头文件,来帮助你上手。</p>
<h2 id="swift-concurrency---actor-执行器">Swift Concurrency - Actor 执行器</h2>
<p>几年前,我们引入了基于 async/await 、Structured Concurrency 以及 actors 构建的并发模型。Swift 的并发模型是一个通用抽象模型,可以适配不同的环境和库。在这个通用抽象模型中有两个主要部分,Tasks 和 Actors:</p>
<ol>
<li>Tasks:代表可以在任意位置顺序执行的逻辑。如果有 <code class="language-plaintext highlighter-rouge">await</code> 关键字,tasks 可以被挂起,等其执行完成后继续恢复执行;</li>
<li>Actors:是一种同步机制,提供对隔离状态的互斥访问权。从外部进入一个 actor 需要进行 <code class="language-plaintext highlighter-rouge">await</code> ,否则当前可能会将 tasks 挂起。</li>
</ol>
<p>在内部实现上,Tasks 在全局并发池(Global Concurrent Pool)上执行。全局并发池根据环境决定如何调度任务。在 Apple 平台中,Dispatch 类库为每个系统提供了针对性优化的调度策略。</p>
<p>但是和前文问题一样,我们考虑更受限的环境下,多线程调度的开销我们无法接受。在这种情况下,Swift 的并发模型则会采用单线程的协同队列(Single-threaded Cooperative Queue)进行工作。同样的代码在多种情况下都可以正常工作,因为通用抽象模型可以描述的场景很广,可以覆盖到更多 case。</p>
<p>在标准的 Swift 并发运行场景下, Actors 是通过无锁任务队列(Lock-free Queue of Tasks)来实现的,但这不是唯一的实现方式。在受限环境下,没有原子操作(Atomics),可以使用其他的并发原语(Concurrency Primitive),比如自旋锁。如果考虑单线程环境,则不需要同步机制,但 Actors 模型仍然可被通用模型覆盖到。如此你可以在单线程和多线程环境中,使用同一份代码。</p>
<p>在 Swift 5.9 中,自定义 Actor 执行器(Executors)允许实现特定的同步机制,这使 Actors 变得更加灵活。我们来看一个例子:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Custom actor executors</span>
<span class="c1">// 定义一个名为MyConnection的actor类,用于管理数据库连接</span>
<span class="kd">actor</span> <span class="kt">MyConnection</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">database</span><span class="p">:</span> <span class="kt">UnsafeMutablePointer</span><span class="o"><</span><span class="n">sqlite3</span><span class="o">></span>
<span class="c1">// 初始化方法,接收一个文件名作为参数,并抛出异常</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">filename</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="c1">// 用于清理旧条目的方法</span>
<span class="kd">func</span> <span class="nf">pruneOldEntries</span><span class="p">()</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="c1">// 根据给定的名称和类型,从数据库中获取一个条目</span>
<span class="kd">func</span> <span class="n">fetchEntry</span><span class="o"><</span><span class="kt">Entry</span><span class="o">></span><span class="p">(</span><span class="nv">named</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="kt">Entry</span><span class="o">.</span><span class="k">Type</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Entry</span><span class="p">?</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 在外部调用时使用"await"来暂停当前任务,等待pruneOldEntries方法完成</span>
<span class="k">await</span> <span class="n">connection</span><span class="o">.</span><span class="nf">pruneOldEntries</span><span class="p">()</span>
</code></pre></div></div>
<p>这是一个管理数据库连接的 Actor 例子。Swift 确保代码对 Actor 互斥访问,所以不会出现对数据库的并发访问。但是如果你需要对同步访问进行控制要如何做呢?例如,当你连接数据库的时候,你想在某个队列上执行,而不是一个未知的、未与其他线程共享的队列。在 Swift 5.9 中,可以自定义 actor 执行器,可以这样实现:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">actor</span> <span class="kt">MyConnection</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">database</span><span class="p">:</span> <span class="kt">UnsafeMutablePointer</span><span class="o"><</span><span class="n">sqlite3</span><span class="o">></span>
<span class="c1">// 执行方法的队列</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">queue</span><span class="p">:</span> <span class="kt">DispatchSerialQueue</span>
<span class="c1">// 这里自定义 actor 的执行器,nonisolated 定义为它是一个非孤立方法,即不需要在外部使用 await 关键字</span>
<span class="kd">nonisolated</span> <span class="k">var</span> <span class="nv">unownedExecutor</span><span class="p">:</span> <span class="kt">UnownedSerialExecutor</span> <span class="p">{</span> <span class="n">queue</span><span class="o">.</span><span class="nf">asUnownedSerialExecutor</span><span class="p">()</span> <span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">filename</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">queue</span><span class="p">:</span> <span class="kt">DispatchSerialQueue</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="kd">func</span> <span class="nf">pruneOldEntries</span><span class="p">()</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="kd">func</span> <span class="n">fetchEntry</span><span class="o"><</span><span class="kt">Entry</span><span class="o">></span><span class="p">(</span><span class="nv">named</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">type</span><span class="p">:</span> <span class="kt">Entry</span><span class="o">.</span><span class="k">Type</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Entry</span><span class="p">?</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">await</span> <span class="n">connection</span><span class="o">.</span><span class="nf">pruneOldEntries</span><span class="p">()</span>
</code></pre></div></div>
<p>上述代码中,我们为 actor 添加了一个串行调度队列,并且提供了一个 <code class="language-plaintext highlighter-rouge">unownedExecutor</code> 的实现,用于生成与该队列关联的执行器。通过这个改变,所有 actor 实例的同步方法将通过这个队列来执行。</p>
<p>当你在外部调用 <code class="language-plaintext highlighter-rouge">await connection.pruneOldEntries()</code> 时,其实现在真正的行为是在上方的队列里调用了 <code class="language-plaintext highlighter-rouge">dispatchQueue.async</code> 。有了这个自定义执行器后,我们可以全方位控制 Actor 的方法调度,甚至可以与未使用 Actor 的方法混用并调度他们的执行顺序。</p>
<p>我们可以通过调度队列对 actor 进行同步调度,是因为调度队列遵循了新的 <code class="language-plaintext highlighter-rouge">SerialExecutor</code> 协议。开发者可以通过实现一个符合该协议的类,从而定义自己的调度机制。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Executor protocols</span>
<span class="kd">protocol</span> <span class="kt">Executor</span><span class="p">:</span> <span class="kt">AnyObject</span><span class="p">,</span> <span class="kt">Sendable</span> <span class="p">{</span>
<span class="c1">// 方法 1 </span>
<span class="kd">func</span> <span class="nf">enqueue</span><span class="p">(</span><span class="n">_</span> <span class="nv">job</span><span class="p">:</span> <span class="n">consuming</span> <span class="kt">ExecutorJob</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">protocol</span> <span class="kt">SerialExecutor</span><span class="p">:</span> <span class="kt">Executor</span> <span class="p">{</span>
<span class="c1">// 方法 2:</span>
<span class="kd">func</span> <span class="nf">asUnownedSerialExecutor</span><span class="p">()</span> <span class="o">-></span> <span class="kt">UnownedSerialExecutor</span>
<span class="c1">// 方法 3:</span>
<span class="kd">func</span> <span class="nf">isSameExclusiveExecutionContext</span><span class="p">(</span><span class="n">other</span> <span class="nv">executor</span><span class="p">:</span> <span class="k">Self</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Bool</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">DispatchSerialQueue</span><span class="p">:</span> <span class="kt">SerialExecutor</span> <span class="p">{</span> <span class="err">…</span> <span class="p">}</span>
</code></pre></div></div>
<p>在这个协议中包括了一些核心操作:</p>
<ol>
<li>检查代码是否已经在执行器上下文中执行:如上代码中的方法 3 <code class="language-plaintext highlighter-rouge">isSameExclusiveExecutionContext(other:)</code>。例如:你可以实现是否在主线程上执行。</li>
<li>可以获取这个 Executor 对应的执行器实例,并访问它:如上代码中的方法 2 <code class="language-plaintext highlighter-rouge">asUnownedSerialExecutor()</code>。</li>
<li>将某个 Job 的所有权给到这个执行器:如上述代码中的方法 1 <code class="language-plaintext highlighter-rouge">enqueue(_:)</code>。</li>
</ol>
<p>Job 是需要在执行器上同步完成异步任务,这样的一个概念。从运行表现上来说,还是列举上面数据库连接的例子,<code class="language-plaintext highlighter-rouge">enqueue</code>方法将会在我们声明的队列上,调用 <code class="language-plaintext highlighter-rouge">dispatchQueue.async</code> 方法。</p>
<p>Swift 并发编程目前已经有了几年的经验,Tasks 和 Actor 这套模型也覆盖了诸多并发场景。从 iPhone 到 Apple Watch ,再到 Server ,其已适应不同的执行环境。这是一套复杂却又实用的系统,如果你希望了解更多,可以查看<a href="https://developer.apple.com/videos/play/wwdc2021/10254/">《Behind the Scenes》</a>和<a href="https://developer.apple.com/videos/play/wwdc2023/10170/">《Beyond the basics of Structured Concurrency》</a>这两个 Session。</p>
<h2 id="foundationdb">FoundationDB</h2>
<p>最后我们介绍一点额外的东西,FoundationDB。这是一个分布式数据库,用于在普通硬件上运行的可扩展数据库解决方案。目前已经支持 macOS 、Linux 和 Windows。</p>
<p>FoundationDB 是一个开源项目,代码量很大,且使用 C++ 编写。这些代码是强异步的,具有自己的分布式 Actor 和 Runtime 实现。FoundationDB 项目希望对其代码进行现代化改造,并且认为 Swift 在性能、安全性和代码可读性上与其需求十分匹配。但是完全使用 Swift 重构是一个非常冒险的任务,所以在最新版代码中,开发人员利用 Swift 与 C++ 的交互新特性,进行部分的重构。</p>
<p>首先我们来看一下 FoundationDB 部分的 Actor 代码片段 C++ 实现:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FoundationDB的“master data” actor的C++实现</span>
<span class="c1">// 异步函数,用于获取版本号</span>
<span class="n">ACTOR</span> <span class="n">Future</span><span class="o"><</span><span class="n">Void</span><span class="o">></span> <span class="n">getVersion</span><span class="p">(</span><span class="n">Reference</span><span class="o"><</span><span class="n">MasterData</span><span class="o">></span> <span class="n">self</span><span class="p">,</span> <span class="n">GetCommitVersionRequest</span> <span class="n">req</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 查找请求代理的迭代器</span>
<span class="n">state</span> <span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o"><</span><span class="n">UID</span><span class="p">,</span> <span class="n">CommitProxyVersionReplies</span><span class="o">>::</span><span class="n">iterator</span> <span class="n">proxyItr</span> <span class="o">=</span> <span class="n">self</span><span class="o">-></span><span class="n">lastCommitProxyVersionReplies</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">requestingProxy</span><span class="p">);</span>
<span class="o">++</span><span class="n">self</span><span class="o">-></span><span class="n">getCommitVersionRequests</span><span class="p">;</span>
<span class="c1">// 如果在映射中找不到代理的迭代器,则发送一个“Never”的响应并返回</span>
<span class="k">if</span> <span class="p">(</span><span class="n">proxyItr</span> <span class="o">==</span> <span class="n">self</span><span class="o">-></span><span class="n">lastCommitProxyVersionReplies</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
<span class="n">req</span><span class="p">.</span><span class="n">reply</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">Never</span><span class="p">());</span>
<span class="k">return</span> <span class="n">Void</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// 等待直到最新的请求编号至少达到 req.requestNum - 1</span>
<span class="n">wait</span><span class="p">(</span><span class="n">proxyItr</span><span class="o">-></span><span class="n">second</span><span class="p">.</span><span class="n">latestRequestNum</span><span class="p">.</span><span class="n">whenAtLeast</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">requestNum</span> <span class="o">-</span> <span class="mi">1</span><span class="p">));</span>
<span class="c1">// 在回复的映射中查找与请求编号对应的回复</span>
<span class="k">auto</span> <span class="n">itr</span> <span class="o">=</span> <span class="n">proxyItr</span><span class="o">-></span><span class="n">second</span><span class="p">.</span><span class="n">replies</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">requestNum</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">itr</span> <span class="o">!=</span> <span class="n">proxyItr</span><span class="o">-></span><span class="n">second</span><span class="p">.</span><span class="n">replies</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
<span class="c1">// 如果找到回复,则将其作为响应发送并返回</span>
<span class="n">req</span><span class="p">.</span><span class="n">reply</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">itr</span><span class="o">-></span><span class="n">second</span><span class="p">);</span>
<span class="k">return</span> <span class="n">Void</span><span class="p">();</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这段代码有很多内容,你并不需要了解这段 C++ 代码。其中,我只想指出一个关键的地方:因为 C++ 没有 async/await ,所以 FoundationDB 使用了类似于预处理器(Preprocessor)的方式进行模拟实现。</p>
<p>通过对应的编号进行筛选,从而显式实现了消息匹配,从而将正确的结果返回。最后,FoundationDB 有自己的引用计数智能指针来帮助自动管理内存。上述 C++ 代码描述的就是这样的一个过程。</p>
<p>但如果我们使用 Swift ,这个方法就可以直接使用异步函数的特性,使用 <code class="language-plaintext highlighter-rouge">await</code> 来表示对于请求的匹配,就节省了上述大量的代码逻辑。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FoundationDB的“master data” actor的Swift实现</span>
<span class="c1">// 异步函数,用于获取版本号</span>
<span class="kd">func</span> <span class="nf">getVersion</span><span class="p">(</span>
<span class="nv">myself</span><span class="p">:</span> <span class="kt">MasterData</span><span class="p">,</span> <span class="nv">req</span><span class="p">:</span> <span class="kt">GetCommitVersionRequest</span>
<span class="p">)</span> <span class="k">async</span> <span class="o">-></span> <span class="kt">GetCommitVersionReply</span><span class="p">?</span> <span class="p">{</span>
<span class="c1">// 增加getCommitVersionRequests计数</span>
<span class="n">myself</span><span class="o">.</span><span class="n">getCommitVersionRequests</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="c1">// 检查是否存在请求代理的最后版本回复</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">lastVersionReplies</span> <span class="o">=</span> <span class="n">lastCommitProxyVersionReplies</span><span class="p">[</span><span class="n">req</span><span class="o">.</span><span class="n">requestingProxy</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="c1">// 等待直到latestRequestNum至少达到req.requestNum - 1</span>
<span class="k">var</span> <span class="nv">latestRequestNum</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">lastVersionReplies</span><span class="o">.</span><span class="n">latestRequestNum</span>
<span class="o">.</span><span class="nf">atLeast</span><span class="p">(</span><span class="kt">VersionMetricHandle</span><span class="o">.</span><span class="kt">ValueType</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">requestNum</span> <span class="o">-</span> <span class="kt">UInt64</span><span class="p">(</span><span class="mi">1</span><span class="p">)))</span>
<span class="c1">// 如果存在请求编号对应的最后回复,则返回该回复</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">lastReply</span> <span class="o">=</span> <span class="n">lastVersionReplies</span><span class="o">.</span><span class="n">replies</span><span class="p">[</span><span class="n">req</span><span class="o">.</span><span class="n">requestNum</span><span class="p">]</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">lastReply</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>是不是可读性提高了不少?</p>
<p>另外,我们在这里使用了很多 C++ 类型,C++ 中的 MasterData 类型使用了引用计数智能指针。通过在 C++ 中对类型进行标注,Swift 编译器可以像任何其他类一样使用该类型,自动管理引用计数。</p>
<p>从这个例子中,我们获取到的经验是,我们可以在需要的时候,使用 Swift 的优势来改写逻辑,与现有的 C++ 代码进行接口互调操作,实现渐进式的代码替换,最终也可以推进项目 Swift 化的进程。</p>
<h2 id="总结">总结</h2>
<p>这个 Session 中我们讨论了很多内容,不乏 Swift 的一些新的语法特性和 Swift Macros。这些功能可以实现更加灵活和健壮的 API,帮助你更好地编写高质量代码。</p>
<p>另外,我们也大篇幅的讨论了在受限环境下使用 Swift 的优势,以及 Swift 如何灵活的适配多种平台设备、语言,这些也为我们在编写代码中,获得更多的思考。</p>
<p>这些特性的出现和设计,都是通过 Swift 社区中从 idea 、公开讨论、结果反馈等等流程中孕育而生的。感谢各位开发者的支持,让 Swift 5.9 这门编程语言更加健壮。</p>]]></content><author><name>冬瓜</name><email>[email protected]</email></author><category term="iOS" /><category term="Swift" /><category term="Apple" /><summary type="html"><![CDATA[摘要:这个 Session 涉及了 Swift 的新语法特性和 Swift Macro 的话题,这些功能对于编写更加灵活和健壮的 API 以及高质量代码起到了很大的帮助。此外,也深入探讨了在受限环境下使用 Swift 的优势,并讨论了 Swift 在适配多种平台设备和语言方面的灵活性。 本文基于 Session 10164 梳理。 审核: stevapple:学生,Swift/Vapor 社区贡献者 kemchenj:老司机技术核心成员 / 开源爱好者 / 最近在做 3D 重建相关的开发工作 今年是 Swift 的一次重大更新,这个 Session 将和大家一起聊聊 Swift 语言今年的一些改进、简洁的语法糖以及一些强大的新 API 的使用。一年前,Swift 项目的核心团队宣布了成立 Language Steering Group ,主要负责监督 Swift 语言和标准库的修改。从这之后,Language Steering Group 已经审核了 40 多个新的提案,这些会覆盖到我们今天所要讨论的几个。 有很多社区提案都有相同的思路和看法,所以我们会将这些类似的提案进行合并。而在这之中,Swift Macro(在 C 的中文教材中,一般称作宏,下文继续使用 Macro 进行代替)是提及最多的一个提案,所以我们后文也会具体讨论 Swift 5.9 中关于 Swift Macro 的新特性。 当然,语言的演进只是 Swift 社区工作的一部分,一门成功的语言需要的远不止这些,它还需要配合优秀的工具链、多平台的强大支持以及丰富的文档。为了全方位的监控进展,核心团队也正在组建一个 Ecosystem Steering Group ,这个新的团队也在 swift.org 的博客中有所提及,我们可以一起期待一下这个团队的进一步公告。 现在我们进入正题,来讨论一下今年 Swift 语言的更新。 if/else 和 switch 语句表达式 Swift 5.9 中允许 if/else 和 switch 作为表达式,从而输出返回值。这个特性将会为你的代码提供一种优雅的写法。例如,你如果有一个 let 变量,其赋值语句是一个非常复杂的三元表达式: let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ " 对于 bullet 变量的赋值条件,你可能会觉得可读性十分差。而现在,我们可以直接使用 if/else 表达式来改善可读性: let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " } 如此修改后,我们的代码会让大家一目了然。 另外,在声明一个全局变量的时候,这种特性会十分友好。之前,你需要将它放在一个 closure 中,写起来是十分繁琐。例如以下代码: let attributedName = { if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" } }() 但是当我们使用这个新特性,我们可以直接去掉累赘的 closure 写法,将其简化成以下代码: let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" } 因为 if/else是一个带有返回值的表达式,所以这个特性可以避免之前啰嗦的写法,让代码更简洁。 Result Builder 相关工具链优化 Result Builder (结果生成器)是驱动 SwiftUI 声明式语法的 Swift 特性之一。在前几个版本中,Swift 编译器需要花费很长的时间来确定错误,因为类型检查器搜索了大量无效路径。 从 Swift 5.8 开始,错误代码的类型检查速度将大幅度提升,对错误代码的错误信息现在也更加准确。 例如,我们来看下以下代码: 在这个代码中,核心的问题就是 NavigationLink(value: .one) 中,.one 是一个类型错误的参数。但是在 Swift 5.7 旧版本中,会报出如图中展示的错误。Swift 5.8 对 Result Builder 诊断做了优化,不仅提高了诊断的准确性,而且也大幅度优化了时间开销。在 Swift 5.8 及之后的版本中,你将会立即查看到正确的语义诊断错误提示,例如下图: repeat 关键字和 Type Parameter Pack 在日常使用 Swift 语言中,我们会经常使用 Array ,并结合泛型特性来提供一个存储任何类型的数组。由于 Swift 具有强大的类型推断能力,使用时只需要提供其中的元素,Swift 编译器将会自动推断出来这个数组的类型。 但是在实际使用中,这个场景其实具有局限性。例如我们有一组数据需要处理,且它们不仅仅是单一类型的 Result,而是多个类型的 Result 入参。 struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) } 这里的 evaluate 方法只是一个实例,因为在实际使用过程中,我们会接收多个参数,就像下面这样: let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3) 所以在实现的时候,我们还需要实现下面的这些多入参泛型方法: func evaluate<Result>(_:) -> (Result) func evaluate<R1, R2>(_:_:) -> (R1, R2) func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3) func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4) func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5) func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6) 如此实现之后,我们就可以接收 1 - 6 个参数。但是好巧不巧,如果需要传入 7 个参数: let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7) 对于这种尴尬的场景,在旧版本的 Swift 中就需要继续增加参数定义,从而兼容 7 个入参的场景。但是 Swift 5.9 将会简化这个流程,我们引入 Type Parameter Pack 这个概念。 func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) 我们来看引入 Type Parameter Pack 概念之后我们将如何修改这个场景。 <each Result> - 这里代表我将创建一个名字叫 Result 的 Type Parameter Pack; repeat each Result - 这是一个 Pack Expansion,它将 Type Parameter Pack 作为实际上表示的类型。其实这里你可以理解成(each Result)...,即 Type Parameter Pack 类型的不定参数。所以 repeat关键字更像是一个运算符(Operator); 通过这样定义,我们就可以传入不受限制个数的参数,并且可以保证每一个入参都是独立泛型类型。 当然这个特性,最大的受益场景就是 SwiftUI ,因为当一个 View 内嵌多个 View 的时候,SwiftUI 官方的方法就是通过泛型以及 Result Builder 进行设计的,并且最大的子 View有 10 个为上限的数量限制。当引入了 Type Parameter Pack 这个特性之后,限制将被突破,API 设计也更加简洁和易读。 Stevapple: type parameter pack 是 variadic generics(可变泛型)系列 feature 的一部分,更大的范畴上是增强 Swift 泛型系统的一部分。Variadic generics 的概念其实可以这么理解:generics 是对类型进行抽象,而 variadic generics 希望在此基础上增加对参数数量的抽象。具体的提案可以查看这里。 Macro 在 Swift 5.9 中,对于 Macro 的支持是重大的更新。通过 Macro ,你可以扩展语言本身的能力,消除繁琐的样板代码,并解锁 Swift 更多的表达能力。 我们先来看断言(assert)方法,它是用于检查一个条件是否为 true。 assert(max(a, b) == c) 如果是 false ,断言将停止程序运行。在通常情况下,你获得到错误信息很少,因为你需要修改每一处断言调用,增加 message 信息,这样才能有效定位很多边界情况问题。 其实 XCTest 也提供了一个 XCAssertEqual 方法,可以展示更多的错误信息。但我们实际操作后发现,即使我们知道了两边的值,也无法定位到到底是 max 方法的错误,还是右边 c 变量的问题。 所以我们应该怎么扩展错误信息呢?在 Macro 特性推出之前,也许我们没有更好的方法。下面我们来看看如何用 Macro 特性来改善这一痛点。首先我们使用新的 hash(#) 的关键字来重写这段代码。 #assert(max(a, b) == c) 或许你会觉得这个写法很熟悉,因为 Swift 中已经有很多类似的关键字,例如 #file、#selector(...) 、#warning 的语法。当我们断言失败后,则会使用文本符号来展示一个更加详尽的图,例如这样: 如果我们想自己定义一个这种带有 # 符号的 Macro 要如何定义呢? 我们可以使用 macro 关键字来声明一个 Macro: @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: "PowerAssertPlugin", type: "PowerAssertMacro" ) 大多数的 Macro 被定义为 External Macro,这些 External Macro 会在编译器插件的独立程序中被定义。Swift 编译器将使用 Macro 的代码传递给插件,插件产生替换后的代码,并整合回 Swift 程序中。此处,Macro 将 #assert 扩展成可以展示每个值的代码,其工作流程如下图: Expression Macro 以上介绍的 #assert 这个 Macro 还是一个独立表达式。我们之所以称之为“独立”,是因为它使用的是 # 这个关键字作为前缀调用的一种类型。这种类型是使用 @freestanding(expression) 进行声明的。这种表达式 Macro ,可以作为一个指定类型的 Value,放在对应的位置,编译器也会更加友好地检测它的类型。 在最新版本的 Foundation 中,Predicate API 提供了一个表达式 Macro 的极好的例子。Predicate Macro 允许你使用闭包的方式,并且会对类型安全进行校验。这种可读性很强,我们看下面的示例代码,一探便知: let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred) 在 Predicate Macro 中,输入的类型是一个 Type Parameter Pack Expansion,这也就是它为什么能接受任意类型、任意数量的闭包入参。而返回值是一个 Bool类型的值。在使用上,我们发现它仍旧使用 # 符号的 Hashtag 方式,所以依旧是一个表达式 Macro。我们可以来看看它的定义代码: @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input> 从定义上我们可以看到,这里很巧妙地用到了 Type Parameter Pack 这个特性,并且其返回值是一个 Predicate 类型的实例,从而可以实现多处复用的 Predicate 范式定义。 Attached Macro 除了这种表达式 Macro,我们还有大量的代码,需要我们对输入的参数,做出自定义的修改或者增加条件限制,显然没有输入参数的表达式 Macro 无法应对这种场景。此时,我们引入其他的 Macro,来帮助我们按照要求生成模版代码。 我们举个例子,例如在开发中,我们需要使用 Path 枚举类型,它可以标记一个路径是绝对路径还是相对路径。此时我们可能会遇到一个需求:从一个集合中,筛选出所有的绝对路径。例如如下代码: enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute } extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } } 我们会发现,对于 enum 中的每一个 case 我们都需要写一个这种 isCaseX 的方法,从而增加一个过滤方法。此处是 Path 只有两种类型,但是如果在 case 更多的场景,这是一个相当繁琐的工作,而且一旦新增会带来很大的代码改动。 此处,我们可以引入 Attached Macro ,来生成繁琐的重复代码。使用后的代码如下: @CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute } 此处的 @CaseDetection 是我们自定义的一个 Attached Macro,这个 Macro 的标志是使用和 Property Wrappers 特性中相同的 @ 符号作为语法关键字,然后将其加到你需要添加的代码前方进行使用。它会根据你对于 Attached Macro 的实现,来生成代码,从而满足需求。 在上面这个例子中,我们的作用范围是 Path 这个 enum 类型的每一个 Member ,所以在使用的时候,表达是 @attached(member)。Attached Macro 提供 5 种作用范围给开发者使用: Attached Macro 类型 作用范围 @attached(member) 为类型/扩展添加声明(Declaration) @attached(peer) 为声明添加新的声明 @attached(accessor) 为存储属性添加访问方法(set/get/willSet/didSet) @attached(memberAttribute) 为类型/扩展添加注解(Attributes)声明 @attached(conformance) 为类型/扩展添加遵循的协议 通过这些 Attached Macro 的组合使用,可以达到很好的效果。这里最重要的例子就是我们在 SwiftUI 中经常使用到的 observation 特性。 通过 observation 特性,我们可以观察到 class 中的 Property 的变化。想要使用这个功能,只需要让类型遵循 ObservableObject 协议,将每个属性标记为 @Published ,最后在 View 中使用 ObservedObject 的 Property Wrapper 即可。我们来写个示例代码: // Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } } 我们会发现以上的 3 步,会有很多繁琐且复杂的工作需要我们手工处理。如果我们忘记了一步,就无法完成观察 Property 变化、自动触发 UI 刷新这样的需求。 当我们有了 Attached Macro 之后,我们就可以简化声明过程。例如我们有一个 @Observable 的 Macro ,可以完成以上操作。则我们的代码就可以精简成这样: // Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } } 我们发现我们的代码得到了极大的精简,这得益于我们组合使用 Attached Macro 的效果。在 Macro 声明部分的代码如下: @attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...). 下面我们来深入探究一下每一个 Attached Macro 的作用。 以下是关键代码: @Observable final class Person { var name: String var age: Int var isFavorite: Bool } 首先,Member Macro 会引入新的属性和方法,编译器将 Macro 代码替换后将变成这样: @Observable final class Person { var name: String var age: Int var isFavorite: Bool // 这里定义了一个 `ObservationRegistrar` 实例, // 用于管理观察事件的实例,当属性发生变化将通知所有 Observer internal let _$observationRegistrar = ObservationRegistrar<Person>() // access 方法会在属性被访问时调用,通过 ObservationRegistrar 的 access // 方法,并传入被访问的属性 keyPath,来触发事件 internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } // withMutation 方法用于在修改属性时触发观察方法。在修改属性之前和之后分别触发观察事件, // 以便于观察者可以检测到属性的变化。这个方法通过 `ObservationRegistrar` 的 `withMutation` // 方法,传入被修改的属性的 keyPath 和一个闭包,这个闭包包含了对属性的修改操作 internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } 其次,Member Attribute Macro 会为所有的属性添加 @ObservationTracked ,这个 Property Wrapper 会为属性添加 get 和 set 方法。 最后,Conformance Macro 会让 Person 这个 class 遵循 Observable协议。 通过这三个宏的改造,编译器将代码进行展开后,我们的真实代码类似以下: @Observable final class Person: ObservableObject { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } 虽然展开后的代码十分复杂,但是绝大多数代码都被 @Observable Macro 封装起来了,我们只需要输入以下的简洁版本即可实现。 @Observable final class Person { var name: String var age: Int var isFavorite: Bool } 当你需要对 Macro 展开,希望更好地理解程序代码时,你可以使用 Xcode 的 Expand Macro 功能对 Macro 进行源代码展开。任何在 Macro 生成的代码中出现的错误消息都会引导你去展开代码,从而快速地发现问题。 Swift Macro 为 Swift 实现更具可读性的 API 、构建更简洁的实现语法提供了一种新的模式。Macro 可以根据我们的需求,批量生成 Swift 代码,并在程序中使用 @ 或者 # 语法快速引入,简化了业务代码,也提高了可读性。 本文只是对 Swift Macro 进行一个初步的介绍以便于你从零到一认识。如果想学习更多,可以查看《Write Swift Macro》这个 Session ,通过实践来编写自己的 Swift Macro。另外如果想对 Swift Macro 的设计理念有更深刻的了解,可以学习《Expand on Swift》 这个 Session。 Swift Foundation 升级 Swift 这门编程语言的定位是一种可扩展的语言,其设计理念是使用清晰、简洁的代码,方便阅读和编写。其中的一些强大的特性,例如泛型、async/await 支持等等,可以支撑像 SwiftUI 或 SwiftData 这种更贴近自然描述的框架,从而允许开发者将关注点聚焦在业务上。 为了实现更加宽泛的可扩展性,我们需要将 Swift 适配较于 Objective-C 更多的领域,其中也包括更底层的操作系统领域,这一部分之前是由 C 或 C++ 进行编写的。 最近我们在 Swift 社区中,开源了 Foundation 的 Swift 重构版本,这个措施将使 Apple 以外的平台也可以享受到 Swift 带来的更加高效的性能和开发优势。但是也以为着我们需要用 Swift 对大量的 Objective-C 和 C 代码进行重构。 截止至 macOS Sonoma 和 iOS 17,一些基本类型已经完成了 Swift 重构版本,例如 Date、Calendar、Locale 和 AttributedString。另外 Swift 实现的 Encoder 和 Decoder 性能相较旧版本也有所提升。 下图是我们通过跑这些类库 Benchmark 测试用例,所得出的 Swift 重构版本的性能提升数据: 这些性能提升除了得益于 Swift 整体工具链的性能,另外还有一个原因就是 macOS Sonoma 系统上,我们避免了语言调用时的桥接成本(Bridging Cost),不需要再调用 Objective-C 的代码。我们从 enumerateDates 这个方法的调用数据统计中可以看到这个变化: Ownership & Non-copyable Type 在对 Foundation 进行重构时,有时在操作系统的底层操作中,为了达到更高的性能水平,需要更细粒度的掌控。Swift 5.9 引入了“所有权”(Ownership)的概念,用来描述在你的代码中,当值在传递时,是哪段代码在“拥有”该值。 也许这个 Ownership 用文字描述起来有一些抽象,我们来看下示例: struct FileDescriptor { private var fd: CInt // 初始化方法,接受文件描述符作为参数 init(descriptor: CInt) { self.fd = descriptor } // 写入方法,接受一个 UInt8 数组作为缓冲区,并抛出可能的错误 func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { // 使用 Darwin.write 函数将缓冲区的内容写入文件描述符,并返回写入的字节数 Darwin.write(fd, $0.baseAddress, $0.count) } // ... } // 关闭方法,关闭文件描述符 func close() { Darwin.close(fd) } } 这是一段 FileDescriptor 的封装代码,我们可以用它更加方便的进行写文件操作。但是在使用的时候我们会经常犯一些错误,例如:你可能会在调用 close() 方法后,再执行 write(buffer:)方法。再比如:你可能会在 write(buffer:) 方法后,忘记调用 close()方法。 对于上面说的第二个场景,我们可以将 struct 修改成 class,通过在 deinit 方法中,调用 close() 方法,以便于在示例释放的时候,自动关闭。 class FileDescriptor { // ... deinit { self.close(fd) } } 但是这种做法也有它的缺点,例如它会造成额外的内存分配。虽然在通常情况下并不是大问题,但是在操作系统代码中某些受限的场景下是一个问题。 另外,class 构造出的示例,传递的都是引用。如果这个 FileDescriptor 在多个线程之间得到访问,导致竞态条件,或者持久化了这个实例导致引用计数始终大于 0 ,内存无法释放,进而引发内存泄漏。 再让我们重新回顾一下之前的 struct 版本。其实这个 struct 的行为也类似于引用类型。它会持有一个 fd 的整数,这个整数就好比引用了一个文件状态值,我们可以理解成打开了一个文件。如果我们复制了一个实例,相当于我们延长了这个文件的打开状态,如果后续代码中无意对其操作,这是不符合我们预期的。 Swift 的类型,无论是 struct 还是 class ,默认都是 Copyable 的。在大多数情况下,不会产生任何问题。但是有的时候,隐式复制的编译器行为,并不是我们希望的结果,尤其是在受限场景下,内存分配是我们要重点关注的问题。在 Swift 5.9 中,可以使用新的语法来强制声明禁止对类型进行隐式复制。当类型不能复制时,则可以像 class 一样提供一个 deinit 方法,在类型的值超出作用域时执行该方法。 struct FileDescriptor: ~Copyable { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } consuming func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } } 像 FileDescriptor 这样被声明为 ~Copyable 的类型,我们称之为 Non-copyable types 。我们通过这样声明可以解决之前提出的第一个场景。 这里的 close 操作,其实就相当于上下文已经放弃了这个实例的 Ownership,这也就是上面代码中 consuming 关键字的含义。当我们将方法标注为 consuming 后,就同时声明了 Ownership 的放弃操作,也就意味着在调用上下文中,后文将无法使用该值。 当我们按照这个写法,在实际业务代码中使用的时候,我们会按照这样的执行顺序进行操作: let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close() 因为 close 操作被我们标记了是 consuming 方法,则它必须在最后调用,以确保在此之前上下文代码具有该实例的 Ownership。如果我们写出了错误的调用顺序,编译器将会报错,并提示我们已经放弃了 Ownership ,无法继续调用其他方法。 Non-copyable Type 是 Swift 针对系统编程领域的一项强大的新功能,但目前仍处在早期阶段,后续版本将会不断迭代和扩展 Non-copyable Type 的功能。 与 C++ 的互操作性 混编代码 Swift 的推广普及其中一个重要的原因就是和 Objective-C 的互操作性。从一开始,开发者就可以使用 Swift 和 Objective-C 混编的方式,在项目中逐渐将代码替换成 Swift。 但是我们了解到,在很多项目中,不仅用到了 Objective-C,而且还用到了 C++ 来编写核心业务,互操作接口的编写比较麻烦。通常情况下,需要手动添加 bridge 层,Swift 经过 Objective-C ,再调用 C++ ,得到返回值后,再反向传出,这是一个十分繁琐的过程。 Swift 5.9 引入了 Swift 与 C++ 的互操作能力特性,Swift 会将 C++ 的 API 映射成 Swift API ,从而方便调用和获得返回值。 C++ 是一个功能强大的语言,具有自己的类、方法、容器等诸多概念。Swift 编译器能够识别 C++ 常见的习惯用法,因此大多数类型可以直接使用。例如下面这个 Person 类型,定义了 C++ 类型中常见的五个成员函数:拷贝构造函数、转移(有些中文教材也叫做移动)构造函数、(两个)赋值重载运算符、析构函数。 // Person.h struct Person { // 拷贝构造函数: 通过从另一个Person对象进行拷贝来构造新的Person对象 Person(const Person &); // 转移构造函数: 通过从另一个Person对象进行移动来构造新的Person对象 Person(Person &&); // 拷贝赋值重载运算符: 将另一个Person对象的值赋给当前对象 Person &operator=(const Person &); // 转移赋值重载运算符: 通过移动另一个Person对象的值来赋给当前对象 Person &operator=(Person &&); // 析构函数: 清理Person对象所持有的资源 ~Person(); // string 类型,存储人员姓名 std::string name; // const 代表只读,用于返回人员年龄 unsigned getAge() const; }; // 函数声明,返回一个 Person 对象的 vector 容器 std::vector<Person> everyone(); 我们通过 Swift 可以直接调用这个 C++ 的 struct ,也可以直接使用上面定义的 vector<Person> 。补充一句:C++ 的常规容器,例如:vector、map 等,Swift 均是可以直接访问的。 // Client.swift func greetAdults() { for person in everyone().filter { $0.getAge() >= 18 } { print("Hello, \(person.name)!") } } 正如 greetAdults() 方法描述的这样,我们在 Swift 中可以直接调用 C++ 定义的类型,从而达到和 C++ 的优秀交互能力。 下面来说说“反向”用 C++ 调用 Swift 的场景。C++ 中使用 Swift 的代码基于与 Objective-C 相同的机制,即编译器会自动生成一个 Header 文件,我们可以在 Xcode 中找到生成后的 C++ header。然而与 Objective-C 不同的是,你不需要使用 @objc 这个注释对方法进行标注。C++ 大多数情况下是可以使用 Swift 完整的 API,包括属性、方法和初始化方法,无需任何桥接成本。 举个例子: // Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = "origin" mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } 这是一个 Swift 定义的 struct ,下面我们在 C++ 文件中来使用它: // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, "unit") unit.moveBy(2, -2) std::cout << unit.label << " moved to " << unit.magnitude() << std::endl; } 我们可以看到,在遵循 C++ 语法习惯的前提下,所有的方法名都没有发生变化,无需对 Swift 代码进行定制化修改即可完成调用。 Swift 的 C++ 交互让大家在业务开发中更加容易。许多 C++ 习惯用法可以直接在 Swift 中表达,通常是自动完成的,但偶尔需要一些标记(annotation)来指示所需的语义。而且,可以直接从 C++ 访问 Swift API,无需注释或更改代码,提升了开发效率,也降低了迁移成本。 有些地方会将 annotation 翻译成“注释”,但是校对者 stevapple 在此处建议使用标记进行翻译,因为是用作编译器来声明额外的语义操作。个人也比较采纳。 C++ 的交互也是一个不断迭代的 feature,如果想了解更多,可以参看《Mix Swift and C++》这个 session。 构建系统 与 C++ 的交互在语言层面上十分重要,但是我们也不能忽视构建系统的适配。因为使用 Xcode 和 Swift Package Manager 来替换 C++ 的整套构建系统,也是开发者的一个障碍。 这也就是为什么我们要将这个 topic 单独拿出来讨论。Swift 与 CMake 开发社区合作改进了 CMake 对 Swift 的支持。你可以将 Swift 声明为项目使用的一种语言,并将 Swift 源文件加入 target 中,从而将 Swift 代码集成到 CMake 构建中。 # CMake project(PingPong LANGUAGES CXX Swift) add_library(PingPong Ping.swift, Pong.swift, TableTennisUtils.cpp ) 值得一提的是,你也可以在单个 Target 中混合使用 C++ 和 Swift ,CMake 将确保分别编译每个语言,并链接适用于两种语言适当的系统库和 Runtime 库。这也就意味着,你可以使用 Swift 来逐步取代跨平台的 C++ 项目。另外,Swift 社区还提供了一个包含 Swift 和混合 C++/Swift Target 的 CMake 实例存储库,其中包括使用桥接和生成的头文件,来帮助你上手。 Swift Concurrency - Actor 执行器 几年前,我们引入了基于 async/await 、Structured Concurrency 以及 actors 构建的并发模型。Swift 的并发模型是一个通用抽象模型,可以适配不同的环境和库。在这个通用抽象模型中有两个主要部分,Tasks 和 Actors: Tasks:代表可以在任意位置顺序执行的逻辑。如果有 await 关键字,tasks 可以被挂起,等其执行完成后继续恢复执行; Actors:是一种同步机制,提供对隔离状态的互斥访问权。从外部进入一个 actor 需要进行 await ,否则当前可能会将 tasks 挂起。 在内部实现上,Tasks 在全局并发池(Global Concurrent Pool)上执行。全局并发池根据环境决定如何调度任务。在 Apple 平台中,Dispatch 类库为每个系统提供了针对性优化的调度策略。 但是和前文问题一样,我们考虑更受限的环境下,多线程调度的开销我们无法接受。在这种情况下,Swift 的并发模型则会采用单线程的协同队列(Single-threaded Cooperative Queue)进行工作。同样的代码在多种情况下都可以正常工作,因为通用抽象模型可以描述的场景很广,可以覆盖到更多 case。 在标准的 Swift 并发运行场景下, Actors 是通过无锁任务队列(Lock-free Queue of Tasks)来实现的,但这不是唯一的实现方式。在受限环境下,没有原子操作(Atomics),可以使用其他的并发原语(Concurrency Primitive),比如自旋锁。如果考虑单线程环境,则不需要同步机制,但 Actors 模型仍然可被通用模型覆盖到。如此你可以在单线程和多线程环境中,使用同一份代码。 在 Swift 5.9 中,自定义 Actor 执行器(Executors)允许实现特定的同步机制,这使 Actors 变得更加灵活。我们来看一个例子: // Custom actor executors // 定义一个名为MyConnection的actor类,用于管理数据库连接 actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> // 初始化方法,接收一个文件名作为参数,并抛出异常 init(filename: String) throws { … } // 用于清理旧条目的方法 func pruneOldEntries() { … } // 根据给定的名称和类型,从数据库中获取一个条目 func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } // 在外部调用时使用"await"来暂停当前任务,等待pruneOldEntries方法完成 await connection.pruneOldEntries() 这是一个管理数据库连接的 Actor 例子。Swift 确保代码对 Actor 互斥访问,所以不会出现对数据库的并发访问。但是如果你需要对同步访问进行控制要如何做呢?例如,当你连接数据库的时候,你想在某个队列上执行,而不是一个未知的、未与其他线程共享的队列。在 Swift 5.9 中,可以自定义 actor 执行器,可以这样实现: actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> // 执行方法的队列 private let queue: DispatchSerialQueue // 这里自定义 actor 的执行器,nonisolated 定义为它是一个非孤立方法,即不需要在外部使用 await 关键字 nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries() 上述代码中,我们为 actor 添加了一个串行调度队列,并且提供了一个 unownedExecutor 的实现,用于生成与该队列关联的执行器。通过这个改变,所有 actor 实例的同步方法将通过这个队列来执行。 当你在外部调用 await connection.pruneOldEntries() 时,其实现在真正的行为是在上方的队列里调用了 dispatchQueue.async 。有了这个自定义执行器后,我们可以全方位控制 Actor 的方法调度,甚至可以与未使用 Actor 的方法混用并调度他们的执行顺序。 我们可以通过调度队列对 actor 进行同步调度,是因为调度队列遵循了新的 SerialExecutor 协议。开发者可以通过实现一个符合该协议的类,从而定义自己的调度机制。 // Executor protocols protocol Executor: AnyObject, Sendable { // 方法 1 func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { // 方法 2: func asUnownedSerialExecutor() -> UnownedSerialExecutor // 方法 3: func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … } 在这个协议中包括了一些核心操作: 检查代码是否已经在执行器上下文中执行:如上代码中的方法 3 isSameExclusiveExecutionContext(other:)。例如:你可以实现是否在主线程上执行。 可以获取这个 Executor 对应的执行器实例,并访问它:如上代码中的方法 2 asUnownedSerialExecutor()。 将某个 Job 的所有权给到这个执行器:如上述代码中的方法 1 enqueue(_:)。 Job 是需要在执行器上同步完成异步任务,这样的一个概念。从运行表现上来说,还是列举上面数据库连接的例子,enqueue方法将会在我们声明的队列上,调用 dispatchQueue.async 方法。 Swift 并发编程目前已经有了几年的经验,Tasks 和 Actor 这套模型也覆盖了诸多并发场景。从 iPhone 到 Apple Watch ,再到 Server ,其已适应不同的执行环境。这是一套复杂却又实用的系统,如果你希望了解更多,可以查看《Behind the Scenes》和《Beyond the basics of Structured Concurrency》这两个 Session。 FoundationDB 最后我们介绍一点额外的东西,FoundationDB。这是一个分布式数据库,用于在普通硬件上运行的可扩展数据库解决方案。目前已经支持 macOS 、Linux 和 Windows。 FoundationDB 是一个开源项目,代码量很大,且使用 C++ 编写。这些代码是强异步的,具有自己的分布式 Actor 和 Runtime 实现。FoundationDB 项目希望对其代码进行现代化改造,并且认为 Swift 在性能、安全性和代码可读性上与其需求十分匹配。但是完全使用 Swift 重构是一个非常冒险的任务,所以在最新版代码中,开发人员利用 Swift 与 C++ 的交互新特性,进行部分的重构。 首先我们来看一下 FoundationDB 部分的 Actor 代码片段 C++ 实现: // FoundationDB的“master data” actor的C++实现 // 异步函数,用于获取版本号 ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { // 查找请求代理的迭代器 state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; // 如果在映射中找不到代理的迭代器,则发送一个“Never”的响应并返回 if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } // 等待直到最新的请求编号至少达到 req.requestNum - 1 wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); // 在回复的映射中查找与请求编号对应的回复 auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { // 如果找到回复,则将其作为响应发送并返回 req.reply.send(itr->second); return Void(); } // ... } 这段代码有很多内容,你并不需要了解这段 C++ 代码。其中,我只想指出一个关键的地方:因为 C++ 没有 async/await ,所以 FoundationDB 使用了类似于预处理器(Preprocessor)的方式进行模拟实现。 通过对应的编号进行筛选,从而显式实现了消息匹配,从而将正确的结果返回。最后,FoundationDB 有自己的引用计数智能指针来帮助自动管理内存。上述 C++ 代码描述的就是这样的一个过程。 但如果我们使用 Swift ,这个方法就可以直接使用异步函数的特性,使用 await 来表示对于请求的匹配,就节省了上述大量的代码逻辑。 // FoundationDB的“master data” actor的Swift实现 // 异步函数,用于获取版本号 func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { // 增加getCommitVersionRequests计数 myself.getCommitVersionRequests += 1 // 检查是否存在请求代理的最后版本回复 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... // 等待直到latestRequestNum至少达到req.requestNum - 1 var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) // 如果存在请求编号对应的最后回复,则返回该回复 if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } } 是不是可读性提高了不少? 另外,我们在这里使用了很多 C++ 类型,C++ 中的 MasterData 类型使用了引用计数智能指针。通过在 C++ 中对类型进行标注,Swift 编译器可以像任何其他类一样使用该类型,自动管理引用计数。 从这个例子中,我们获取到的经验是,我们可以在需要的时候,使用 Swift 的优势来改写逻辑,与现有的 C++ 代码进行接口互调操作,实现渐进式的代码替换,最终也可以推进项目 Swift 化的进程。 总结 这个 Session 中我们讨论了很多内容,不乏 Swift 的一些新的语法特性和 Swift Macros。这些功能可以实现更加灵活和健壮的 API,帮助你更好地编写高质量代码。 另外,我们也大篇幅的讨论了在受限环境下使用 Swift 的优势,以及 Swift 如何灵活的适配多种平台设备、语言,这些也为我们在编写代码中,获得更多的思考。 这些特性的出现和设计,都是通过 Swift 社区中从 idea 、公开讨论、结果反馈等等流程中孕育而生的。感谢各位开发者的支持,让 Swift 5.9 这门编程语言更加健壮。]]></summary></entry><entry><title type="html">PancakeHunny 闪电贷 LP 池操控攻击分析</title><link href="https://www.desgard.com/2021/11/03/defi-hunny-attack-analysis.html" rel="alternate" type="text/html" title="PancakeHunny 闪电贷 LP 池操控攻击分析" /><published>2021-11-03T00:00:00-08:00</published><updated>2021-11-03T00:00:00-08:00</updated><id>https://www.desgard.com/2021/11/03/defi-hunny-attack-analysis</id><content type="html" xml:base="https://www.desgard.com/2021/11/03/defi-hunny-attack-analysis.html"><![CDATA[<h1 id="背景">背景</h1>
<p>2021 年 10 月 20 日 UTC 时间上午 9 点,PancakeHunny 平台遭遇闪电贷智能合约攻击,攻击者通过操纵 PCS 上的 WBNB/TUSD 的流动性从而操纵了兑换比例,实现了 HUNNY 铸币合约的大量铸币,完成攻击。</p>
<p>最终攻击者获利 230 万美元(64.2 万是稳定币 + 435.31 ETH),并且大量铸造 HUNNY 代币,将 HUNNY 的价格从 0.3 抛售到 0.1 美元。
</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/1635583246116-9c41c78d-cfc2-4008-8ad1-2682ae038fe4.png" alt="" /></p>
<p>这一操作的 TxHash 从 bscscan 上可以找到:<a href="https://bscscan.com/tx/0x1b698231965b72f64d55c561634600b087154f71bc73fc775622a45112a94a77">0x1b698231965b72f64d55c561634600b087154f71bc73fc775622a45112a94a77</a>。
下面我们来复盘一下整个攻击手法和流程。</p>
<h1 id="代码中的根本原因">代码中的根本原因</h1>
<p>可以查看合约 <a href="https://bscscan.com/address/0x27d4ca4bb855e435959295ec273fa16fe8caea14#code">VaultStrategyAlpacaRabbit</a>,这个合约是可升级合约的原合约地址。目前该合约仍旧由线上 <a href="https://bscscan.com/address/0xef43313e8218f25fe63d5ae76d98182d7a4797cc">TUSD 单币池合约地址</a>进行代理转发(也就是线上还没有进行更换),但是目前官方已经发现了问题,已经关闭了该池的铸币(那其实还不如直接存 Alpaca Finance)。
我们在 <a href="https://bscscan.com/address/0x27d4ca4bb855e435959295ec273fa16fe8caea14#code">VaultStrategyAlpacaRabbit</a> 合约中,可看到以下代码:</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/1635956502729-dab865ce-a8bc-4ee7-9104-991739decb3b.png" alt="" />
</p>
<p>在上述代码中,黄色高亮的一行就是此次攻击的根本原因。原因就是因为这个 swap 的 Path 最终选用的是 <code class="language-plaintext highlighter-rouge">[ALPACA, WBNB, TUSD]</code> ,<strong>然而 TUSD/WBNB 的 LP Token 其流动性仅有 2 美元(这是目前的情况,可以</strong><a href="https://pancakeswap.finance/info/pool/0x1b011a21c02194a449e32f729489d299f907e71a"><strong>查看 PCS 的流动性数据</strong></a><strong>)</strong>,于是攻击者就通过闪电贷放大资金量,从而控制这组 LP Token 的兑换汇率,从而进行攻击。
</p>
<p><img src="https://raw.githubusercontent.com/Desgard/img/master/img/1635957697293-931a04a8-5b51-416f-999b-f2a7fe26ab52.png" alt="" /></p>
<p>接下来我们来分步骤解析这个过程:
</p>
<ul>
<li>攻击者利用闪电贷,借出 270 万 TUSD,并且全部通过 <code class="language-plaintext highlighter-rouge">[TUSD, WBNB]</code> 的 Path 兑换成了 WBNB。根据 AMM 的恒定乘积公式 \(x \times y = k\) ,由于大量的 TUSD 进入到了 TUSD/WBNB Lp 池中,<strong>所以通过十分少量的 WBNB 沿着相反的 Path 就能兑换出大量的 TUSD</strong>。</li>
<li>第二步,攻击者会将一笔可观的 TUSD 数额放入 TUSD 单币池中,让其占据了该池 99% 的收益。此时因为步骤一种操控了 Lp 池,大量的 TUSD 会被兑换出来。</li>
<li>第三步,攻击者会调用 <code class="language-plaintext highlighter-rouge">getReward()</code> 方法,这个方法会调用 <code class="language-plaintext highlighter-rouge">_withdrawStakingToken()</code> 方法,其中会返回 <code class="language-plaintext highlighter-rouge">withdrawAmount</code> 这个变量。</li>
</ul>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">withdrawAmount</span> <span class="o">=</span> <span class="n">_stakingToken</span><span class="nf">.balanceOf</span><span class="p">(</span><span class="nf">address</span><span class="p">(</span><span class="n">this</span><span class="p">))</span><span class="nf">.sub</span><span class="p">(</span><span class="n">stakingTokenBefore</span><span class="p">);</span>
</code></pre></div></div>
<p>它会通过 <code class="language-plaintext highlighter-rouge">_stakingToken</code> 也就是我们的 TUSD 总量来计算。而 <code class="language-plaintext highlighter-rouge">withdrawAmount</code> 就是用来传入到 <code class="language-plaintext highlighter-rouge">minter</code> 中,其价值的 30% 为总量负责铸造 HUNNY 代币的数量控制变量,从而造成大量的 <code class="language-plaintext highlighter-rouge">HUNNY</code> 被铸造。</p>
<ul>
<li>攻击者抛售大量的 <code class="language-plaintext highlighter-rouge">HUNNY</code> 完成此次经济攻击。</li>
</ul>
<h1 id="复盘">复盘</h1>
<p>攻击者完成本次攻击,是与以往的 <code class="language-plaintext highlighter-rouge">BUNNY</code> 攻击有所区别的,<code class="language-plaintext highlighter-rouge">BUNNY</code> 中的错误实在是太低级了,使用了账户余额的代币数量来铸造 <code class="language-plaintext highlighter-rouge">BUNNY</code> 。虽然 <code class="language-plaintext highlighter-rouge">HUNNY</code> 通过使用增量变量的方式避免了 <code class="language-plaintext highlighter-rouge">BUNNY</code> 的漏洞,但对于 LP Token 市值太低容易操纵这一环节没有戒备心,从而导致了经济漏洞。
</p>
<p>反思:在制作机枪池的时候,如果有 Minter 进行铸造操纵,一定要慎之又慎,来验证每一步用到的数量关系,再进行代码编写。</p>]]></content><author><name>冬瓜</name><email>[email protected]</email></author><category term="Blockchain" /><category term="DeFi" /><category term="Web3" /><summary type="html"><![CDATA[背景 2021 年 10 月 20 日 UTC 时间上午 9 点,PancakeHunny 平台遭遇闪电贷智能合约攻击,攻击者通过操纵 PCS 上的 WBNB/TUSD 的流动性从而操纵了兑换比例,实现了 HUNNY 铸币合约的大量铸币,完成攻击。 最终攻击者获利 230 万美元(64.2 万是稳定币 + 435.31 ETH),并且大量铸造 HUNNY 代币,将 HUNNY 的价格从 0.3 抛售到 0.1 美元。 这一操作的 TxHash 从 bscscan 上可以找到:0x1b698231965b72f64d55c561634600b087154f71bc73fc775622a45112a94a77。 下面我们来复盘一下整个攻击手法和流程。 代码中的根本原因 可以查看合约 VaultStrategyAlpacaRabbit,这个合约是可升级合约的原合约地址。目前该合约仍旧由线上 TUSD 单币池合约地址进行代理转发(也就是线上还没有进行更换),但是目前官方已经发现了问题,已经关闭了该池的铸币(那其实还不如直接存 Alpaca Finance)。 我们在 VaultStrategyAlpacaRabbit 合约中,可看到以下代码: 在上述代码中,黄色高亮的一行就是此次攻击的根本原因。原因就是因为这个 swap 的 Path 最终选用的是 [ALPACA, WBNB, TUSD] ,然而 TUSD/WBNB 的 LP Token 其流动性仅有 2 美元(这是目前的情况,可以查看 PCS 的流动性数据),于是攻击者就通过闪电贷放大资金量,从而控制这组 LP Token 的兑换汇率,从而进行攻击。 接下来我们来分步骤解析这个过程: 攻击者利用闪电贷,借出 270 万 TUSD,并且全部通过 [TUSD, WBNB] 的 Path 兑换成了 WBNB。根据 AMM 的恒定乘积公式 \(x \times y = k\) ,由于大量的 TUSD 进入到了 TUSD/WBNB Lp 池中,所以通过十分少量的 WBNB 沿着相反的 Path 就能兑换出大量的 TUSD。 第二步,攻击者会将一笔可观的 TUSD 数额放入 TUSD 单币池中,让其占据了该池 99% 的收益。此时因为步骤一种操控了 Lp 池,大量的 TUSD 会被兑换出来。 第三步,攻击者会调用 getReward() 方法,这个方法会调用 _withdrawStakingToken() 方法,其中会返回 withdrawAmount 这个变量。 withdrawAmount = _stakingToken.balanceOf(address(this)).sub(stakingTokenBefore); 它会通过 _stakingToken 也就是我们的 TUSD 总量来计算。而 withdrawAmount 就是用来传入到 minter 中,其价值的 30% 为总量负责铸造 HUNNY 代币的数量控制变量,从而造成大量的 HUNNY 被铸造。 攻击者抛售大量的 HUNNY 完成此次经济攻击。 复盘 攻击者完成本次攻击,是与以往的 BUNNY 攻击有所区别的,BUNNY 中的错误实在是太低级了,使用了账户余额的代币数量来铸造 BUNNY 。虽然 HUNNY 通过使用增量变量的方式避免了 BUNNY 的漏洞,但对于 LP Token 市值太低容易操纵这一环节没有戒备心,从而导致了经济漏洞。 反思:在制作机枪池的时候,如果有 Minter 进行铸造操纵,一定要慎之又慎,来验证每一步用到的数量关系,再进行代码编写。]]></summary></entry><entry><title type="html">LP Token 价格计算推导及安全性</title><link href="https://www.desgard.com/2021/09/06/defi-lp-price.html" rel="alternate" type="text/html" title="LP Token 价格计算推导及安全性" /><published>2021-09-06T00:00:00-08:00</published><updated>2021-09-06T00:00:00-08:00</updated><id>https://www.desgard.com/2021/09/06/defi-lp-price</id><content type="html" xml:base="https://www.desgard.com/2021/09/06/defi-lp-price.html"><![CDATA[<h1 id="背景">背景</h1>
<p>在实现 CakeBot 的 USDT/USDC 池时,需要计算 LP Token 的代币价值,从而方便的给用户提示 LP Token 当前准确的价格,来计算收益率。所以对 LP Token 的价值计算做了一点深入的研究,并且还翻阅到 Alpha Finance 团队的关于安全获取 LP 价格的方法。
本位将这些学习笔记分享给大家。
</p>
<h1 id="一般-lp-token-价格的获取方法">一般 LP Token 价格的获取方法</h1>
<p>我们知道对于一般 Token 的价格,在 Cex 中其实是市场上交易撮合的成交价。在 Dex 中,由于 AMM 做市商模型通过一组 LP 来构建价格的锚定。所以如果我们想获取到一个 Token 的价格,都是通过对于稳定币 USDT、USDC 或者 BUSD 的币对关系,从而反映现实世界的价格。
</p>
<p>我们知道 LP Token 是不具有流动性池的,如果有那就是套娃了。那么我们应该如何去计算价格呢?其实我们只需要用总增发量和货币价格反推即可。
</p>
\[Cap_x = P_x \times T_x\]
<p></p>
<p>任意一个 Token X 的总市值是 $Cap_x$,是用当前的价格 $P_x$ 和当前总铸造数量 $T_x$相乘可得。对于 LP Token,我们可以用这个公式来反推币价。因为在 LP Token 中,总市值是可以通过两种币的数量和对应价格求得,并且总的制造数量也是已知的。
</p>
<p>所以我们可以如此计算 LP Token 总价格:
</p>
\[P_{LP} = \frac{Cap_{LP}}{T_{LP}} = \frac{r_0 \times price_0 + r_1 \times price_1}{totalSupply}\]
<p></p>
<p>其中,$r_0$和 $r_1$就是 LP Token 合约中两种代币的存量,$price_0$和 $price_1$分别代表 $r_0$和 $r_1$ 对应 Token 的价格。市面上无论 BSC、ETH 还是 Polygon 还是 Heco 链等,其 LP 代币基本都是 fork Uniswap 的,所以 $r_0$和 $r_1$、$price_0$和 $price_1$ 都是能拿到的。
</p>
<p>上面的公式我们其实可以看出,是通过市值反推价格,也没有什么巨大的逻辑问题。当我们需要访问其币价的时候已经可以满足需求。在 Web3.js 前端中,我们就可以照此拿到结果。</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">getLpTokenPrice</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span>
<span class="nx">lpAddress</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span>
<span class="nx">lib</span><span class="p">:</span> <span class="nx">any</span><span class="p">,</span>
<span class="nx">price0</span><span class="p">:</span> <span class="nx">BigNumber</span><span class="p">,</span>
<span class="nx">price1</span><span class="p">:</span> <span class="nx">BigNumber</span>
<span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">lpToken</span> <span class="o">=</span> <span class="nx">getPancakeLp</span><span class="p">(</span><span class="nx">lib</span><span class="p">,</span> <span class="nx">lpAddress</span><span class="p">);</span>
<span class="kd">let</span> <span class="p">[</span><span class="nx">r0</span><span class="p">,</span> <span class="nx">r1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="k">await</span> <span class="nx">lpToken</span><span class="p">.</span><span class="nx">getReserves</span><span class="p">()).</span><span class="nx">map</span><span class="p">((</span><span class="nx">n</span><span class="p">)</span> <span class="o">=></span> <span class="nx">bignumberToBN</span><span class="p">(</span><span class="nx">n</span><span class="p">));</span>
<span class="kd">let</span> <span class="nx">totalSupply</span> <span class="o">=</span> <span class="nx">bignumberToBN</span><span class="p">(</span><span class="k">await</span> <span class="nx">lpToken</span><span class="p">.</span><span class="nx">totalSupply</span><span class="p">());</span>
<span class="k">return</span> <span class="nx">r0</span>
<span class="p">.</span><span class="nx">multipliedBy</span><span class="p">(</span><span class="nx">price0</span><span class="p">)</span>
<span class="p">.</span><span class="nx">plus</span><span class="p">(</span><span class="nx">r1</span><span class="p">.</span><span class="nx">multipliedBy</span><span class="p">(</span><span class="nx">price1</span><span class="p">))</span>
<span class="p">.</span><span class="nx">dividedBy</span><span class="p">(</span><span class="nx">totalSupply</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>至此,我的需求完成。</p>
<h1 id="延时喂价漏洞">延时喂价漏洞</h1>
<p>对于上文公式:
</p>
\[price_{lp}= \frac{r_0 \times price_0 + r_1 \times price_1}{totalSupply}\]
<p>其实乍一看是不存在问题的。但是如果我们所做的需求,不仅仅是一个价格展示,而是一个借贷系统,用这种方式来获取清算系数,就会<strong>存在被闪电贷的风险</strong>。虽然 $price_0$和 $price_1$不能被操控,但是 $r_0$和 $r_1$是可以的。黑客可以通过操作 $r_0$ 和 $r_1$,从而对价格实现控制。</p>
<p>之前漫雾团队写过一篇<a href="https://mp.weixin.qq.com/s/ues5U9Bl971hSqGO1a4SYA">「Warp Finance 被黑详解」</a>的分析,采用了如下攻击流程:</p>
<ol>
<li>通过 dydx 与 Uniswap 闪电贷借出 DAI 和 WETH;</li>
<li>用小部分 DAI 和 WETH 在 Uniswap 的 WETH-DAI LP 中添加流动性,获得 LP Token;</li>
<li>将 LP Token 抵押到 Wrap Finance 中;</li>
<li>用巨量的 WETH 兑换成 DAI,因为 WETH 迅速进入了 WETH-DAI 流动池,总数量大增。但是由于价格使用的是 Uniswap 的预言机,访问的是 Uniswap 的 LP 池,所以 WETH 的价格并未发生变化。从而导致 Wrap Finance 中的 WETH-DAI LP Token 价格迅速提高;</li>
<li>由于 LP Token 单价变高,导致黑客抵押的 LP Token 可以借出更多的稳定币来获息。</li>
</ol>
<p></p>
<p>这里,我们发现漏洞的关键地方,其实是 $price$ 计算对于借贷项目中,使用的是他人的 LP 合约,还未等套利者来平衡价格,从而终究留出了时间差。
</p>
<p>为了解决这个问题,如果我们可以找到一种方式,从而规避价格查询,就能大概率防止上述漏洞。这里,Alpha Finance 给出了另外一个推导公式。</p>
<h1 id="获取公平-lp-价格方法">获取公平 LP 价格方法</h1>
<p>首先我们在一个 LP 池中,我们能保证的是恒定乘积 $K$ 值的大小,我们定义价格比值是 $P$,那么我们会有以下关系式:
</p>
\[\begin{cases}
K=r_0 \times r_1 \\
P = \frac{r_1}{r_0}
\end{cases}\]
<p></p>
<p>因为 $r_0$ 和 $r_1$ 在旧方法中是可以被操纵的,所以我们用 $K$ 和 $P$ 来反解真实的 $r’_{0}$ 和 $r’_1$ :
</p>
\[\begin{cases}
r'_0 = \sqrt{K / P} \\
r'_1 = \sqrt{K \times P}
\end{cases}\]
<p>如此,我们在带入一开始计算 $price_{lp}$的公式中:
</p>
\[\begin{align}
price_{lp} &= \frac{r'_0 \times price_0 + r'_1 \times price_1}{totalSupply} \\
& = \frac{\sqrt{K/P}·price_0 + \sqrt{K·P}·price_1}{totalSupply} \\
& = \frac{\sqrt{K · \frac{price_1}{price_0}·price_0^2} + \sqrt{K·\frac{price_0}{price_1}·price_1^2}}{totalSupply} \\
& = \frac{2\sqrt{K·price_0·price_1}}{totalSupply} \\
& = 2 \frac{\sqrt{r_0·r_1}·\sqrt{price_0 · price_1}}{totalSupply}
\end{align}\]
<p>我们可以发现,最终 Alpha Finance 给我们的推导式中,不会存在独立的 $r_0$ 和 $r_1$ ,取而代之的是它们的恒定乘积 $K$。</p>
<h1 id="攻击可能性分析">攻击可能性分析</h1>
<p>使用以上公式,我们可以真正的避免攻击吗?</p>
<ol>
<li>$price_0$和 $price_1$ 首先是可信源获取的正确价格,无法操纵;</li>
<li>$totalSupply$ 只是改变了质押数量,其变化与质押的两个代币数量有关系;</li>
<li>对于 $r_0$ 和 $r_1$ ,在 Alpha Finance 的博客中提供了两种思路:
<ol>
<li>直接进行代币兑换(类似于上述攻击手段),由于 $r_0 \times r_1$ 是定值 $K$,所以无论如何变化都不会影响计算结果;</li>
<li>直接将 Token 打入 LP Token 合约地址中,由于 $r_0$ 和 $r_1$ 都是在二次根式下,所以付出 $x$ 倍的成果,最终只能获得 $\sqrt{x}$ 倍的收益,这显然是亏本的;</li>
</ol>
</li>
</ol>
<p>综上,在已知情况下,是可以有效避免攻击的。</p>
<h1 id="总结">总结</h1>
<p>通过这次对 LP Token 价格计算的研究,并且对延时喂价漏洞的探求,了解了 LP 抵押使用一般方式计算带来的风险。计算价格的需求,一定要根据所做业务的类型,谨慎选择。</p>
<h1 id="参考链接">参考链接</h1>
<ul>
<li><a href="https://blog.alphafinance.io/fair-lp-token-pricing/">Alpha Finance 关于获取公平 LP 价格的方法</a></li>
<li><a href="https://ethfans.org/posts/a-safe-way-to-get-the-prices-of-lp-tokens">一种安全的 LP 价格的获取方法曾汨</a></li>
</ul>
<hr />
<blockquote>