-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
2029 lines (1737 loc) · 465 KB
/
search.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"?>
<search>
<entry>
<title>cmake调用windeployqt实现自动打包qt的dll文件</title>
<url>/2024/06/27/cmake%E8%B0%83%E7%94%A8windeployqt%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85qt%E7%9A%84dll%E6%96%87%E4%BB%B6/</url>
<content><![CDATA[<p>最近在编写一个Qt项目,发现Qt在windows部署有一个很方便的工具<code>windeployqt.exe</code>,遂研究如何在cmake里调用这个工具,在install时执行,实现全自动化的发布构建。</p>
<p>代码如下:</p>
<div class="code-container" data-rel="Cmake"><figure class="iseeu highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 1. 找到qmake的执行路径(Qt5请更换为你实际的Qt版本)</span></span><br><span class="line"><span class="keyword">get_target_property</span>(qmake_exec_filepath Qt5::qmake IMPORTED_LOCATION)</span><br><span class="line"><span class="comment"># 2. 找到Qt的bin文件夹</span></span><br><span class="line"><span class="keyword">get_filename_component</span>(qt_exec_bin_dir <span class="string">"${qmake_exec_filepath}"</span> DIRECTORY)</span><br><span class="line"><span class="comment"># 3. 找到windeployqt的路径</span></span><br><span class="line"><span class="keyword">find_program</span>(windeployqt_exec_filepath windeployqt HINTS <span class="string">"${qt_exec_bin_dir}"</span>)</span><br><span class="line"><span class="comment"># 4. 在控制台执行命令</span></span><br><span class="line"><span class="keyword">add_custom_command</span>(<span class="keyword">TARGET</span> app POST_BUILD</span><br><span class="line"> <span class="keyword">COMMAND</span> <span class="string">"${windeployqt_exec_filepath}"</span> <span class="string">"--dir"</span> <span class="string">"${CMAKE_INSTALL_PREFIX}/你的二进制文件存放的文件夹"</span> <span class="string">"$<TARGET_FILE:你的target名>"</span></span><br><span class="line"> COMMENT <span class="string">"Running windeployqt..."</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure></div>
<p>简单来说思路就是:通过库文件找到qmake的路径->通过qmake找到bin的路径->通过bin找到windeployqt的路径->就可以在控制台调用windeployqt执行命令了。</p>
<p><code>--dir</code>是指定输出的目录,我使用install进行打包,如果不指定的话会在build目录下执行。</p>
<hr>
<p>如果没有像qt一样这么方便的工具怎么办?另外附上一段另外写的,查找其他库的dll并复制过来的代码:</p>
<div class="code-container" data-rel="Cmake"><figure class="iseeu highlight cmake"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 获取到库的bin路径</span></span><br><span class="line"><span class="keyword">set</span>(VTK_DLL_PATH <span class="variable">${VTK_DIR}</span>/../../../bin)</span><br><span class="line"><span class="comment"># 搜索bin下的dll</span></span><br><span class="line"><span class="keyword">file</span>(GLOB VTK_DLLS <span class="variable">${VTK_DLL_PATH}</span>/*.dll)</span><br><span class="line"><span class="comment"># 对所有dll依次install</span></span><br><span class="line"><span class="keyword">foreach</span>(DLL <span class="variable">${VTK_DLLS}</span>)</span><br><span class="line"> <span class="keyword">install</span>(FILES <span class="variable">${DLL}</span> DESTINATION bin)</span><br><span class="line"><span class="keyword">endforeach</span>()</span><br></pre></td></tr></table></figure></div>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>cmake</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】10. 泛型、trait与生命周期</title>
<url>/2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</url>
<content><![CDATA[<h1 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h1><p>泛型是一种具体类型或者其他属性的抽象替代,通常用来减少代码的重复,接下来将从泛型的几个实际应用场景开始介绍泛型</p>
<h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><h3 id="在函数定义中使用"><a href="#在函数定义中使用" class="headerlink" title="在函数定义中使用"></a>在函数定义中使用</h3><p>现在假设我们要写一个寻找数组最大值的功能,我要怎么实现既能从字符数组里查找最大值,又能从整数数组里查找最大值?定义两个函数分别查找的话难免重复性有点高,这时候就需要使用泛型。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">largest</span><T>(list: &<T>) <span class="punctuation">-></span> T{</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">largest</span> = list[<span class="number">0</span>];</span><br><span class="line"> <span class="keyword">for</span> &item <span class="keyword">in</span> list.<span class="title function_ invoke__">iter</span>() {</span><br><span class="line"> <span class="keyword">if</span> item > largest {</span><br><span class="line"> largest = item;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> largest</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>以上代码定义了一个寻找数组最大值的泛型函数,首先</p>
<ol>
<li>需要声明一个泛型名称<code>T</code>,放置在函数名和参数的圆括号之间,用尖括号括起来,<code>largest(list: &<T>)</code></li>
<li>后续的类型声明,就都可以用<code>T</code>来代替了</li>
</ol>
<p>但以上代码暂时无法提示,rust-analyzer会报错<code>binary operation '>' cannoy be applied to type 'T'</code>,也就是说<code>></code>运算符不能直接用于泛型参数,这个问题会在后续解决,现在重点先放在泛型的应用场景。</p>
<h3 id="在结构体定义中使用"><a href="#在结构体定义中使用" class="headerlink" title="在结构体定义中使用"></a>在结构体定义中使用</h3><div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="meta">#[derive(Debug)]</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Point</span><T> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: T</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">p1</span> = Point{ x: <span class="number">5</span>, y: <span class="number">10</span> };</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">p2</span> = Point{ x: <span class="number">1.0</span>, y: <span class="number">4.0</span> };</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, p1);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, p2);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>结构体中,泛型名称声明在结构体名字后面,<code>Point</code>,这段代码是可以编译通过的。</p>
<p>但注意,当你使用两种类型的变量创建泛型结构体时,就无法编译通过了。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="meta">#[derive(Debug)]</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Point</span><T> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: T</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">p1</span> = Point{ x: <span class="number">5</span>, y: <span class="number">10.0</span> };</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, p1);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>报错:<code>expected integer, found floating-point number</code></p>
<p>这是因为,当你向泛型<code>T</code>传入第一次传入值5的时候,编译器会自动为<code>T</code>赋值为和5相同的类型,即整型。也就是说,<strong>泛型并不是代表能接受所有类型的变量,而是编译器自动帮你识别为第一次接收到的变量类型</strong>。</p>
<p>但如果我们就是可能传入两个类型呢?解决这个问题也简单,我们声明两个泛型,存储两个类型即可</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="meta">#[derive(Debug)]</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Point</span><T, U> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: U</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">p1</span> = Point{ x: <span class="number">5</span>, y: <span class="number">10.0</span> };</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, p1);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>声明多个泛型只需要在尖括号内用逗号隔开即可。</p>
<p>这段代码里,我们声明了两个泛型名称<code>T, U</code>,这时候我们分别为类型为<code>T, U</code>的变量<code>x, y</code>传入5,10.0,对应的,此时<code>T</code>代表整型,<code>U</code>代表浮点型</p>
<h3 id="在方法定义中使用"><a href="#在方法定义中使用" class="headerlink" title="在方法定义中使用"></a>在方法定义中使用</h3><p>有了泛型的结构体,自然也就能有泛型的结构体方法了</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Point</span><T> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: T</span><br><span class="line">}</span><br><span class="line"><span class="keyword">impl</span><T> Point<T> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">x</span>(&<span class="keyword">self</span>, other: &Point<T>) <span class="punctuation">-></span> &T{</span><br><span class="line"> &<span class="keyword">self</span>.x</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>注意,这里我们使用两次,也就是说,我们需要在<code>impl</code>后声明一次泛型名称,再在后续指定泛型。</p>
<p>这是因为,在泛型结构体里我们可以单独的为某个类型实现方法,而不是一定要所有类型都使用同一个方法,例如:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Point</span><T> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: T</span><br><span class="line">}</span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Point</span><<span class="type">f32</span>> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">x</span>(&<span class="keyword">self</span>, other: &Point<<span class="type">f32</span>>) <span class="punctuation">-></span> &<span class="type">f32</span>{</span><br><span class="line"> &<span class="keyword">self</span>.x</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>在这段代码里,我们就相当于单独的为<code>f32</code>类型设定了方法x,只有当<code>T</code>的类型为<code>f32</code>时可以使用这个方法。这种写法可以很经常的被用于处理不同类型的不同情况。</p>
<p>因此,我们需要先声明以下泛型名字,才能确保编译器知道你后面的尖括号到底是泛型还是具体类型。</p>
<p>当然,我们的方法也可以和函数一样,再次声明自己的泛型名称</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">impl</span> <T, U>Point<T, U> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">mixup</span><V, W>(<span class="keyword">self</span>, other:Point<V, W>)<span class="punctuation">-></span>Point<T, W>{</span><br><span class="line"> Point{</span><br><span class="line"> x: <span class="keyword">self</span>.x,</span><br><span class="line"> y: other.y,</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段代码里,我们在<code>mixup</code>函数里新定义了<code>V, W</code>两个泛型,用来接收可能不同类型的其他<code>Point</code>实例,并把两个实例的类型进行混合后,作为新的<code>Point</code>返回</p>
<h3 id="在枚举类型定义中使用"><a href="#在枚举类型定义中使用" class="headerlink" title="在枚举类型定义中使用"></a>在枚举类型定义中使用</h3><p>在之前章节的学习里,我们就知道了<code>Result</code>和<code>Option</code>枚举。其中<code>Option</code>就是典型的单泛型枚举,<code>Result</code>就是典型的包含两个泛型的枚举</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Option</span><T>{</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(T),</span><br><span class="line"> <span class="literal">None</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">Result</span><T, E>{</span><br><span class="line"> <span class="title function_ invoke__">Ok</span>(T),</span><br><span class="line"> <span class="title function_ invoke__">Err</span>(E),</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h2 id="泛型的性能"><a href="#泛型的性能" class="headerlink" title="泛型的性能"></a>泛型的性能</h2><p>可能有人会担心,泛型会不会和Python一样,使得程序有运行时的性能影响?</p>
<p>实际上是不会的,Rust的泛型和c++的auto差不多,会在编译器就静态固定好对应的类型,因此不会产生运行时的损耗,只会在编译期有性能损耗</p>
<h1 id="trait:定义共享行为"><a href="#trait:定义共享行为" class="headerlink" title="trait:定义共享行为"></a>trait:定义共享行为</h1><p><code>trait</code>(特征?)是用来描述一个类型的功能,可以用来和多个类型共享。例如说求和,每个类型的求和都不尽相同的时候,你可以定义一个trait名为sum,然后再分别为不同的类型实现sum</p>
<p><code>trait</code>可能和其他语言的<code>interface</code>功能类似,但也不完全相同</p>
<h2 id="定义trait"><a href="#定义trait" class="headerlink" title="定义trait"></a>定义trait</h2><p>现在假设我们有两个结构体类型,一个是文章(Article),一个是推特(Tweet),我们需要同时为这两个文字内容主体生成摘要,于是我们就可以定义一个Summary的trait,来规定一个适用于所有类型的生成摘要的接口。</p>
<p>接下来我们新建一个库文件<code>lib.rs</code>,然后定义一个trait</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub trait Summary {</span><br><span class="line"> fn summarize(&self) -> String;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>在这段代码里,首先我们定义了一个公有的trait <code>Summary</code>,并规定了trait里有一个函数<code>summarize</code>,在 trait 里,称作<strong>签名</strong>,它传入类型实例自己,返回<code>String</code>,但这里我们省略了函数的具体实现,具体实现交由不同的类型按照自己的规则来进行实现。</p>
<p>当然一个trait里可以有多个签名,这里只定义了一个。</p>
<h2 id="实现trait"><a href="#实现trait" class="headerlink" title="实现trait"></a>实现trait</h2><p>接下来,我们就需要给文章和推特两个结构体类型实现一下用于提取摘要的trait</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub struct Article{</span><br><span class="line"> pub title: String,</span><br><span class="line"> pub location: String,</span><br><span class="line"> pub author: String,</span><br><span class="line"> pub content: String,</span><br><span class="line">}</span><br><span class="line">impl Summary for Article {</span><br><span class="line"> fn summarize(&self) -> String {</span><br><span class="line"> format!("{}, by {} ({})", self.title, self.author, self.location)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">pub struct Tweet{</span><br><span class="line"> pub username: String,</span><br><span class="line"> pub content: String,</span><br><span class="line">}</span><br><span class="line">impl Summary for Tweet {</span><br><span class="line"> fn summarize(&self) -> String {</span><br><span class="line"> format!("{}: {}", self.username, self.content)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段代码,我们定义了两个公有的结构体<code>Article</code>和<code>Tweet</code>,并使用<code>impl Summary for xxx</code>语句,声明为结构体实现<code>Summary</code>这个trait,然后再在<code>impl</code>块里,实现trait内的签名<code>summarize</code>,这时候就可以根据实际情况来返回不同的摘要了。代码中我们是使用<code>format!</code>格式化返回不同的内容。</p>
<p>实现后,我们就可以在具体的实例里使用这个trait了</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use test10::{Tweet, Summary};</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let test = Tweet{</span><br><span class="line"> username: String::from("TwoSix"),</span><br><span class="line"> content: String::from("Hello, world!"),</span><br><span class="line"> };</span><br><span class="line"> println!("A new tweet: {}", test.summarize());</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>注意看我们的use代码<code>use test10::{Tweet, Summary}</code>,这里我们同时引进<code>Tweet</code>类型和<code>Summary</code>这个trait,才能让<code>Tweet</code>实例使用trait对应的成员函数,否则会编译报错,不信你可以试试。(test10是我自己创建的根目录名字)</p>
<p>这一点和其他语言都不一样,有点让人迷惑,实现了trait之后难道不是相当于结构体的成员函数了吗?为什么成员还需要额外引进才能使用?</p>
<p>这是因为trait提供了相当的灵活性,以至于编译器并不好自动检查怎么使用,例如以下场景:我们实现了两个trait,Summary1 和 Summary2,并且这两个trait里都有一个签名叫做 summarize ,然后我们还在Tweet 结构体里同时实现了这两个 trait</p>
<p>是的,Rust允许这种场景的存在,那你说这时候调用 summarize时,应该调用的是Summary1 还是 Summary2?因此,必须显示引入,才能正常使用。</p>
<p>使用就如此,那实现自然也是,如果你想实现别人定义的 trait,那你就需要把别人的 trait 显示引入当前的作用域,才能实现别人的 trait。</p>
<h2 id="默认实现"><a href="#默认实现" class="headerlink" title="默认实现"></a>默认实现</h2><p>前面我们没有在trait内实现<code>summarize</code>签名,交由每个类型自己实现,但实际上我们也可以为其定义一个默认实现。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub trait Summary {</span><br><span class="line"> fn summarize(&self) -> String{</span><br><span class="line"> String::from("(Read more...)")</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们在trait的定义内实现了<code>summarize</code>签名,默认返回一个 (Read more…) 的字符串</p>
<p>然后我们修改一下<code>Article</code>的实现</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub struct Article{</span><br><span class="line"> pub title: String,</span><br><span class="line"> pub location: String,</span><br><span class="line"> pub author: String,</span><br><span class="line"> pub content: String,</span><br><span class="line">}</span><br><span class="line">impl Summary for Article {}</span><br></pre></td></tr></table></figure></div>
<p>我们在实现<code>Summary</code>的时候,直接使用空的花括号,没有实现具体的 trait 签名,然后我们再使用看一下效果。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use test10::{Tweet, Article, Summary};</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let test_tweet: Tweet = Tweet{</span><br><span class="line"> username: String::from("TwoSix"),</span><br><span class="line"> content: String::from("Hello, world!"),</span><br><span class="line"> };</span><br><span class="line"> let test_article = Article{</span><br><span class="line"> title: String::from("Hello"),</span><br><span class="line"> location: String::from("World"),</span><br><span class="line"> author: String::from("TwoSix"),</span><br><span class="line"> content: String::from("This is a test"),</span><br><span class="line"> };</span><br><span class="line"> println!("A new article: {}", test_tweet.summarize());</span><br><span class="line"> println!("A new tweet: {}", test_article.summarize());</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>没有意外,正常的输出 (Read more…),并且 Tweet 的输出正常,不会受到影响。这个概念也很常见,也就是重载。</p>
<h2 id="把trait作为参数"><a href="#把trait作为参数" class="headerlink" title="把trait作为参数"></a>把trait作为参数</h2><p>trait 甚至能作为函数的参数传入,见以下示例</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use test10::{Tweet, Article, Summary};</span><br><span class="line"></span><br><span class="line">fn summarize(item: impl Summary) -> String{</span><br><span class="line"> item.summarize()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let test_tweet: Tweet = Tweet{</span><br><span class="line"> username: String::from("TwoSix"),</span><br><span class="line"> content: String::from("Hello, world!"),</span><br><span class="line"> };</span><br><span class="line"> let test_article = Article{</span><br><span class="line"> title: String::from("Hello"),</span><br><span class="line"> location: String::from("World"),</span><br><span class="line"> author: String::from("TwoSix"),</span><br><span class="line"> content: String::from("This is a test"),</span><br><span class="line"> };</span><br><span class="line"> println!("A new article: {}", summarize(test_tweet));</span><br><span class="line"> println!("A new tweet: {}", summarize(test_article));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>我们定义了一个函数<code>summarize</code>来调用每个实现了 Summary trait 的类型的 summarize 函数。这里的<code>impl Summary</code> 就是指代的所有实现了 Summary trait 的类型。</p>
<h3 id="trait约束"><a href="#trait约束" class="headerlink" title="trait约束"></a>trait约束</h3><p>以上<code>impl Summary</code>实际上只是一个语法糖,它的完整声明形式称作 triat 约束,写作如下</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize<T: Summary>(item: T) -> String{</span><br><span class="line"> item.summarize()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>意思就是声明了一个泛型<code>T</code>,并使用<code>:Summary</code>对泛型T指代的类型进行了约束,使它只能代表实现了 Summary trait 的类型。</p>
<p>实际上在函数较复杂的时候,triat 约束要比之前的语法糖要好用</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize<T: Summary>(item1: T, item2:T, item3: T) -> String{</span><br><span class="line"> item1.summarize();</span><br><span class="line"> item2.summarize();</span><br><span class="line"> item3.summarize()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn summarize(item1: impl Summary, item2:impl Summary, item3: impl Summary) -> String{</span><br><span class="line"> item1.summarize();</span><br><span class="line"> item2.summarize();</span><br><span class="line"> item3.summarize()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>对比一下这两种写法,是不是在复杂的情况,反而 triat 约束更简洁了一些?</p>
<h3 id="多个trait约束"><a href="#多个trait约束" class="headerlink" title="多个trait约束"></a>多个trait约束</h3><p>如果我想让泛型 T 指代实现了多个 triat 的类型怎么办?使用 + 法可以解决这个问题</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize<T: Summary + Display>(item: T) -> String{</span><br><span class="line"> item.summarize()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里就表示,传入的 item 必须是同时实现了 Summary trait 和标准库的 Display trait 的类型。</p>
<h3 id="简化trait约束"><a href="#简化trait约束" class="headerlink" title="简化trait约束"></a>简化trait约束</h3><p>当有多个泛型参数,每个泛型参数有多个 trait 约束的时候,会写成这样</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize<T: Summary + Display, U: Summary + Display>(item1: T, item2: U) -> String{</span><br></pre></td></tr></table></figure></div>
<p>这样就会导致函数定义很长又有很多重复内容,阅读费劲,难以理解,所以 rust 提供了一个 where 从句的方法,提高这种情况下的可读性</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize<T, U>(item1: T, item2: U) -> String</span><br><span class="line"> where T: Summary + Display,</span><br><span class="line"> U: Summary + Display</span><br><span class="line">{</span><br></pre></td></tr></table></figure></div>
<p>我们可以在返回值的类型后面,加上一个 where 从句,把每个泛型的 trait 约束换一行之后再定义,就美观多了。</p>
<h2 id="把trait作为函数返回值类型"><a href="#把trait作为函数返回值类型" class="headerlink" title="把trait作为函数返回值类型"></a>把trait作为函数返回值类型</h2><p>既然能作为函数参数传入,自然也能作为函数返回值进行返回了</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize() -> impl Summary</span><br><span class="line">{</span><br><span class="line"> Tweet{</span><br><span class="line"> username: String::from("TwoSix"),</span><br><span class="line"> content: String::from("Hello, world!"),</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这一段代码则让 summarize 函数固定返回实现了 Summary 的 Tweet 类型,但这种用法似乎感觉没有什么用?没关系,书上说后续讲解闭包等概念的时候,会使用到这种语法。</p>
<p>需要注意的是,Rust 编译器同样会对 impl Trait 进行静态推理保存,碍于 impl Trait 工作方式的限制,所以你只能在返回一个类型的时候,使用 trait 作为返回值类型,如果你既想返回 Tweet 也想返回 Article 是不行的。</p>
<p>如以下的代码就会报错</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn summarize(switch: bool) -> impl Summary</span><br><span class="line">{</span><br><span class="line"> if switch {</span><br><span class="line"> Tweet{</span><br><span class="line"> username: String::from("TwoSix"),</span><br><span class="line"> content: String::from("Hello, world!"),</span><br><span class="line"> }</span><br><span class="line"> } else {</span><br><span class="line"> Article{</span><br><span class="line"> title: String::from("Hello"),</span><br><span class="line"> location: String::from("World"),</span><br><span class="line"> author: String::from("TwoSix"),</span><br><span class="line"> content: String::from("This is a test"),</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h2 id="练手"><a href="#练手" class="headerlink" title="练手"></a>练手</h2><p>还记得我们之前在讲泛型的时候使用的查找最大值的例子吗,之前代码编译不通过,但现在的我们已经有办法修复它了。</p>
<p>先回顾以下这段代码</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn largest<T>(list: &[T]) -> T{</span><br><span class="line"> let mut largest = list[0];</span><br><span class="line"> for &item in list.iter() {</span><br><span class="line"> if item > largest {</span><br><span class="line"> largest = item;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> largest</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>报错的是 <code>></code> 号不能用于泛型<code>T</code>,而 <code>></code> 实际上是一个叫做 <code>PartialOrd</code> 的 trait,所以这段代码报错的核心是,不是每个类型都实现了 <code>PartialOrd</code>,所以我们可以给它加个 trait 约束。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn largest<T: PartialOrd>(list: &[T]) -> T{</span><br><span class="line"> let mut largest = list[0];</span><br><span class="line"> for &item in list.iter() {</span><br><span class="line"> if item > largest {</span><br><span class="line"> largest = item;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> largest</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>因为 <code>PartialOrd</code> 是预导入模块,所以我们可以直接使用,而不需要 use。我们修改完后,这段代码出现了新的报错:<code>cannot move out of here | move occurs because list[_] has type T, which does not implement the Copy trait</code>;意思就是,不是每个类型都实现了 Copy trait,所以我们没有办法把泛型 T 列表内的元素赋值出来,解决也很简单,那就是再加个 Copy 约束即可。</p>
<p>以下这段代码,就能正确的编译并找到不同类型数组的最大值了:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn largest<T: PartialOrd+Copy>(list: &[T]) -> T{</span><br><span class="line"> let mut largest = list[0];</span><br><span class="line"> for &item in list.iter() {</span><br><span class="line"> if item > largest {</span><br><span class="line"> largest = item;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> largest</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn main(){</span><br><span class="line"> let a = vec![1, 2, 3, 4, 5];</span><br><span class="line"> let b = vec!['a', 'b', 'c', 'd', 'e'];</span><br><span class="line"> let largest_a = largest(&a);</span><br><span class="line"> let largest_b = largest(&b);</span><br><span class="line"> println!("The largest number is {}", largest_a);</span><br><span class="line"> println!("The largest char is {}", largest_b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<blockquote>
<p>PS:</p>
<p>这里我有点迷惑,这难道不是对应赋值吗?什么类型不能赋值出来?我查了一下资料,大致得出的结论如下,不知道对不对:</p>
<p>翻译应该沾点锅,看了下原文的意思应该是:不是所有类型都有 Copy trait,这里的 Copy trait 指的是深拷贝,在浅拷贝的变量里,赋值操作应该是 move,而 move 则对应了所有权的转移,对于一个列表内的变量,我们把它所有权转移出来之后,但数组自己是不知道自己的元素所有权已经没有了,这不就出问题了?</p>
<p>所以最重要的原因还是在于这一句代码:<code>let mut largest = list[0];</code> 这里把<code>list[0]</code>的元素所有权移出来了,自然有问题。所以我把代码改成下面这个样子,多加了一些引用的使用,不转移所有权,也就不需要使用Copy约束了。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn largest<T: PartialOrd>(list: &[T]) -> &T{</span><br><span class="line"> let mut largest = &list[0];</span><br><span class="line"> for item in list.iter() {</span><br><span class="line"> if item > largest {</span><br><span class="line"> largest = item;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> largest</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
</blockquote>
<h2 id="通过约束为指定类型实现方法"><a href="#通过约束为指定类型实现方法" class="headerlink" title="通过约束为指定类型实现方法"></a>通过约束为指定类型实现方法</h2><p>也就是当我们定义了一个泛型结构体时,可以让这个结构体内的一些方法只能让指定的类型调用。</p>
<p>例如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::fmt::Display;</span><br><span class="line"></span><br><span class="line">struct Pair<T> {</span><br><span class="line"> x: T,</span><br><span class="line"> y: T,</span><br><span class="line">}</span><br><span class="line">impl<T> Pair<T> {</span><br><span class="line"> fn new(x: T, y: T) -> Self {</span><br><span class="line"> Self {</span><br><span class="line"> x,</span><br><span class="line"> y,</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">impl<T: PartialOrd+Display> Pair<T> {</span><br><span class="line"> fn cmp_display(&self){</span><br><span class="line"> if self.x >= self.y{</span><br><span class="line"> println!("The largest member is x = {}", self.x);</span><br><span class="line"> } else {</span><br><span class="line"> println!("The largest member is y = {}", self.y);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn main(){</span><br><span class="line"> let pair = Pair::new(1, 2);</span><br><span class="line"> pair.cmp_display();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们就规定了 <code>Pair</code> 结构体里只有存放的类型是同时实现了可以比较可以打印两个 trait 的类型,才能调用<code>cmp_display</code>这个方法。(注意 <code>Display</code> trait 不是预导入的,虽然是标准库,也要自己 use)</p>
<p>当然,基于此,自然也可以为结构体的指定类型实现 trait。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">impl<T:Display> ToString for Pair<T> {</span><br><span class="line"> fn to_string(&self) -> String {</span><br><span class="line"> format!("x = {}, y = {}", self.x, self.y)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn main(){</span><br><span class="line"> let pair = Pair::new(1, 2);</span><br><span class="line"> println!("{}", pair.to_string());</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h1 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h1><p>普通的泛型可以用来消除重复代码,也可以向编译器指明程序员希望这些类型拥有什么样的行为,而生命周期就是一种特殊的泛型,用来<strong>确保引用在我们使用的过程中一直有效</strong></p>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在介绍生命周期前,我们需要介绍编译器是怎么检查因为超出作用域而导致的悬垂引用问题的。我们来看看以下这个例子</p>
<p><img
lazyload
src="/images/loading.svg"
data-src="/images/image.png"
alt="img"
></p>
<p>这个例子中,我们定义了一个变量<code>r</code>,在下一个花括号中,我们定义了一个变量<code>x</code>,并把<code>x</code>的所有权借给<code>r</code>,但x的作用域只在这个花括号为止,超出了这个花括号之后所有权就被回收了,<code>r</code>也就成了悬垂引用。</p>
<p>右边的<code>'a,'b</code>就分别代表了r和x的生命周期,我们可以明显的看到,x的生命周期’b明显要比r的生命周期’a短,所以编译器就可以通过检查生命周期的长短来查找你可能的悬垂引用问题,进而提出报错。</p>
<p>那我们为什么要指定生命周期?让我们来写一段代码</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn longest(x: &str, y: &str) -> &str {</span><br><span class="line"> if x.len() > y.len() {</span><br><span class="line"> x</span><br><span class="line"> } else {</span><br><span class="line"> y</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这个函数用于比较得到长度更长的字符串,因为不想只是比较以下就夺取所有权,所以使用引用的方式传入,也使用引用的方式返回。</p>
<p>不出意外,会有错误提示<code>missing lifetime specifier</code>,我们回顾以下编译器检查悬垂引用的方式,需要比较一下引用的生命周期长短,而在以上情况中,我们有一个分支判断,既可能返回x,也可能返回y,编译器不知道会返回x还是返回y,也不知道该比较哪个和哪个引用之间的长短,也就无法进行检查,进而报错,提示我们需要指定生命周期,明确一下引用之间的关系,方便编译器进行比较。</p>
<h2 id="标注生命周期"><a href="#标注生命周期" class="headerlink" title="标注生命周期"></a>标注生命周期</h2><h3 id="基本语法"><a href="#基本语法" class="headerlink" title="基本语法"></a>基本语法</h3><p>标注生命周期的语法很简单,和我们之前举例的命名一样,生命周期的命名以<code>'</code>开头,如<code>'a</code></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">&i32 // 这是一个普通引用</span><br><span class="line">&'a i32 // 这是一个生命周期为'a的引用</span><br><span class="line">&'a mut i32 // 这是一个生命周期为'a的可变引用</span><br></pre></td></tr></table></figure></div>
<h3 id="函数中的生命周期标注"><a href="#函数中的生命周期标注" class="headerlink" title="函数中的生命周期标注"></a>函数中的生命周期标注</h3><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {</span><br><span class="line"> if x.len() > y.len() {</span><br><span class="line"> x</span><br><span class="line"> } else {</span><br><span class="line"> y</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>可见,我们用类似定义泛型的方式,定义了一个生命周期,名为<code>'a</code>,并且给后面的引用都指定了生命周期为<code>'a</code>,也就是告诉编译器,这个函数里传入的引用必然都是相同的生命周期,放心比较!于是编译器就会选择一个引用,推导出实际的生命周期<code>'a</code>,然后和函数外的实际拥有所有权的变量进行比较,然后发现外边的变量生命周期都比<code>'a</code>长,最后得出结果,这段代码可以编译通过。</p>
<p>那x和y是两个不同的变量啊,这里编译器最终得到的生命周期’a究竟是什么地方的生命周期?</p>
<p>答案也很简单,就是x和y重叠部分的生命周期</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>(){</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"abcd"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="string">"xyz"</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">result</span> = <span class="title function_ invoke__">longest</span>(a.<span class="title function_ invoke__">as_str</span>(), b);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"The longest string is {}"</span>, result);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>如以上代码,<code>'a</code>的长度等同于变量<code>b</code>的生命周期(a,b重叠部分,也就是取最短的一个就行),我们定义了返回的引用生命周期也是<code>'a</code>,因此返回的<code>result</code>生命周期也应该是在<code>b</code>的生命周期范围内,这段代码里和<code>b</code>一起被回收,所以没有问题,编译通过。</p>
<p>错误示例如下,result变量的生命周期要长于b的生命周期,则无法通过编译:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>(){</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">result</span>;</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"abcd"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="string">"xyz"</span>;</span><br><span class="line"> result = <span class="title function_ invoke__">longest</span>(a.<span class="title function_ invoke__">as_str</span>(), b);</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"The longest string is {}"</span>, result);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h2 id="深入理解生命周期"><a href="#深入理解生命周期" class="headerlink" title="深入理解生命周期"></a>深入理解生命周期</h2><p>由上面我们可以知道,其实标注生命周期的作用就是为了方便编译器检查。</p>
<p>所以自然而然的,不需要参与检查的变量也就不是必须标注的了,如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn longest<'a>(x: &'a str, y: &str) -> &'a str {</span><br><span class="line"> x</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们规定直接返回x,所以编译器只会顺着第一个参数x进行检查,所以我们只标注了x的生命周期,不标注y也是可以编译通过的,因为y和返回值没有半毛钱关系。</p>
<p>其次,我们标注生命周期,只是向编译器声明了以下传入的引用的生命周期关系,<strong>并没有改变任意一方的生命周期</strong></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn longest<'a>(x: &str, y: &str) -> &'a str {</span><br><span class="line"> let result = String::from("really long string");</span><br><span class="line"> result.as_str()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>例如这一段代码,我们给返回值声明了生命周期<code>'a</code>,但返回值<code>result</code>是在函数内定义的,他也就只能活在这个函数里,并不是说我们给他声明了一个生命周期,他就能活到外面去了。</p>
<h2 id="结构体中的生命周期"><a href="#结构体中的生命周期" class="headerlink" title="结构体中的生命周期"></a>结构体中的生命周期</h2><p>一般情况下结构体都是存储自持有的变量,但实际上也可以存储引用,这时候就需要用到生命周期</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">struct ImportantExcerpt<'a> {</span><br><span class="line"> part: &'a str,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn main(){</span><br><span class="line"> let novel = String::from("Call me Ishmael. Some years ago...");</span><br><span class="line"> let first_sentence = novel.split('.').next().expect("Could not find a '.'");</span><br><span class="line"> let i = ImportantExcerpt { part: first_sentence };</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>定义的语法也是类似即可。</p>
<h2 id="生命周期省略"><a href="#生命周期省略" class="headerlink" title="生命周期省略"></a>生命周期省略</h2><p>可以说,所有引用必然是需要有自己的生命周期的,但其实以前编写的很多函数都没有指明生命周期,也能传入引用,是为什么呢?</p>
<p>其实早期的Rust是所有引用都必须显示标注生命周期的,但随着慢慢的发展,Rust的团队发现有很多情况下,是能够使用编译器推导出返回值的生命周期的,重复的写生命周期有点烦,也就把这部分情况,写成了可省略的生命周期规则。</p>
<p>编译器检查生命周期的规则有以下三条:</p>
<ol>
<li>每一个引用的参数,都有自己的生命周期</li>
<li>当只存在一个输入的生命周期参数时,这个生命周期会被赋予给所有输出的生命周期参数</li>
<li>当拥有多个输入的生命周期参数时,若其中一个是<code>&self</code>或<code>&mut self</code>,<code>self</code>的生命周期会被赋予给所有输出的生命周期参数</li>
<li>若以上三条规则使用完毕,编译器仍然无法推导出所有生命周期,则报错,让用户指定。</li>
</ol>
<p>这些规则帮助我们省略了很多生命周期的编写。为了更好了理解这些规则,我们举一些例子看看。</p>
<p>例如这段代码:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn test(s: &str)->&str{</span><br></pre></td></tr></table></figure></div>
<p>按照规则1,编译器先给所有输入参数赋予自己的生命周期</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn test<'a>(s: &'a str)->&str{</span><br></pre></td></tr></table></figure></div>
<p>由于只有一个输入参数s,满足规则2,编译器把生命周期赋予给所有输出的参数</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn test<'a>(s: &'a str)->&'a str{</span><br></pre></td></tr></table></figure></div>
<p>至此,编译器自己推导出了所有参数的生命周期,也就不用我们写了。</p>
<p>接着,我们再距离说明一下规则3</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">struct ImportantExcerpt<'a> {</span><br><span class="line"> part: &'a str,</span><br><span class="line">}</span><br><span class="line">impl <'a> ImportantExcerpt<'a> {</span><br><span class="line"> fn announce_and_return_part(&self, announcement: &str) -> &str {</span><br><span class="line"> self.part</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p><code>impl <'a> ImportantExcerpt<'a></code>这个声明语句中的生命周期声明不能省略(我也不知道为什么)</p>
<p>在方法<code>announce_and_return_part</code>中,编译器会首先按照规则1,赋予声明周期</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn announce_and_return_part(&'a self, announcement: &'b str) -> &str {</span><br></pre></td></tr></table></figure></div>
<p>因为有两个参数,规则不生效</p>
<p>最后因为参数里有self,所以按照规则3,赋予输出参数self的声明周期</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn announce_and_return_part(&'a self, announcement: &'b str) -> &'a str {</span><br></pre></td></tr></table></figure></div>
<p>这时候,所有生命周期也就推导完毕了,不需要手动指定,也可编译通过。</p>
<blockquote>
<p>如果不返回<code>self.part</code>,返回<code>announcement</code>一样会报错,但此时报错的提示是announcement的生命周期不一定比self长,而不是缺少生命周期声明,可见编译器确实给输出赋予了self的声明周期,并进行检查</p>
</blockquote>
<h2 id="静态生命周期"><a href="#静态生命周期" class="headerlink" title="静态生命周期"></a>静态生命周期</h2><p>一种特殊的生命周期,意味在整个程序的执行期中都可以存活</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">let s:&'static str = "I have a static lifetime.";</span><br></pre></td></tr></table></figure></div>
<p>但使用需要谨慎,1:你需要确保他确实可以在整个程序的生命周期存活;2:你确定它真的需要活这么长时间。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>最后用一段代码,同时使用泛型、trait约束、生命周期</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::fmt::Display;</span><br><span class="line"></span><br><span class="line">fn longest_with_ann<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str</span><br><span class="line"> where T: Display</span><br><span class="line">{</span><br><span class="line"> println!("Announcement! {}", ann);</span><br><span class="line"> if x.len() > y.len(){</span><br><span class="line"> x</span><br><span class="line"> }</span><br><span class="line"> else{</span><br><span class="line"> y</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">fn main(){</span><br><span class="line"> let string1 = String::from("abcd");</span><br><span class="line"> let string2 = "xyz";</span><br><span class="line"> let result = longest_with_ann(string1.as_str(), string2, 123);</span><br><span class="line"> println!("The longest string is {}", result);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>简单解释一下代码</p>
<ol>
<li>定义了一个声明周期<code>'a</code>,用来声明传入的两个字符串x, y的生命周期,以及返回字符串的生命周期</li>
<li>定义了一个泛型<code>T</code></li>
<li>约束了泛型<code>T</code>只能是实现了<code>Display</code>这个trait的类型,方便后续直接使用<code>println!</code>输出</li>
</ol>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>Rust</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】12. 编写一个命令行程序</title>
<url>/2023/05/12/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9112-%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%A8%8B%E5%BA%8F/</url>
<content><![CDATA[<blockquote>
<p>本章节我们将开始学习编写一个小项目——开发一个能够和文件系统交互并处理命令行输入、输出的工具</p>
</blockquote>
<h1 id="基本功能实现"><a href="#基本功能实现" class="headerlink" title="基本功能实现"></a>基本功能实现</h1><p>首先自然是新建一个项目,名为<em>minigrep</em></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo new minigrep</span><br></pre></td></tr></table></figure></div>
<p>实现这一个工具的首要任务自然是接收命令行的参数,例如我们要实现在一个文件里搜索字符串,就得在运行时接收两个参数,一个待搜索的字符串,一个搜索的文件名,例如</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run string filename.txt</span><br></pre></td></tr></table></figure></div>
<p>这种基础的功能自然是已经有一些现成的crate可以使用来实现这些功能的了,不过因为我们的主要目的是学习,所以接下来我们会从零开始实现这些功能。</p>
<h2 id="读取参数值"><a href="#读取参数值" class="headerlink" title="读取参数值"></a>读取参数值</h2><p>这一部分我们需要用到标准库里的<code>std::env::args</code>函数,这个函数会返回一个命令行参数的迭代器,使得程序可以读取所有传递给它的命令行参数值,放到一个动态数组里。</p>
<p>用例:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段代码简单来说就是通过<code>env::args()</code>读取命令行参数的迭代器,再通过<code>collect()</code>函数自动遍历迭代器把参数收集到一个动态数组里并返回。</p>
<p>接下来我们用终端运行代码,传入参数试试</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run hahha arg </span><br><span class="line"> Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 1.81s</span><br><span class="line"> Running `target\debug\minigrep.exe hahha arg`</span><br><span class="line">["target\\debug\\minigrep.exe", "hahha", "arg"]</span><br></pre></td></tr></table></figure></div>
<p>这里的输出的第一个参数是当前运行的程序的二进制文件入口路径,这一功能主要方便程序员打印程序名称/路径,后续才是我们传入的参数字符串,一般情况下我们忽略第一个参数只处理后面两个即可。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line"> </span><br><span class="line"> let query = &args[1];</span><br><span class="line"> let filename = &args[2];</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", query);</span><br><span class="line"> println!("In file {}", filename);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>接下来我们再运行以下这段代码,输出一切正常的话,第一步接收命令行参数的任务我们就完成啦!</p>
<h2 id="读取文件"><a href="#读取文件" class="headerlink" title="读取文件"></a>读取文件</h2><p>既然是要在文件内容里搜索指定字符串,自然要读取文件的内容了。但在这之前我们要先有一个文件,现在我们在项目的<strong>根目录</strong>下,新建一个<code>poem.txt</code>文件,内容就选择书上给的这首诗吧:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">I'm nobody! Who are you?</span><br><span class="line">Are you nobody, too?</span><br><span class="line">Then there's a pair of us - don't tell!</span><br><span class="line">They'd banish us, you know.</span><br><span class="line"></span><br><span class="line">How dreary to be somebody!</span><br><span class="line">How public, like a frog</span><br><span class="line">To tell your name the livelong day</span><br><span class="line">To an admiring bog!</span><br></pre></td></tr></table></figure></div>
<p>接下来,我们就可以在代码里读取这个文件了,也很简单,用之前使用过的<code>fs</code>库即可,代码如下。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::fs;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line"> </span><br><span class="line"> let query = &args[1];</span><br><span class="line"> let filename = &args[2];</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", query);</span><br><span class="line"> println!("In file {}", filename);</span><br><span class="line"></span><br><span class="line"> let contents = fs::read_to_string(filename).expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>接下来,再运行以下这段代码,用命令行传递参数的方法传递我们的文件名poem.txt</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run bog poem.txt </span><br><span class="line"> Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.63s</span><br><span class="line"> Running `target\debug\minigrep.exe bog poem.txt`</span><br><span class="line">["target\\debug\\minigrep.exe", "bog", "poem.txt"]</span><br><span class="line">Searching for bog</span><br><span class="line">In file poem.txt</span><br><span class="line">With text:</span><br><span class="line">I'm nobody! Who are you?</span><br><span class="line">Are you nobody, too?</span><br><span class="line">Then there's a pair of us - don't tell!</span><br><span class="line">They'd banish us, you know.</span><br><span class="line"></span><br><span class="line">How dreary to be somebody!</span><br><span class="line">How public, like a frog</span><br><span class="line">To tell your name the livelong day</span><br><span class="line">To an admiring bog!</span><br></pre></td></tr></table></figure></div>
<p>可见,顺利的输出了我们的文件内容,代码没有问题。</p>
<p>到此为止,我们的基本功能就实现完了。但目前为止,我们现在写的代码都一股脑的堆积在<code>main.rs</code>里,非常简单,但也不具备可扩展性以及可维护性,远远称不上一个”项目”。接下来我们就要用到之前学习的模块化管理的知识,重构一下我们的代码,让它变得更规范化。</p>
<h1 id="模块化与错误处理"><a href="#模块化与错误处理" class="headerlink" title="模块化与错误处理"></a>模块化与错误处理</h1><h2 id="问题与计划"><a href="#问题与计划" class="headerlink" title="问题与计划"></a>问题与计划</h2><p>接下来我们将针对以下四个问题,来制定计划重构我们的代码:</p>
<ol>
<li>功能如果都堆积在main函数里的话,随着我们的功能越来越多,这个文件的代码将会变得越来越复杂,越来越难让人理解,更难去测试,耦合程度很高。所以我们要把函数拆分开来,一个函数负责一个任务</li>
<li>query和filename是存储程序配置的,contents是用于业务逻辑的,当一个作用域的变量越来越多时,我们就很难准确的追踪每个变量的实际用途;所以我们最好应该把用途相同的变量合并到一个结构体里,方便管理也明确用途。</li>
<li>错误处理方面处理的不够详细,读取文件失败可能有很多原因,我们应该准确定位到每个原因,抛出相关的提示,给用户提供更有效的排错信息。</li>
<li>错误管理也应该模块化,例如当用户没有指定运行参数时,底层报错是一个数组越位时,抛出错误”index out of bounds”,这无法帮助用户有效的理解问题本身,我们最好把用于错误处理的代码集中管理,更加方便的处理相关的错误逻辑,也方便为用户打印有意义,便于理解的报错信息。</li>
</ol>
<h2 id="二进制项目的组织结构"><a href="#二进制项目的组织结构" class="headerlink" title="二进制项目的组织结构"></a>二进制项目的组织结构</h2><p>Rust社区对于程序的组织结构有一套自己的原则</p>
<ol>
<li>程序拆分为main.rs和lib.rs,实际的逻辑放在lib.rs,main.rs只作简单调用</li>
<li>如果逻辑相对简单,再考虑留在main.rs,保留在main中的代码量应该小到可以一眼看出这段代码的正确性</li>
</ol>
<p>也就是,main负责运行,lib负责实际逻辑,同时由上一章自动化测试可知,如果我们把逻辑放到lib.rs也更方便我们编写集成测试的代码。</p>
<p>所以,我们这个项目的main函数功能大概如下</p>
<ul>
<li>处理命令行参数</li>
<li>程序配置的变量</li>
<li>调用lib.rs中的run函数</li>
<li>对run函数进行错误处理</li>
</ul>
<p>接下来我们就可以开始按照以上原则重构了</p>
<h2 id="分离基本功能逻辑"><a href="#分离基本功能逻辑" class="headerlink" title="分离基本功能逻辑"></a>分离基本功能逻辑</h2><h3 id="提取解析参数代码"><a href="#提取解析参数代码" class="headerlink" title="提取解析参数代码"></a>提取解析参数代码</h3><p>首先,我们把解析参数做成一个函数,提供main函数调用,方便后面再把这个功能转移到lib</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::fs;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line"> </span><br><span class="line"> let (query, filename) = parse_config(&args);</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", query);</span><br><span class="line"> println!("In file {}", filename);</span><br><span class="line"></span><br><span class="line"> let contents = fs::read_to_string(filename).expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn parse_config(args: &Vec<String>) -> (&str, &str){</span><br><span class="line"> let query = &args[1];</span><br><span class="line"> let filename = &args[2];</span><br><span class="line"> (query, filename)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h3 id="组合参数值"><a href="#组合参数值" class="headerlink" title="组合参数值"></a>组合参数值</h3><p>上面的代码我们选择返回一个元组,又把一个元组拆分成两个变量,这还是有点意义不明,不方便使用,所以我们选择把这两个值放到一个结构体里,再用两个明确的变量名存储这两个参数,使得这个函数的返回值更加明确。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::fs;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line"> </span><br><span class="line"> let config = parse_config(&args);</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", config.query);</span><br><span class="line"> println!("In file {}", config.filename);</span><br><span class="line"></span><br><span class="line"> let contents = fs::read_to_string(config.filename).expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">struct Config{</span><br><span class="line"> query: String,</span><br><span class="line"> filename: String,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fn parse_config(args: &Vec<String>) -> Config{</span><br><span class="line"> let config = Config{</span><br><span class="line"> query: args[1].clone(),</span><br><span class="line"> filename: args[2].clone(),</span><br><span class="line"> };</span><br><span class="line"> config</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里,我们定义了一个<code>Config</code>结构体来存储我们的配置值,同时,秉持着结构体最好还是持有自己变量所有权的理念,这里我们需要把字符串clone一下再存入config变量,这种做法虽然比起直接使用引用多用了一些时间和内存,但也省去写生命周期的麻烦,也增加了代码可读性,毕竟开发效率也是效率,该做的牺牲还得做。</p>
<p>可见,现在main函数里解析参数,使用参数的逻辑已经非常清晰了。</p>
<blockquote>
<p>有人对运行时代价很敏感,就是不喜欢用clone解决所有权问题,这个后面在13章会学习一些其他方法更有效率的处理这种情形。但对于我们现在来说,clone是一点没有问题的,因为我们这里的字符串只有读取程序参数的时候用一次clone,而且还只是两个很短的字符串,这点性能真算不上浪费。</p>
</blockquote>
<h3 id="为Config创建一个构造器"><a href="#为Config创建一个构造器" class="headerlink" title="为Config创建一个构造器"></a>为Config创建一个构造器</h3><p>这时候我们再回头看一下,其实<code>parse_config</code>这个函数和我们的<code>Config</code>结构体也是紧密相关的,因为parse_config的目的就是构造一个Config结构体变量,那我们为什么不在Config结构体里写一个构造函数来实现同样的功能,增强这个函数和Config结构体的相关性,更好理解呢</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::fs;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> println!("{:?}", args);</span><br><span class="line"> </span><br><span class="line"> let config = Config::new(&args);</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", config.query);</span><br><span class="line"> println!("In file {}", config.filename);</span><br><span class="line"></span><br><span class="line"> let contents = fs::read_to_string(config.filename).expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">struct Config{</span><br><span class="line"> query: String,</span><br><span class="line"> filename: String,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">impl Config {</span><br><span class="line"> fn new(args: &Vec<String>) -> Config{</span><br><span class="line"> let query = args[1].clone();</span><br><span class="line"> let filename = args[2].clone();</span><br><span class="line"> Config{query, filename}</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>于是我们的代码变成了上面这样,使用<code>Config::new</code>代替了之前的<code>parse_config</code>,并顺利运行。</p>
<h3 id="分离主体逻辑"><a href="#分离主体逻辑" class="headerlink" title="分离主体逻辑"></a>分离主体逻辑</h3><p>这里我们的项目主体运行逻辑就是打开文件并搜索字符串,目前我们只写了打开文件,就先把这部分拆出来,写到<code>run</code>函数里</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn run(config: Config){</span><br><span class="line"> let contents = fs::read_to_string(config.filename)</span><br><span class="line"> .expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h2 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h2><p>依照计划,现在我们开始修复一下错误处理相关的逻辑</p>
<h3 id="改进错误提示信息"><a href="#改进错误提示信息" class="headerlink" title="改进错误提示信息"></a>改进错误提示信息</h3><p>首先,第一个能想到的错误就是,用户输入的参数不够,例如没有输入参数时,程序会报错“index out of bounds: the len is 1 but the index is 1”,但这个错误会让用户不知所云,所以我们需要完善一下报错提示:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">impl Config {</span><br><span class="line"> fn new(args: &Vec<String>) -> Config{</span><br><span class="line"> if args.len() < 3 {</span><br><span class="line"> panic!("Not enough arguments");</span><br><span class="line"> }</span><br><span class="line"> let query = args[1].clone();</span><br><span class="line"> let filename = args[2].clone();</span><br><span class="line"> Config{query, filename}</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>在参数数量少于3个的时候,我们主动抛出panic,提示参数不够,这时的报错信息就更人性化了。</p>
<h3 id="返回Result而不是直接Panic"><a href="#返回Result而不是直接Panic" class="headerlink" title="返回Result而不是直接Panic"></a>返回Result而不是直接Panic</h3><p>对于函数里的错误,使用Result作为返回结果,让调用函数的用户决定是否panic的方法会更加友好一点。所以我们再修改一下这个new函数,让它在运行成功时返回一个Config变量,在失败时返回报错信息</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">impl Config {</span><br><span class="line"> fn new(args: &Vec<String>) -> Result<Config, &str>{</span><br><span class="line"> if args.len() < 3 {</span><br><span class="line"> return Err("Not enough arguments")</span><br><span class="line"> }</span><br><span class="line"> let query = args[1].clone();</span><br><span class="line"> let filename = args[2].clone();</span><br><span class="line"> Ok(Config{query, filename})</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>因为修改了返回值,所以我们还需要对应修改一下main函数的逻辑</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::fs;</span><br><span class="line">use std::process;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> </span><br><span class="line"> let config = Config::new(&args).unwrap_or_else(|err|{</span><br><span class="line"> println!("Problem parsing args: {}", err);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", config.query);</span><br><span class="line"> println!("In file {}", config.filename);</span><br><span class="line"></span><br><span class="line"> let contents = fs::read_to_string(config.filename).expect("Error in reading file");</span><br><span class="line"> println!("With text:\n{}", contents)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>在以上代码里</p>
<ol>
<li>我们新use了一个process库用来退出程序</li>
<li>我们使用了之前没用过的错误处理函数<code>unwrap_or_else</code>,其大致工作内容也很简单,主要就是捕获错误信息,把报错信息传入闭包<code>err</code>中,作为参数传递进下面的花括号(匿名函数)里,运行花括号的代码。如果没有报错,则自动获取Ok里存储的变量返回。大致就是,和<code>unwrap</code>类似,不过多了一个可以执行额外错误处理代码的功能</li>
</ol>
<p>现在我们再来跑一个错误的命令,来看看报错提示</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run </span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.00s</span><br><span class="line"> Running `target\debug\minigrep.exe`</span><br><span class="line">Problem parsing args: Not enough arguments</span><br><span class="line">error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)</span><br></pre></td></tr></table></figure></div>
<p>完美,提示简短且易懂。</p>
<p>此外,对于run函数我们也作同样的操作;</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::error::Error;</span><br><span class="line"></span><br><span class="line">fn run(config: Config) -> Result<(), Box<dyn Error>>{</span><br><span class="line"> let contents = fs::read_to_string(config.filename)?;</span><br><span class="line"> println!("With text:\n{}", contents);</span><br><span class="line"> Ok(())</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>对于run,我们使用了一种新的处理方式->问号表达式,之前学过,这可以自动为我们在有错误的时候返回错误,减少代码的编写量;然后我们use了一个新的东西<code>std::error::Error</code>,并且使用了一个叫做<code>Box</code>的trait,指定返回的值是一个<code>Error</code>的trait,同时使用了dyn关键词;这里的主要作用就是,因为问号表达式返回的错误类型不是我们容易知道的,所以使用trait的方法可以方便的,动态的帮我们捕获不同的错误类型并进行返回。最后,因为run函数不需要返回什么值,所以只需要Ok里包含空元组就可以了。</p>
<p>同样的,最后再修改一下main函数,处理返回的Result即可</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> </span><br><span class="line"> let config = Config::new(&args).unwrap_or_else(|err|{</span><br><span class="line"> println!("Problem parsing args: {}", err);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", config.query);</span><br><span class="line"> println!("In file {}", config.filename);</span><br><span class="line"></span><br><span class="line"> if let Err(e) = run(config){</span><br><span class="line"> println!("Application error: {}", e);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们也使用了一种新的处理方式,也就是在<a href="https://twosix.page/archives/327">6. 枚举与模式匹配</a>里学到的if let语句,用于匹配枚举类型中的其中一种情况。这里因为我们的run函数必定返回一个空元组,所以我们没必要通过unwrap_or_else提取这个空元组,所以简单对Err这种情况进行处理即可。</p>
<p>当然,语言是很灵活的,我们也可以选择使用match等语句处理我们的报错。</p>
<h2 id="把代码分离成独立的包"><a href="#把代码分离成独立的包" class="headerlink" title="把代码分离成独立的包"></a>把代码分离成独立的包</h2><p>代码基本已经分离完成了,现在我们只需要把分离的代码放到lib.rs里即可。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::fs;</span><br><span class="line">use std::error::Error;</span><br><span class="line"></span><br><span class="line">pub struct Config{</span><br><span class="line"> pub query: String,</span><br><span class="line"> pub filename: String,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">impl Config {</span><br><span class="line"> pub fn new(args: &Vec<String>) -> Result<Config, &str>{</span><br><span class="line"> if args.len() < 3 {</span><br><span class="line"> return Err("Not enough arguments");</span><br><span class="line"> }</span><br><span class="line"> let query = args[1].clone();</span><br><span class="line"> let filename = args[2].clone();</span><br><span class="line"> Ok(Config{query, filename})</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">pub fn run(config: Config) -> Result<(), Box<dyn Error>>{</span><br><span class="line"> let contents = fs::read_to_string(config.filename)?;</span><br><span class="line"> println!("With text:\n{}", contents);</span><br><span class="line"> Ok(())</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>别忘了声明接口为pub,让外部可以调用~以及相关的use语句也要搬过来。</p>
<p>搬迁完毕,接下来只要到main.rs里把我们的库use进来即可,或者不用use,直接使用顶层包进行绝对路径访问也没问题,详见<a href="https://twosix.page/archives/332">7. 包、单元包和模块</a></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::process;</span><br><span class="line"></span><br><span class="line">use minigrep::Config;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> </span><br><span class="line"> let config = Config::new(&args).unwrap_or_else(|err|{</span><br><span class="line"> println!("Problem parsing args: {}", err);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> println!("Searching for {}", config.query);</span><br><span class="line"> println!("In file {}", config.filename);</span><br><span class="line"></span><br><span class="line"> if let Err(e) = minigrep::run(config){</span><br><span class="line"> println!("Application error: {}", e);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里的Config就是通过use导入,run函数因为处于顶层,所以使用我们顶层包名minigrep直接调用也很方便(如果你创建项目的时候名字不是minigrep记得改成你的项目名字)</p>
<p>运行通过!我们的基础功能就基本完成了,后面再把用于测试用的<code>println</code>语句删除就好。</p>
<p>好了,以上代码基本结合了我们学习的大部分内容了,用模块化的思路设计完基本功能逻辑完成后,接下来就该结合一下我们测试的知识了</p>
<h1 id="使用测试驱动开发来编写库功能"><a href="#使用测试驱动开发来编写库功能" class="headerlink" title="使用测试驱动开发来编写库功能"></a>使用测试驱动开发来编写库功能</h1><p>软件主要功能是搜索文件里的字符串,那还差一个搜索的功能。本小节就主要讲的是按照测试驱动开发(TDD)的流程来开发搜索的逻辑。大概是以下步骤</p>
<ol>
<li>编写一个必然失败的测试,运行测试,确保它一定失败</li>
<li>编写或修改刚好足够多的代码,让测试通过</li>
<li>在保证测试通过的前提下,重构代码</li>
<li>回到步骤1,进行开发</li>
</ol>
<p>这只是其中一种开发技术,主要思想是:优先编写测试,再编写能通过测试的代码,有助于开发过程中保持较高的测试覆盖率。</p>
<p>接下来,我们就开始第一步</p>
<h2 id="编写一个会失败的测试"><a href="#编写一个会失败的测试" class="headerlink" title="编写一个会失败的测试"></a>编写一个会失败的测试</h2><p><strong>lib.rs</strong></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">mod test{</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn one_result(){</span><br><span class="line"> let query = "duct";</span><br><span class="line"> let content = "</span><br><span class="line">Rust:</span><br><span class="line">safe, fast, productive.</span><br><span class="line">Pick three.";</span><br><span class="line"> </span><br><span class="line"> assert_eq!(vec!["safe, fast, productive."], search(query, content));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们编写了一个测试模块,给出了测试用的查询字符串,以及内容字符串,再调用了一下实现搜索的功能函数,断言搜索的结果是一个vec数组;</p>
<p>现在我们还没实现search功能,所以后面就来实现一下。但因为我们首先要编写一个必然失败的测试,所以我们先来试试写一个函数,必然返回空数组,这样就和断言的结果不同,导致失败。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{</span><br><span class="line"> vec![]</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里因为我们选择传入字符串的引用,所以需要使用生命周期,这里返回的值主要存的是contents里的部分内容,所以只需要在contents和返回值里标上生命周期即可,query并不需要</p>
<p>接下来我们用<code>cargo test</code>运行一下测试吧</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 1 test</span><br><span class="line">test test::one_result ... FAILED</span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"></span><br><span class="line">---- test::one_result stdout ----</span><br><span class="line">thread 'test::one_result' panicked at 'assertion failed: `(left == right)`</span><br><span class="line"> left: `["safe, fast, productive."]`,</span><br><span class="line"> right: `[]`', src\lib.rs:41:9</span><br><span class="line">note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"> test::one_result</span><br><span class="line"></span><br><span class="line">test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>输出测试失败,左右不相等,一切按我们计划的在走,那接下来就是第二步了,编写或修改代码,让测试通过。</p>
<h2 id="编写可以通过测试的代码"><a href="#编写可以通过测试的代码" class="headerlink" title="编写可以通过测试的代码"></a>编写可以通过测试的代码</h2><p>接下来我们按照正常的思路实验一下search函数的功能:</p>
<ol>
<li>遍历内容的每一行</li>
<li>搜索行里是否包含搜索的字符串</li>
<li>如果包含,就把这行添加到列表里;否则忽略</li>
<li>返回列表</li>
</ol>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn search<'a>(query: & str, contents: &'a str) -> Vec<&'a str>{</span><br><span class="line"> let mut ret = Vec::new();</span><br><span class="line"> for eachline in contents.lines() {</span><br><span class="line"> if eachline.contains(query){</span><br><span class="line"> ret.push(eachline);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ret</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>大概解释一下:</p>
<ol>
<li>new了一个可变的动态列表<code>ret</code>,用来存返回值</li>
<li>用for循环遍历contents的每一行,lines()函数可以把字符串分割成若干行,返回一个迭代器</li>
<li>使用contains函数判断query是否是eachline的子串,如果是,push到ret里</li>
<li>返回ret</li>
</ol>
<p>最后再运行一下测试,不出意外就通过了。</p>
<h2 id="把search集成到run函数内"><a href="#把search集成到run函数内" class="headerlink" title="把search集成到run函数内"></a>把search集成到run函数内</h2><p>上面已经把所有要写的都写完啦,可以集成功能并试着运行了</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn run(config: Config) -> Result<(), Box<dyn Error>>{</span><br><span class="line"> let contents = fs::read_to_string(config.filename)?;</span><br><span class="line"> for (i, line) in search(&config.query, &contents).iter().enumerate(){</span><br><span class="line"> println!("{}: {}\n", i, line);</span><br><span class="line"> }</span><br><span class="line"> Ok(())</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>也简单解释一下:</p>
<ol>
<li>通过for循环遍历所有的搜索结果</li>
<li>通过.iter().enumerate()让迭代器生成的结果附带上当前的循环次数</li>
<li>输出搜索的结果</li>
</ol>
<p>接下来就通过我们熟悉的命令行参数运行程序吧:<code>cargo run frog poem.txt</code></p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run frog poem.txt</span><br><span class="line"> Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.26s</span><br><span class="line"> Running `target\debug\minigrep.exe frog poem.txt`</span><br><span class="line">0: How public, like a frog</span><br></pre></td></tr></table></figure></div>
<p>完美。</p>
<h1 id="处理环境变量"><a href="#处理环境变量" class="headerlink" title="处理环境变量"></a>处理环境变量</h1><p>如果有这么一个参数,例如是否开启不分大小写搜索,那每次用户启动的时候都需要通过命令行来输入一串指令开启就有点太麻烦了。这一部分我们就基于此优化一下我们的代码,主要依赖的就是<strong>环境变量</strong>。</p>
<p>同样基于TDD的计划流程来编写功能。</p>
<h2 id="编写一个必然失败的不区分大小写搜索测试"><a href="#编写一个必然失败的不区分大小写搜索测试" class="headerlink" title="编写一个必然失败的不区分大小写搜索测试"></a>编写一个必然失败的不区分大小写搜索测试</h2><p>首先是写测试,就不解释了,主要是实现了两个测试,一个大小写敏感和一个大小写不敏感。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">#[cfg(test)]</span><br><span class="line">mod test{</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn one_result(){</span><br><span class="line"> let query = "duct";</span><br><span class="line"> let content = "</span><br><span class="line">Rust:</span><br><span class="line">safe, fast, productive.</span><br><span class="line">Pick three.";</span><br><span class="line"> </span><br><span class="line"> assert_eq!(vec!["safe, fast, productive."], search(query, content));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn case_sensitive(){</span><br><span class="line"> let query = "duct";</span><br><span class="line"> let content = "</span><br><span class="line">Rust:</span><br><span class="line">safe, fast, productive.</span><br><span class="line">Pick three.</span><br><span class="line">Duct tape.";</span><br><span class="line"></span><br><span class="line"> assert_eq!(vec!["safe, fast, productive."], search(query, content));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn case_insensitive(){</span><br><span class="line"> let query = "rUst";</span><br><span class="line"> let content = "</span><br><span class="line">Rust:</span><br><span class="line">safe, fast, productive.</span><br><span class="line">Pick three.</span><br><span class="line">Trust me.";</span><br><span class="line"> assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, content));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里省略掉实现search_case_insensitive返回空数组,导致测试失败的部分。可以自己试试,观察是否确保测试失败。</p>
<h2 id="编写能通过测试的代码"><a href="#编写能通过测试的代码" class="headerlink" title="编写能通过测试的代码"></a>编写能通过测试的代码</h2><p>然后是实现大小写不敏感的搜索功能函数,所谓大小写不敏感,其实就是把所有字母转成小写/大小再来作比较。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{</span><br><span class="line"> let mut ret = Vec::new();</span><br><span class="line"> let query = query.to_lowercase();</span><br><span class="line"> for eachline in contents.lines() {</span><br><span class="line"> if eachline.to_lowercase().contains(&query){</span><br><span class="line"> ret.push(eachline);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ret</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>以上代码需要注意的是:因为<code>to_lowercase</code>函数把字符串原来的数据更改了,所以会创建一个新的字符串存储新的数据,所以这里我们的query变成了拥有自己所有权的String变量,而不再是原来的字符串切片,所以后续传入contains时又使用了&来借用。</p>
<p>运行测试通过的话,就没有问题了。</p>
<h2 id="把功能搬迁到run函数内"><a href="#把功能搬迁到run函数内" class="headerlink" title="把功能搬迁到run函数内"></a>把功能搬迁到run函数内</h2><p>因为用户自行决定是否开启大小写敏感的功能,所以要新增一个配置项来存储这个结果,所以我们修改一个Config结构</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub struct Config{</span><br><span class="line"> pub query: String,</span><br><span class="line"> pub filename: String,</span><br><span class="line"> pub is_sensitive: bool,</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>接下来,再结合is_sensitive把search_case_insensitive的功能搬到run函数里就差不多了</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn run(config: Config) -> Result<(), Box<dyn Error>>{</span><br><span class="line"> let contents = fs::read_to_string(config.filename)?;</span><br><span class="line"> let result = if config.is_sensitive{</span><br><span class="line"> search(&config.query, &contents)</span><br><span class="line"> }</span><br><span class="line"> else{</span><br><span class="line"> search_case_insensitive(&config.query, &contents)</span><br><span class="line"> };</span><br><span class="line"> for (i, line) in result.iter().enumerate(){</span><br><span class="line"> println!("{}: {}\n", i, line);</span><br><span class="line"> }</span><br><span class="line"> Ok(())</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>不过这段代码还不能编译通过,因为我们还没修改Config的new函数。这里因为我们不希望通过之前的命令行参数解析的方法来得到is_sensitive的值,所以我们要使用新的方法——读取环境变量。</p>
<p>环境变量可以让一个参数的值再整个会话内都有效,就不需要每次运行都输入一遍了,很方便。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">impl Config {</span><br><span class="line"> pub fn new(args: &Vec<String>) -> Result<Config, &str>{</span><br><span class="line"> if args.len() < 3 {</span><br><span class="line"> return Err("Not enough arguments");</span><br><span class="line"> }</span><br><span class="line"> let query = args[1].clone();</span><br><span class="line"> let filename = args[2].clone();</span><br><span class="line"> let is_sensitive = !(env::var("SENSITIVE").is_err());</span><br><span class="line"> Ok(Config{query, filename, is_sensitive})</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<ol>
<li>我们新use了env,用来读取环境变量</li>
<li>添加了新的一行,通过env的var函数,读取名为“SENSITIVE”的环境变量</li>
<li>用Result的is_err()来处理报错,因为我们这里的is_sensitive就是一个bool值,is_err也是返回一个布尔值,正确读取环境变量的时候返回False,读取失败的时候返回True,用<code>!</code>取反之后整好对上我们想实现的逻辑,也就是设置了环境变量的时候,则为True大小写敏感,否则False大小写不敏感。因为我们不需要关注环境变量的值,只想知道有没有。</li>
</ol>
<p>写完之后,我们就来运行一下看看吧</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run to poem.txt</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.00s</span><br><span class="line"> Running `target\debug\minigrep.exe to poem.txt`</span><br><span class="line">0: Are you nobody, too?</span><br><span class="line"></span><br><span class="line">1: How dreary to be somebody!</span><br><span class="line"></span><br><span class="line">2: To tell your name the livelong day</span><br><span class="line"></span><br><span class="line">3: To an admiring bog!</span><br></pre></td></tr></table></figure></div>
<p>我们在poem.txt里搜索to,结果没有问题,大小写不敏感,搜索出了to和To的结果</p>
<p>接下来,我们设置一下环境变量,这对于每个系统方法都不一样,这里只说一下windows的方法,在终端里输入以下语句</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">$env:SENSITIVE=1</span><br></pre></td></tr></table></figure></div>
<p>然后再运行代码</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run to poem.txt </span><br><span class="line"> Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.41s</span><br><span class="line"> Running `target\debug\minigrep.exe to poem.txt`</span><br><span class="line">0: Are you nobody, too?</span><br><span class="line"></span><br><span class="line">1: How dreary to be somebody!</span><br></pre></td></tr></table></figure></div>
<p>也没有问题,只搜索出小写的to结果</p>
<p>现在,这一部分也完成了。</p>
<h1 id="将错误提示信息打印到标准错误而不是标准输出"><a href="#将错误提示信息打印到标准错误而不是标准输出" class="headerlink" title="将错误提示信息打印到标准错误而不是标准输出"></a>将错误提示信息打印到标准错误而不是标准输出</h1><p>println是输出到标准输出流,也就是打印到屏幕上,另一种输出流是标准错误流,会将正常的输出保存到文件里,错误的输出依旧打印到屏幕上。</p>
<p>我觉得这部分不重要,就简单放代码了。</p>
<p>把<code>println</code>改成<code>eprintln</code>即可输出到标准错误</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use std::env;</span><br><span class="line">use std::process;</span><br><span class="line"></span><br><span class="line">use minigrep::Config;</span><br><span class="line"></span><br><span class="line">fn main() {</span><br><span class="line"> let args: Vec<String> = env::args().collect();</span><br><span class="line"> </span><br><span class="line"> let config = Config::new(&args).unwrap_or_else(|err|{</span><br><span class="line"> eprintln!("Problem parsing arguments: {}", err);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> if let Err(e) = minigrep::run(config){</span><br><span class="line"> eprintln!("Application error: {}", e);</span><br><span class="line"> process::exit(1);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>运行错误指令,并指定输出文件,查看输出</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run > output.txt</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.00s</span><br><span class="line"> Running `target\debug\minigrep.exe`</span><br><span class="line">Problem parsing arguments: Not enough arguments</span><br><span class="line">error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)</span><br></pre></td></tr></table></figure></div>
<p>可以看见错误信息打印到了屏幕上,而output.txt没有内容</p>
<p>运行正确指令,查看输出</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo run to poem.txt > output.txt</span><br><span class="line"> Finished dev [unoptimized + debuginfo] target(s) in 0.00s</span><br><span class="line"> Running `target\debug\minigrep.exe to poem.txt</span><br></pre></td></tr></table></figure></div>
<p>可以发现终端没有任何内容输出,输出搜索结果都在output.txt里</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这章主要还是复习以前的内容吧,大概就是</p>
<ol>
<li>结合基本的概念,如所有权,生命周期等编写基本逻辑</li>
<li>如何使用函数,结构体,模块等功能结构化管理程序逻辑</li>
<li>如何捕获并正确的处理程序错误</li>
<li>如何编写测试模块</li>
<li>一些乱七八糟的命令行处理等</li>
</ol>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>Rust</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】11. 编写自动化测试</title>
<url>/2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/</url>
<content><![CDATA[<blockquote>
<p>这一章讲的就是怎么在Rust编写单元测试代码,这一部分的思想不仅适用于Rust,在绝大多数语言都是有用武之地的</p>
</blockquote>
<h1 id="如何编写测试"><a href="#如何编写测试" class="headerlink" title="如何编写测试"></a>如何编写测试</h1><h2 id="测试代码的构成"><a href="#测试代码的构成" class="headerlink" title="测试代码的构成"></a>测试代码的构成</h2><h3 id="构成"><a href="#构成" class="headerlink" title="构成"></a>构成</h3><p>通用测试代码通常包括三个部分</p>
<ol>
<li>准备所需的数据或者前置状态</li>
<li>调用需要测试的代码</li>
<li>使用断言,判断运行结果是否和我们期望的一致</li>
</ol>
<p>在Rust中,有专门用于编写测试代码的相关功能,包含test属性,测试宏,should_panic属性等等</p>
<p>在最简单的情况下,Rust中的测试就是一个标注有<code>test</code>属性的函数。只需要将<code>#[test]</code>添加到函数的关键字<code>fn</code>上,就能使用<code>cargo test</code>命令来运行测试。</p>
<p>测试命令会构建一个可执行文件,调用所有标注了test的函数,生成相关报告。</p>
<blockquote>
<p>PS:</p>
<p><strong>属性</strong>是一种修饰代码的一种元数据,例如之前为了输出结构体时,加入的<code>#[derive(Debug)]</code>就是一个属性,声明属性后,会为下面的代码自动生成一些实现,如<code>#[derive(Debug)]</code>修饰结构体时,就会为结构体生成Debug trait的实现</p>
</blockquote>
<h3 id="初次尝试"><a href="#初次尝试" class="headerlink" title="初次尝试"></a>初次尝试</h3><p>接下来我们就试试怎么测试</p>
<p>首先新建一个名为adder的项目<code>cargo new adder --lib</code>(–lib指生成lib.rs文件)</p>
<p>可能是版本比较新,lib.rs里直接生成有了以下代码</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn add(left: usize, right: usize) -> usize {</span><br><span class="line"> left + right</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn it_works() {</span><br><span class="line"> let result = add(2, 2);</span><br><span class="line"> assert_eq!(result, 4);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>我们先忽略一些没讲过的关键词,这段代码里我们定义了一个<code>it_works</code>函数,并标注为测试函数,然后使用断言判断<code>add</code>函数的结果是否正确的等于4。在了解了大概功能之后,我们直接运行测试看看</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test</span><br><span class="line"> Compiling adder v0.1.0 (E:\Code\rust\adder)</span><br><span class="line"> Finished test [unoptimized + debuginfo] target(s) in 0.29s</span><br><span class="line"> Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)</span><br><span class="line"></span><br><span class="line">running 1 test</span><br><span class="line">test tests::it_works ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line"> Doc-tests adder</span><br><span class="line"></span><br><span class="line">running 0 tests</span><br><span class="line"></span><br><span class="line">test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>对应的测试结果如上</p>
<ul>
<li>passed: 测试通过的函数数量,我们这里只有一个it_works函数,且测试通过,所以为1</li>
<li>failed: 测试失败的函数数量</li>
<li>ignored: 被标记为忽略的测试函数,后面会提</li>
<li>measured: Rust还提供了衡量函数性能的benchmark方法,不过编写书的时候似乎这部分还不完善,所以不会有讲解,想了解需要自行学习</li>
<li>filtered out:被过滤掉的测试函数</li>
<li>Doc-tests:文档测试,这是个很好用的特性,可以防止你在修改了函数之后,忘记修改自己的文档,保证文档能和实际代码同步。</li>
</ul>
<p>测试时,每一个测试函数都是运行在独立的线程里的,所以发生panic时并不会影响其他的测试,我们可以写一个错误的函数看看</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn add(left: usize, right: usize) -> usize {</span><br><span class="line"> left + right</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn error() {</span><br><span class="line"> let result = add(3, 2);</span><br><span class="line"> assert_eq!(result, 4);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn it_works() {</span><br><span class="line"> let result = add(2, 2);</span><br><span class="line"> assert_eq!(result, 4);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>输出结果:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test</span><br><span class="line"> Compiling adder v0.1.0 (E:\Code\rust\adder)</span><br><span class="line"> Finished test [unoptimized + debuginfo] target(s) in 0.24s</span><br><span class="line"> Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)</span><br><span class="line"></span><br><span class="line">running 2 tests</span><br><span class="line">test tests::it_works ... ok</span><br><span class="line">test tests::error ... FAILED</span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"></span><br><span class="line">---- tests::error stdout ----</span><br><span class="line">thread 'tests::error' panicked at 'assertion failed: `(left == right)`</span><br><span class="line"> left: `5`,</span><br><span class="line"> right: `4`', src\lib.rs:18:9</span><br><span class="line">note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"> tests::error</span><br><span class="line"></span><br><span class="line">test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line">error: test failed, to rerun pass `--lib`</span><br></pre></td></tr></table></figure></div>
<p>可以看见,error的panic并不影响it_works的测试通过。</p>
<h2 id="assert-宏"><a href="#assert-宏" class="headerlink" title="assert!宏"></a>assert!宏</h2><h3 id="assert"><a href="#assert" class="headerlink" title="assert!"></a>assert!</h3><p><code>assert!</code>宏主要的功能是用来确保某个值为<code>true</code>,所以常被用于测试中。如<code>a>b</code>等场景,返回的是一个bool值,就完美的符合assert!的使用场景,可以使用assert!进行测试,例如</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn cmp(a: i32, b: i32) -> bool {</span><br><span class="line"> if a>b{</span><br><span class="line"> true</span><br><span class="line"> }</span><br><span class="line"> else{</span><br><span class="line"> false</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn cmp_test(){</span><br><span class="line"> let result = cmp(3, 2);</span><br><span class="line"> assert!(result);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h3 id="assert-eq-和assert-ne"><a href="#assert-eq-和assert-ne" class="headerlink" title="assert_eq!和assert_ne!"></a>assert_eq!和assert_ne!</h3><p>那如果返回值不是bool值呢?前面也出现过了,我们可以使用<code>assert_eq!</code>或者<code>assert_ne!</code>来断言两个值是否相等。</p>
<p>eq则对应的只有相等才能通过断言,ne则对应的只有不相等才能通过断言,用例见上面的add测试即可。</p>
<p><strong>但是注意</strong>,assert_eq!和assert_ne!使用了<code>==</code>和<code>!=</code>来实现是否相等的判断,也就意味着,传入这两个宏的参数是必须实现了<code>PartialEq</code>这个trait的。同时,我们可见错误的输出中会打印出详细的不相等原因,也就是说它还同时需要实现了<code>Debug</code>宏帮助打印输出。一般绝大部分参数都是满足要求的,自定义的结构体时需要注意。</p>
<blockquote>
<p>之前提到过属性这个概念,会为你自动实现一些功能,实际上PartialEq和Debug作为可派生的宏,也内置了属性的实现,你只需要在自己定义的结构体上加上<code>#[derive(PartialEq, Debug)]</code>,就能自动帮你实现这两个宏</p>
</blockquote>
<h3 id="自定义错误提示代码"><a href="#自定义错误提示代码" class="headerlink" title="自定义错误提示代码"></a>自定义错误提示代码</h3><p>上面我们说到assert_eq是会有详细输出的,告诉你怎么不相等了,帮助你排除bug,但普通的assert!只判断布尔值,所以没办法有详细的输出,这时候我们可以定制一个输出,使得错误提示更人性化一点。</p>
<p>如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn cmp(a: i32, b: i32) -> bool {</span><br><span class="line"> if a>b{</span><br><span class="line"> true</span><br><span class="line"> }</span><br><span class="line"> else{</span><br><span class="line"> false</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn cmp_test(){</span><br><span class="line"> let a = 2;</span><br><span class="line"> let b = 3;</span><br><span class="line"> let result = cmp(a, b);</span><br><span class="line"> assert!(result, "{} is not bigger than {}", a, b);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>和一般不一样,我们不需要用什么格式化字符串的方法先格式化一个字符串,再传入这个字符串,断言支持直接使用格式化的语法。这段代码的输出如下</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 1 test</span><br><span class="line">thread 'tests::cmp_test' panicked at '2 is not bigger than 3', src\lib.rs:23:9</span><br><span class="line">stack backtrace:</span><br></pre></td></tr></table></figure></div>
<p>可见报错提示相对于单纯的panicked at更人性化了一些。</p>
<p>当然,自定义输出也支持在assert_eq和assert_ne里使用</p>
<h2 id="should-panic"><a href="#should-panic" class="headerlink" title="should_panic"></a>should_panic</h2><p>should_panic也是一个属性,用来测试代码是否能正确的在出错时发生panic。用例如下</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn positive_num(a: i32) -> i32 {</span><br><span class="line"> if a > 0 {</span><br><span class="line"> a</span><br><span class="line"> } else {</span><br><span class="line"> panic!("{} is not positive", a)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> #[should_panic]</span><br><span class="line"> fn pos_test(){</span><br><span class="line"> let a = -1;</span><br><span class="line"> positive_num(a);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段代码用来检查一个数是否是正数,在不是时抛出panic,接下来我们使用<code>#[should_panic]</code>来检查程序是否正确的panic,这段代码运行测试通过没问题。</p>
<p>接下来我们修改一下a的值,让程序不抛出panic,看看会发生什么</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 1 test</span><br><span class="line">test tests::pos_test - should panic ... FAILED</span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"></span><br><span class="line">---- tests::pos_test stdout ----</span><br><span class="line">note: test did not panic as expected</span><br><span class="line"></span><br><span class="line">failures:</span><br><span class="line"> tests::pos_test</span><br><span class="line"></span><br><span class="line">test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line">error: test failed, to rerun pass `-p adder --lib`</span><br></pre></td></tr></table></figure></div>
<p>测试失败,告诉你pos_test没有按照预期发生panic。这个特性可以用来检查你的代码是否能正确的处理报错,发生panic以阻止程序进一步运行,产生不可预估的后果。</p>
<p>但是,单纯这么使用感觉有点含糊不清,因为程序发生panic的原因可能不是我们所预期的,假如其他一些我们不知道的原因抛出了panic,也会导致测试通过。所以我们可以添加一个可选参数<code>expected</code>,用来检查panic发生报错的输出信息里是否包含指定的文字。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">#[test]</span><br><span class="line">#[should_panic(expected = "positive")]</span><br><span class="line">fn pos_test(){</span><br><span class="line"> let a = -1;</span><br><span class="line"> positive_num(a);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这时候,should_panic就会检查发生的panic输出的报错信息是否包含”positive”这个字符串,如果是,才会测试通过,输出如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 1 test</span><br><span class="line">thread 'tests::pos_test' panicked at '-1 is not positive', src\lib.rs:9:9</span><br><span class="line">stack backtrace:</span><br></pre></td></tr></table></figure></div>
<p>可见,输出中也包含了报错的信息,更人性化了。</p>
<h2 id="使用Result编写测试"><a href="#使用Result编写测试" class="headerlink" title="使用Result编写测试"></a>使用Result编写测试</h2><p>之前学习Result枚举的时候我们就知道了这东西是用来处理报错的,自然也就可以用来处理测试。使用时也很简单,我们只需要声明测试函数的返回值是Result,test命令就会自动根据Result的枚举结果来判断是否测试成功了。如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn it_works() -> Result<(), String>{</span><br><span class="line"> if 2+2 == 4 {</span><br><span class="line"> Ok(())</span><br><span class="line"> } else {</span><br><span class="line"> Err(String::from("two plus two does not equal four"))</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>使用Result编写测试函数的主要优势是可以使用问号表达式进行错误捕获,更方便我们去编写一些复杂的测试函数,可以让函数在任一时刻有错误被捕获到时,就返回报错。</p>
<p>问号表达式的使用可见<a href="https://twosix.page/archives/345">【Rust 学习记录】9. 错误处理</a>的?运算符部分,这里就不再写代码举例了(主要书上没例子,我也懒的写)</p>
<h1 id="控制测试的运行方式"><a href="#控制测试的运行方式" class="headerlink" title="控制测试的运行方式"></a>控制测试的运行方式</h1><p>这一部分主要是对cargo test命令的讲解,具体的运行方式,参数的使用等。</p>
<p>cargo test的参数统一需要写在<code>--</code>后面,也就是说你想要使用<code>--help</code>显示参数文档时,需要使用以下命令</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test -- --help</span><br></pre></td></tr></table></figure></div>
<h2 id="并行或串行的执行代码"><a href="#并行或串行的执行代码" class="headerlink" title="并行或串行的执行代码"></a>并行或串行的执行代码</h2><p>默认情况下,测试是多线程并发执行的,这可以使测试更快速的完成,且相互之间不会影响结果。但如果测试间有相互依赖关系,则需要串行执行。例如两个测试用例同时在操作一个文件,一个测试在写内容,一个测试在读内容时,则容易导致测试结果不合预期。</p>
<p>我们可以使用<code>--test-threads=1</code>来指定测试的线程数为1,即可实现串行执行,当然,你想执行的更快也可以指定更多的线程</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test -- --test-threads=1</span><br></pre></td></tr></table></figure></div>
<h2 id="显示函数的输出"><a href="#显示函数的输出" class="headerlink" title="显示函数的输出"></a>显示函数的输出</h2><p>默认情况下,test命令会捕获所有测试成功时的输出,也就是说,对于测试成功的函数,即使你使用了println!打印输出,你也无法在控制台看见你的输出,因为它被test命令捕获吞掉了。</p>
<p>如果你想要在控制台显示你的输出,只需要用<code>--nocapture</code>设置不捕获输出即可</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test -- --nocapture</span><br></pre></td></tr></table></figure></div>
<h2 id="只运行部分特定名称的测试"><a href="#只运行部分特定名称的测试" class="headerlink" title="只运行部分特定名称的测试"></a>只运行部分特定名称的测试</h2><p>如果测试的函数越写越多,执行所有的测试可能很花时间,通常我们编写了一个新的功能并想进行测试的时候,我们只需要测试这一个功能就足够了,因此可以向test命令指定函数名称来进行测试。</p>
<p>如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn add_two(a: i32, b: i32) -> i32 {</span><br><span class="line"> a + b</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn add_test_1() {</span><br><span class="line"> assert_eq!(add_two(1, 2), 3);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn add_test_2() {</span><br><span class="line"> assert_eq!(add_two(2, 2), 4);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn one_hundred() {</span><br><span class="line"> assert_eq!(100, 100);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>对于这段代码,我们只想测试<code>one_hundred</code>这个函数,只需要对test命令指定运行one_hundred即可</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test one_hundred</span><br></pre></td></tr></table></figure></div>
<p>输出如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 1 test</span><br><span class="line">test tests::one_hundred ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>这里显示<code>2 filtered out</code>,代表有两个测试用例被我们过滤掉了。</p>
<p>当然,这个方法也并不是只能运行一个测试函数,也可以通过部分匹配的方法执行多个名称里包含相同字符串的测试函数,例如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test add </span><br><span class="line"></span><br><span class="line">running 2 tests</span><br><span class="line">test tests::add_test_1 ... ok</span><br><span class="line">test tests::add_test_2 ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>我们使用<code>cargo test add</code>命令,则可以测试所有名字里带<code>add</code>的测试函数,忽略掉了one_hundred函数。</p>
<p>不过需要注意的是,这种方法一次只能使用一个参数进行匹配并测试,如果你想同时用多个规则匹配多类的测试函数,就需要用其他方法了。</p>
<h2 id="通过显示指定来忽略某些测试"><a href="#通过显示指定来忽略某些测试" class="headerlink" title="通过显示指定来忽略某些测试"></a>通过显示指定来忽略某些测试</h2><h3 id="忽略部分测试函数"><a href="#忽略部分测试函数" class="headerlink" title="忽略部分测试函数"></a>忽略部分测试函数</h3><p>当有部分测试函数执行特别耗时时,我们不想每次测试都执行这个函数,我们就可以通过<code>#[ignore]</code>属性来显示指定忽略这个测试函数。如:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn add_two(a: i32, b: i32) -> i32 {</span><br><span class="line"> a + b</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn add_test_1() {</span><br><span class="line"> assert_eq!(add_two(1, 2), 3);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> #[ignore]</span><br><span class="line"> fn add_test_2() {</span><br><span class="line"> assert_eq!(add_two(2, 2), 4);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn one_hundred() {</span><br><span class="line"> assert_eq!(100, 100);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>此时我们直接执行<code>cargo test</code>,输出如下</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">running 3 tests</span><br><span class="line">test tests::add_test_2 ... ignored</span><br><span class="line">test tests::add_test_1 ... ok</span><br><span class="line">test tests::one_hundred ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line"> Doc-tests adder</span><br></pre></td></tr></table></figure></div>
<p>可见<code>add_test_2</code>函数被忽略不执行了,提示1 ignored。</p>
<h3 id="单独执行被忽略的测试函数"><a href="#单独执行被忽略的测试函数" class="headerlink" title="单独执行被忽略的测试函数"></a>单独执行被忽略的测试函数</h3><p>如果我们想单独执行这些被忽略的函数,则可以使用<code>--ignored</code>命令</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test -- --ignored</span><br><span class="line"></span><br><span class="line">running 1 test</span><br><span class="line">test tests::add_test_2 ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>可见,只有<code>add_test_2</code>被执行了。</p>
<h1 id="测试的组织结构"><a href="#测试的组织结构" class="headerlink" title="测试的组织结构"></a>测试的组织结构</h1><p>测试通常分为两类,单元测试和集成测试。单元测试小而专注,集中于测试一个私有接口或模块;集成测试则独立于代码库之外,正常的从外部调用公共接口,一次测试可能使用多个模块</p>
<h2 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h2><p>单元测试的目的在于将一小段代码单独隔离开来,快速确定代码结果是否符合预期。一般来说,单元测试的代码<strong>和需要测试的代码存放在同一文件中</strong>。同时也约定俗成的在每个源代码文件里都会新建一个tests模块来存放测试函数,并使用cfg(test)来标注。</p>
<h3 id="测试模块和-cfg-test"><a href="#测试模块和-cfg-test" class="headerlink" title="测试模块和#[cfg(test)]"></a>测试模块和#[cfg(test)]</h3><p><code>#[cfg(test)]</code>旨在让Rust只在执行Cargo test命令的时候编译和运行这段代码,而在cargo build的时候剔除掉它们,只用于测试,节省编译时间与空间,使得我们可以更方便的把测试代码和源代码放在同一个文件里。(集成测试时一般不需要标注,因为集成测试一般是独立的一个文件)</p>
<p>我们之前编写的测试模块就使用了这个属性</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn it_works() {</span><br><span class="line"> let result = add(2, 2);</span><br><span class="line"> assert_eq!(result, 4);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<h3 id="测试私有函数"><a href="#测试私有函数" class="headerlink" title="测试私有函数"></a>测试私有函数</h3><p>是不是应该测试私有函数一直有争议,不管你觉得要不要,但Rust提供了方法供你方便的测试。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">fn add(left: usize, right: usize) -> usize {</span><br><span class="line"> left + right</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#[cfg(test)]</span><br><span class="line">mod tests {</span><br><span class="line"> use super::*;</span><br><span class="line"></span><br><span class="line"> #[test]</span><br><span class="line"> fn it_works() {</span><br><span class="line"> let result = add(2, 2);</span><br><span class="line"> assert_eq!(result, 4);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>以上代码中的add没有标注pub关键字,也就是私有的,但因为Rust的测试代码本身也属于Rust代码,所以可以通过use的方法把私有的函数引入当前作用域来测试,也就是对应的代码里的<code>use super::*;</code></p>
<h2 id="集成测试"><a href="#集成测试" class="headerlink" title="集成测试"></a>集成测试</h2><p>集成测试通常是新建一个tests目录,只调用对外公开的那部分接口。</p>
<h3 id="tests目录"><a href="#tests目录" class="headerlink" title="tests目录"></a>tests目录</h3><p>tests目录需要和src文件夹并列,Cargo会自动在这个目录下面寻找测试文件。</p>
<p>现在,我们新建一个<code>tests/integration_test.rs</code>文件,保留之前的lib.rs代码(add函数如果改了私有记得改回公有),并编写测试代码。</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">use adder;</span><br><span class="line"></span><br><span class="line">#[test]</span><br><span class="line">fn add_two() {</span><br><span class="line"> assert_eq!(adder::add(2, 2), 4);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>集成测试就不需要<code>#[cfg(test)]</code>了,Rust有单独为tests目录做处理,不会build这个目录下的文件。</p>
<p>接下来,我们再执行以下Cargo test,看看输出</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">cargo test</span><br><span class="line"> Compiling adder v0.1.0 (E:\Code\rust\adder)</span><br><span class="line"> Finished test [unoptimized + debuginfo] target(s) in 0.32s</span><br><span class="line"> Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)</span><br><span class="line"></span><br><span class="line">running 1 test</span><br><span class="line">test tests::it_works ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line"> Running tests\integration_test.rs (target\debug\deps\integration_test-57f19c149db40d76.exe)</span><br><span class="line"></span><br><span class="line">running 1 test</span><br><span class="line">test add_two ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br><span class="line"></span><br><span class="line"> Doc-tests adder</span><br><span class="line"></span><br><span class="line">running 0 tests</span><br><span class="line"></span><br><span class="line">test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>我们可以看到输出里</p>
<ol>
<li>先输出了单元测试的结果,<code>test tests::it_works … ok</code>,每行输出一个单元测试结果</li>
<li>再输出集成测试的结果,<code>Running tests\integration_test.rs</code>,表示正在测试哪个文件的测试模块,后续跟着这个文件的测试结果</li>
<li>最后是文档测试</li>
</ol>
<p>当编写的测试代码越多,输出也就会越多越杂,所以我们也可以使用<code>--test</code>参数指定集成测试的文件名,单独进行测试。如:<code>cargo test --test integration_test</code></p>
<h3 id="在集成测试中使用子模块"><a href="#在集成测试中使用子模块" class="headerlink" title="在集成测试中使用子模块"></a>在集成测试中使用子模块</h3><p>测试模块也和普通模块差不多,可以把函数分解到不同文件不同子目录里,当我们需要测试内容越来越多的时候,就会需要这么做。</p>
<p>但因为测试的特殊性,rust会把每个集成测试的文件编译成独立的包来隔离作用域,模拟用户实际的使用环境,这就意味着我们以前在src目录下管理文件的方法并不完全适用于tests目录了。</p>
<p>例如,我们需要编写一个common.rs文件,并且编写一个setup函数,这个函数将会用在多个不同的测试文件里使用,如</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn setup(){</span><br><span class="line"> // 一些测试所需要初始化的数据</span><br><span class="line"> a = 1;</span><br><span class="line"> a</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>我们执行cargo test时会有以下输出:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line"> Running tests\common.rs (target\debug\deps\common-15055e88a26e37ec.exe)</span><br><span class="line"></span><br><span class="line">running 0 tests</span><br><span class="line"></span><br><span class="line">test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span><br></pre></td></tr></table></figure></div>
<p>可以发现,即便我们没有在common.rs里写任何测试函数,它依旧会将它作为测试文件执行,并输出无意义的running 0 tests。这明显不是我们所希望的,那如何解决呢?</p>
<p>我们可以使用mod.rs文件,把common.rs的文件内容移到tests/common/mod.rs里面,这样的意思是让Rust把common视作一个模块,而不是集成测试文件。</p>
<p>于是,我们就可以通过mod关键字引入common模块并使用其中的函数,例如</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">// tests/integration_test.rs</span><br><span class="line">use adder;</span><br><span class="line"></span><br><span class="line">mod common;</span><br><span class="line"></span><br><span class="line">#[test]</span><br><span class="line">fn add_two() {</span><br><span class="line"> common::setup();</span><br><span class="line"> assert_eq!(adder::add(2, 2), 4);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>此时再运行cargo test,就不会出现common相关的测试输出了。</p>
<h3 id="二进制包的集成测试"><a href="#二进制包的集成测试" class="headerlink" title="二进制包的集成测试"></a>二进制包的集成测试</h3><p>如果我们的项目只有src/main.rs而没有src/lib.rs的话,是没有办法在tests中进行集成测试的,因为只有把代码用lib.rs文件指定为一个代码包crate,才能把函数暴露给其他包来使用,而main.rs对应的是二进制包,只能单独执行自己。</p>
<p>所以Rust的二进制项目通常会把逻辑编写在src/lib.rs里,main.rs只对lib.rs的内容进行简单的调用。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>没什么好总结的,下一章写项目去了。</p>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>Rust</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】13. 闭包与迭代器</title>
<url>/2024/01/18/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9113-%E9%97%AD%E5%8C%85%E4%B8%8E%E8%BF%AD%E4%BB%A3%E5%99%A8/</url>
<content><![CDATA[<h1 id="闭包:能够捕获环境的匿名函数"><a href="#闭包:能够捕获环境的匿名函数" class="headerlink" title="闭包:能够捕获环境的匿名函数"></a>闭包:能够捕获环境的匿名函数</h1><p>如这个小节的标题所示,闭包其实就是一个匿名函数,可以接收变量,也可以返回值,主要是用来实现一些代码复用和自定义的行为。</p>
<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>首先,闭包的基本定义方法如下</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">c</span> = |a, b|{a+b};</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">d</span> = |a, b| a-b;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}, {}"</span>, <span class="title function_ invoke__">c</span>(a, b), <span class="title function_ invoke__">d</span>(a, b));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<ol>
<li>通过**||**包裹住传入的参数,通过逗号隔开</li>
<li>花括号内定义函数的行为。当然,对于简短的函数,例如a+b这种一条表达式直接返回值的,也可以不用花括号,如d</li>
</ol>
<p>闭包与函数不同的是</p>
<ol>
<li><p>闭包内<strong>不强迫显示标注类型</strong>,因为考虑到闭包这种通常使用的场景就是一个狭窄的上下文内,进行一个暂时的函数定义,而不会被广泛的定义。So, rust的设计者认为这种场景下干脆默认直接交给编译器推理就行。所以,闭包也有编译期固定类型的特性,闭包第一次被调用的时候,就按照第一次传入的参数被固定了,那第二次调用的时候是无法使用其他类型的参数传入的。如下面的代码会报错:<code>error[E0308]: arguments to this function are incorrect</code></p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">closure</span> = |a, b|{a+b};</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, <span class="title function_ invoke__">closure</span>(a, b));</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">c</span> = <span class="number">1.1</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">d</span> = <span class="number">2.2</span>;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, <span class="title function_ invoke__">closure</span>(c, d));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>如果要实现复杂的闭包,用泛型等,自行指定类型也是没有问题的</p>
</li>
<li><p>闭包<strong>可以作为变量存储</strong>,这也就增加了更多代码复用的写法,例如作为变量存到结构体里,在结构体里按需调用之类的</p>
</li>
<li><p>闭包<strong>可以捕获环境上下文</strong>;这是一个与函数相比最大的不同点,这个就稍微展开来讲讲。</p>
</li>
</ol>
<h2 id="使用闭包捕获上下文环境"><a href="#使用闭包捕获上下文环境" class="headerlink" title="使用闭包捕获上下文环境"></a>使用闭包捕获上下文环境</h2><p>首先,捕获上下文是什么意思?看代码就知道了:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">closure</span> = |num| num==a;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, <span class="title function_ invoke__">closure</span>(<span class="number">1</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>在代码里可以看到,我们的闭包在没有传入a的情况下,获取到了闭包外的值a来进行一系列的处理。而普通函数如果不对a进行显示的传入的话,是不可能获得a的值的,这就是所谓的捕获上下文的能力。</p>
<p>那么问题来了,rust里的核心机制就是所有权,参数到了函数里,所有权也会转到函数中,那a在闭包内的所有权是怎么变化的呢?</p>
<p>我们换个例子来讲,因为a是整型,是存储在栈里的变量,在传入传出的时候是无脑的复制一份所有权,所以不具有代表性,我们换成数组。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">closure</span> = |num| num ==a[<span class="number">0</span>];</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, <span class="title function_ invoke__">closure</span>(<span class="number">1</span>));</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, a);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>闭包里面包含了三个trait,分别是<strong>FnOnce</strong>, <strong>FnMut</strong>, <strong>Fn</strong>,而对于每一个闭包,至少实现了这三个trait中的一个。</p>
<ol>
<li><strong>FnOnce</strong>是在闭包定义时会调用的,而且也只会调用一次的trait,他会把捕获到的环境变量所有权转移到闭包环境内。</li>
<li><strong>FnMut</strong>是可变的借用捕获的变量,不夺取所有权</li>
<li><strong>Fn</strong>则是不可变的借用</li>
</ol>
<p>闭包会对变量实现哪个trait是自动的,主要取决于你在闭包内用变量来干了什么。例如我的数组例子里,是可以运行成功的,因为我只简单的使用了一下数组内的元素进行==的比较,并没有设计数值修改,所以闭包自动选择了不可变借用的形式。如以下形式,就是可变借用:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]; <span class="comment">// 注意a要改成可变</span></span><br><span class="line"> <span class="keyword">let</span> <span class="variable">closure</span> = |tmp| {</span><br><span class="line"> a = tmp;</span><br><span class="line"> a</span><br><span class="line"> };</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="built_in">vec!</span>[<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, <span class="title function_ invoke__">closure</span>(b));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>如果你希望夺取所有权的话,有一个关键词<strong>move</strong>可以实现这么一个功能。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">closure</span> = <span class="keyword">move</span> |num| num == a[<span class="number">0</span>];</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, <span class="title function_ invoke__">closure</span>(<span class="number">1</span>));</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, a);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>定义为move后因为a的所有权被移到了闭包内,因此代码报错,无法运行。</p>
<p>除了自动实现的以外,你也可以采用之前讲的trait相关内容,为闭包指定trait类型,也可以重载实现trait以自定义更多的行为,这里就不多做讲解了。</p>
<h1 id="迭代器:处理元素序列"><a href="#迭代器:处理元素序列" class="headerlink" title="迭代器:处理元素序列"></a>迭代器:处理元素序列</h1><p>迭代器也并不是一个新概念了,简单来说,迭代器主要就是用来遍历序列的,主要是解决传统遍历方式需要程序员自己判断序列是什么时候结束的问题,用迭代器就可以省去每次遍历都要定义一个=0的量,获取一次序列大小的逻辑。</p>
<p>迭代器的本质是一个<code>Iterator</code>的trait,如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub trait Iterator {</span><br><span class="line"> type Item;</span><br><span class="line"></span><br><span class="line"> fn next(&mut self) -> Option<Self::Item>;</span><br><span class="line"></span><br><span class="line"> // 这里省略了由Rust给出的默认实现方法</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>所以迭代器本质上就是不断的调用next方法,生成下一个元素的值,包装成Option然后返回。</p>
<p>迭代器的生成是lazy的,和python的yield一样,只有当你遍历到那个元素的时候,迭代器才会使用next生成一个元素,然后返回到外面提供用户使用,在没有使用到的时候什么也不会发生,这在一定程度上节约了内存的使用。</p>
<p>上面的代码有一些新的语法,<code>type Item</code>,<code>Self::Item</code>等,这个Item的功能是用来指定一个数据类型,后面会讲。</p>
<h2 id="创建一个迭代器"><a href="#创建一个迭代器" class="headerlink" title="创建一个迭代器"></a>创建一个迭代器</h2><h3 id="iter"><a href="#iter" class="headerlink" title=".iter()"></a>.iter()</h3><p>创建一个迭代器有很多方法,最基础的就是.iter()</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a_iter</span> = a.<span class="title function_ invoke__">iter</span>();</span><br><span class="line"> <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> a_iter {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, i);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>以上代码就是一个最简单的,使用<code>.iter()</code>创建一个列表的迭代器来遍历列表的示例。</p>
<p>当然,既然说过迭代器的本质是不断调用next方法,所以我们也可以使用next方法来使用迭代器。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">a_iter</span> = a.<span class="title function_ invoke__">iter</span>();</span><br><span class="line"> <span class="built_in">assert_eq!</span>(a_iter.<span class="title function_ invoke__">next</span>(), <span class="title function_ invoke__">Some</span>(&<span class="number">1</span>));</span><br><span class="line"> <span class="built_in">assert_eq!</span>(a_iter.<span class="title function_ invoke__">next</span>(), <span class="title function_ invoke__">Some</span>(&<span class="number">2</span>));</span><br><span class="line"> <span class="built_in">assert_eq!</span>(a_iter.<span class="title function_ invoke__">next</span>(), <span class="title function_ invoke__">Some</span>(&<span class="number">3</span>));</span><br><span class="line"> <span class="built_in">assert_eq!</span>(a_iter.<span class="title function_ invoke__">next</span>(), <span class="literal">None</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里需要注意的是,<strong>a_iter必须定义为可变</strong>,因为调用next是会不断的吃掉上一个变量,替换成下一个变量的迭代器。</p>
<h3 id=""><a href="#" class="headerlink" title=""></a></h3><h2 id="使用迭代器"><a href="#使用迭代器" class="headerlink" title="使用迭代器"></a>使用迭代器</h2><p>因为next方法是会不断“吃掉”上一个元素的,因此迭代器的使用也伴随着迭代器的消耗。基本的for循环就是使用迭代器的一个方法。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a_iter</span> = a.<span class="title function_ invoke__">iter</span>();</span><br><span class="line"> <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> a_iter {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, i);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> a_iter {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, i);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段代码运行将会报错,可以看到,a_iter在被第一个for循环使用完之后,就无法继续使用了。</p>
<p>除了基本的循环外,还有很多提供便捷功能的迭代器相关方法,它们也同样会消耗迭代器。</p>
<ol>
<li><p><code>sum</code>方法会自动生成所有迭代器,并返回其中所有元素的和。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a_iter</span> = a.<span class="title function_ invoke__">iter</span>();</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">total</span>: <span class="type">i32</span> = a_iter.<span class="title function_ invoke__">sum</span>();</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, total);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>需要注意的是,这里必须把a_iter.sum()的结果存储到变量中打印,并显示标注total的类型,但这是后面再讲的问题了</p>
</li>
<li><p><code>collect</code>方法可以自动收集所有迭代器生成的元素,返回一个列表。</p>
</li>
</ol>
<p>除了消耗迭代器返回元素的方法外,还有一种方法可以消耗旧的迭代器,生成新的符合一定要求的迭代器。这种方法成为<strong>迭代器适配器</strong></p>
<ol>
<li><p><code>map</code>方法可以接收一个闭包作为参数,自定义一个新的迭代器出来。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a_iter</span> = a.<span class="title function_ invoke__">iter</span>();</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a2_iter</span> = a_iter.<span class="title function_ invoke__">map</span>(|x| x * <span class="number">2</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span>: <span class="type">Vec</span><<span class="type">i32</span>> = a2_iter.<span class="title function_ invoke__">collect</span>();</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这个示例中,我们使用map方法,生成一个会把原来元素*2再返回的迭代器,再使用collect方法收集成新的列表并打印输出</p>
</li>
<li><p><code>filter</code>方法同样也是接收闭包为参数,但这个闭包必须是返回一个<strong>bool</strong>值,利用闭包的bool结果,filter方法会过滤掉返回结果不为true的迭代器。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="built_in">vec!</span>[<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a_iter</span> = a.<span class="title function_ invoke__">into_iter</span>();</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a2_iter</span> = a_iter.<span class="title function_ invoke__">filter</span>(|x| x % <span class="number">2</span> == <span class="number">0</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span>: <span class="type">Vec</span><<span class="type">i32</span>> = a2_iter.<span class="title function_ invoke__">collect</span>();</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{:?}"</span>, b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里使用的是into_iter,因为a.iter()返回的是一个<strong>引用</strong>,所以在执行%运算的时候会报类型不匹配的错误;要么解引用,要么用into_iter直接夺取所有权,这里我偷了个懒。(PS:上面的乘法能编译通过是因为乘法这种基本运算会自动解引用。)</p>
<p>当然,既然filter和map的传入参数是闭包,自然也可以捕获环境上下文,感兴趣可以自己试试,这里就不多写演示代码了。</p>
</li>
</ol>
<p>还有很多方法,例如zip(),skip(),这里就没办法一一展开了</p>
<h2 id="自定义迭代器"><a href="#自定义迭代器" class="headerlink" title="自定义迭代器"></a>自定义迭代器</h2><p>迭代器本质上是一个Iterator trait,所以我们自然也可以定义一个结构体,为他实现一个Iterator trait,进而实现一个我们自定义的迭代器。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Counter</span> {</span><br><span class="line"> count: <span class="type">u32</span>,</span><br><span class="line">}</span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Counter</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">new</span>() <span class="punctuation">-></span> Counter {</span><br><span class="line"> Counter { count: <span class="number">0</span> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Iterator</span> <span class="keyword">for</span> <span class="title class_">Counter</span> {</span><br><span class="line"> <span class="keyword">type</span> <span class="title class_">Item</span> = <span class="type">u32</span>;</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">next</span>(&<span class="keyword">mut</span> <span class="keyword">self</span>) <span class="punctuation">-></span> <span class="type">Option</span><<span class="keyword">Self</span>::Item> {</span><br><span class="line"> <span class="keyword">self</span>.count += <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">self</span>.count < <span class="number">6</span> {</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(<span class="keyword">self</span>.count)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="literal">None</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">counter</span> = Counter::<span class="title function_ invoke__">new</span>();</span><br><span class="line"> <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> counter {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, i);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段示例里,我们就定义了一个结构体<strong>Counter</strong>,内置一个u32变量<strong>count</strong>,初始值为0,然后为Counter实现了一个Iterator trait,让Counter结构体变成了一个迭代器,迭代的逻辑就是不断的递增结构体内的count变量,直到count>=6。</p>
<h2 id="迭代器的性能分析"><a href="#迭代器的性能分析" class="headerlink" title="迭代器的性能分析"></a>迭代器的性能分析</h2><p>例如说我们现在有一个功能,需要在一堆字符串中,找到所有包含某个子串的字符串。那最简单的,利用循环的写法可以写成如下形式:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">search</span><<span class="symbol">'a</span>>(query: &<span class="type">str</span>, contents: &<span class="symbol">'a</span> <span class="type">str</span>) <span class="punctuation">-></span> <span class="type">Vec</span><&<span class="symbol">'a</span> <span class="type">str</span>> {</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">results</span> = <span class="type">Vec</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line"> <span class="keyword">for</span> <span class="variable">line</span> <span class="keyword">in</span> contents.<span class="title function_ invoke__">lines</span>() {</span><br><span class="line"> <span class="keyword">if</span> line.<span class="title function_ invoke__">contains</span>(query) {</span><br><span class="line"> results.<span class="title function_ invoke__">push</span>(line);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> results</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>那在知道迭代器之后,我们自然也可以想到可以通过迭代器的filter方法,过滤出只包含这个子串的迭代器,迭代器的版本可以这么写:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">search</span><<span class="symbol">'a</span>>(query: &<span class="type">str</span>, contents: &<span class="symbol">'a</span> <span class="type">str</span>) <span class="punctuation">-></span> <span class="type">Vec</span><&<span class="symbol">'a</span> <span class="type">str</span>> {</span><br><span class="line"> contents.<span class="title function_ invoke__">lines</span>()</span><br><span class="line"> .<span class="title function_ invoke__">filter</span>(|line| line.<span class="title function_ invoke__">contains</span>(query))</span><br><span class="line"> .<span class="title function_ invoke__">collect</span>()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>那这两种写法有什么区别呢,多抽象一层迭代器的话,会不会有性能损失?</p>
<p>答案是不会的。</p>
<p>Rust官方书籍里用一本小说做了一次bench mark,结果表面,迭代器的写法在经过优化后比for循环更快。</p>
<div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">test</span> bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)</span><br><span class="line"><span class="built_in">test</span> bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)</span><br></pre></td></tr></table></figure></div>
<p>迭代器是Rust里的一种零开销抽象,所以我们在使用的时候根本不需要担心会不会引入额外的运行成本。</p>
<p>至于快了的那一点,本质上是因为迭代器在编译过程中会运行一次展开优化,例如本来要运行12次的循环代码,如果是迭代器的话,在编译之后会展开成重复执行12次的代码,减少了循环控制时候的开销。</p>
<p>但个人觉得可以忽略不计,大可依照个人习惯的去选择使用这两种写法,benchmark本质上只是证明了iter并不会引入额外开销,放心使用。</p>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>Rust</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】14. 进一步认识Cargo及crates.io</title>
<url>/2024/01/23/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9114-%E8%BF%9B%E4%B8%80%E6%AD%A5%E8%AE%A4%E8%AF%86Cargo%E5%8F%8Acrates-io/</url>
<content><![CDATA[<p>这一章就主要讲讲Cargo这个工具的一些用途,主要是以下几个部分,没有涉猎到的可以在<a class="link" href="https://doc.rust-lang.org/cargo/" >官网<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>查看cargo更全面的介绍:</p>
<ol>
<li>build时使用的release profile相关介绍</li>
<li>怎么将你写的包发布到creates.io上给别人用</li>
<li>使用工作空间组织项目</li>
<li>下载安装craetes.io的包</li>
<li>使用自定义命令来扩展cargo</li>
</ol>
<h1 id="release-profile"><a href="#release-profile" class="headerlink" title="release profile"></a>release profile</h1><p>Rust内置了两套release profile,一套就是我们在<code>cargo build</code>时候使用的debug用dev profile,一套就是<code>cargo build --release</code>对应的发布用release profile</p>
<p>我们可以在toml文件内自定义的修改这两套配置。对应的在[<a class="link" href="http://profile.xxx/" >profile.xxx<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>]字段内,如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[profile.dev]</span><br><span class="line">opt-level = 0</span><br><span class="line"></span><br><span class="line">[profile.release]</span><br><span class="line">opt-level = 3</span><br></pre></td></tr></table></figure></div>
<p><strong>opt-level</strong>对应的是编译时的优化等级,等级越高,编译耗时越长,但最终可执行文件的运行速度就越快,值只能是0,1,2,3。其中dev配置默认是0,relese配置默认是3。如果有必要,你也可以自行选择修改成1和2</p>
<h1 id="怎么发布你的包到creates-io"><a href="#怎么发布你的包到creates-io" class="headerlink" title="怎么发布你的包到creates.io"></a>怎么发布你的包到creates.io</h1><h2 id="编写有用的文档注释"><a href="#编写有用的文档注释" class="headerlink" title="编写有用的文档注释"></a>编写有用的文档注释</h2><p>一份好的文档能帮助用户快速理解这个包的具体用途和使用方法,Rust提供了一种可以快<strong>速生成HTML文档</strong>的注释方法(nb,我还以为是docstring,居然可以直接生成文档)</p>
<p>之前我们都知道双斜杠<code>//</code>可以注释,而我们的文档注释就是<code>///</code>三斜杠,文档注释主要用来介绍使用方法,而不是介绍包的实现细节,例如:</p>
<p><strong>lib.rs</strong></p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="comment">/// 将传入的数字加1</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// # Examples</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// ```</span></span><br><span class="line"><span class="comment">/// let arg = 5;</span></span><br><span class="line"><span class="comment">/// let answer = my_crate::add_one(arg);</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// assert_eq!(6, answer);</span></span><br><span class="line"><span class="comment">/// ```</span></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">add_one</span>(x: <span class="type">i32</span>) <span class="punctuation">-></span> <span class="type">i32</span> {</span><br><span class="line"> x + <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>第一行介绍了函数对应的功能,然后用类似markdown的语法表明了一块Examples区域,最后提供了一片代码块示例。写完之后,我们只需要运行命令:<code>cargo doc</code>就可以自动生成一份HTML文档了,再执行<code>cargo doc --open</code>就可以自动用浏览器打开文档了。</p>
<p>生成后的样子:<br> <img
lazyload
src="/images/loading.svg"
data-src="/images/image-wgmx.png"
alt="img"
></p>
<p>除了Examples外,还有其他一些可选的文档区域,如:</p>
<ul>
<li><strong>Panics</strong>,指出函数可能引发panic的场景。不想触发panic的调用者应当确保自己的代码不会在这些场景下调用该函数。</li>
<li><strong>Errors</strong>,当函数返回Result作为结果时,这个区域会指出可能出现的错误,以及造成这些错误的具体原因,它可以帮助调用者在编写代码时为不同的错误采取不同的措施。</li>
<li><strong>Safety</strong>,当函数使用了unsafe关键字(在第19章讨论)时,这个区域会指出当前函数不安全的原因,以及调用者应当确保的使用前提。</li>
</ul>
<p>此外,有意思的是,你写的示例内的代码并不只是给用户看的。在你执行<code>cargo test</code>的时候,<strong>cargo会把你文档注释内的代码一并执行测试</strong>,因为没有比文档内的示例代码不能正常执行更让人恶心的了哈哈哈。</p>
<hr>
<p>另一种文档注释的形式是<code>//!</code>。这种文档注释会显示在注释的最外层,一般用在包的根文件上,用来介绍整个包的大致情况。我们改成如下注释看看:</p>
<p><strong>lib.rs</strong></p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="comment">//! # My Crate</span></span><br><span class="line"><span class="comment">//!</span></span><br><span class="line"><span class="comment">//! my_crate是一系列工具的集合,</span></span><br><span class="line"><span class="comment">//! 这些工具被用来简化特定的计算操作</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/// 将传入的数字加1</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// # Examples</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// ```</span></span><br><span class="line"><span class="comment">/// let arg = 5;</span></span><br><span class="line"><span class="comment">/// let answer = my_crate::add_one(arg);</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// assert_eq!(6, answer);</span></span><br><span class="line"><span class="comment">/// ```</span></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">add_one</span>(x: <span class="type">i32</span>) <span class="punctuation">-></span> <span class="type">i32</span> {</span><br><span class="line"> x + <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>此时生成的文档如下:</p>
<p><img
lazyload
src="/images/loading.svg"
data-src="/images/image-cinf.png"
alt="img"
></p>
<p>可以看见<code>//!</code>的注释显示在了add_one对应注释的外层,点击add_one后才会进入刚才对应的add_one用例文档</p>
<h2 id="使用pub-use来导出合适的公共API"><a href="#使用pub-use来导出合适的公共API" class="headerlink" title="使用pub use来导出合适的公共API"></a>使用pub use来导出合适的公共API</h2><p>这一部分的内容和use章里讲的内容基本是异曲同工,不过这里可以结合文档部分再讲一下。</p>
<p>这一部分解决的问题主要是:假如我们的代码结构嵌套的非常复杂的时候,那么用户在调用的时候代码就会写成<code>use::aaa::bbb::ccc::ddd::eee::fff</code>,很长很难看也很不好用,那该怎么办呢?</p>
<p>结合文档来看一下,例如以下代码:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="comment">//! # Art</span></span><br><span class="line"><span class="comment">//!</span></span><br><span class="line"><span class="comment">//! 一个用来建模艺术概念的代码库</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">mod</span> kinds {</span><br><span class="line"> <span class="comment">/// RYB颜色模型的三原色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">enum</span> <span class="title class_">PrimaryColor</span> {</span><br><span class="line"> Red,</span><br><span class="line"> Yellow,</span><br><span class="line"> Blue,</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// RYB模型的调和色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">enum</span> <span class="title class_">SecondaryColor</span> {</span><br><span class="line"> Orange,</span><br><span class="line"> Green,</span><br><span class="line"> Purple,</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">mod</span> utils {</span><br><span class="line"> <span class="keyword">use</span> crate::kinds::*;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 将两种等量的原色混合生成调和色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">mix</span>(c1: PrimaryColor, c2: PrimaryColor) <span class="punctuation">-></span> SecondaryColor {</span><br><span class="line"> <span class="comment">// --略--</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>会生成以下文档:</p>
<p><img
lazyload
src="/images/loading.svg"
data-src="/images/image-imzx.png"
alt="img"
></p>
<p>可见暴露在外的只有kinds和utils,而我们高频使用的应该是kinds和utils的内容,这部分却要点进二级目录才能看到具体有什么。</p>
<p>要解决这个问题,我们只需要在结构外use一遍,把里面的结构暴露到外面即可。如下:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="comment">//! # Art</span></span><br><span class="line"><span class="comment">//!</span></span><br><span class="line"><span class="comment">//! 一个用来建模艺术概念的代码库</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">use</span> self::kinds::PrimaryColor;</span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">use</span> self::kinds::SecondaryColor;</span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">use</span> self::utils::mix;</span><br><span class="line"></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">mod</span> kinds {</span><br><span class="line"> <span class="comment">/// RYB颜色模型的三原色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">enum</span> <span class="title class_">PrimaryColor</span> {</span><br><span class="line"> Red,</span><br><span class="line"> Yellow,</span><br><span class="line"> Blue,</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// RYB模型的调和色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">enum</span> <span class="title class_">SecondaryColor</span> {</span><br><span class="line"> Orange,</span><br><span class="line"> Green,</span><br><span class="line"> Purple,</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">pub</span> <span class="keyword">mod</span> utils {</span><br><span class="line"> <span class="keyword">use</span> crate::kinds::*;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// 将两种等量的原色混合生成调和色</span></span><br><span class="line"> <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">mix</span>(c1: PrimaryColor, c2: PrimaryColor) <span class="punctuation">-></span> SecondaryColor {</span><br><span class="line"> <span class="comment">// --略--</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>以上代码我们只是在开头添加了几句<code>pub use</code>,生成的文档如下</p>
<p><img
lazyload
src="/images/loading.svg"
data-src="/images/image-klpp.png"
alt="img"
></p>
<p>可见我们<code>pub use</code>的函数也暴露在文档首页了;不过具体注释还是要点进去才能看见</p>
<p>同时,我们在文件外使用这些函数的时候也从:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub use art::kinds::PrimaryColor;</span><br><span class="line">pub use art::kinds::SecondaryColor;</span><br><span class="line">pub use art::utils::mix;</span><br></pre></td></tr></table></figure></div>
<p>变成了:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub use art::PrimaryColor;</span><br><span class="line">pub use art::SecondaryColor;</span><br><span class="line">pub use art::mix;</span><br></pre></td></tr></table></figure></div>
<h2 id="发布你的包"><a href="#发布你的包" class="headerlink" title="发布你的包"></a>发布你的包</h2><p>代码完成后,发布包只需按照以下几个步骤</p>
<ol>
<li><p>创建crates.io账户</p>
<p>创建省略</p>
<p>创建完毕后需要获取api令牌,并使用cargo进行本地身份验证,如:</p>
<div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="code"><pre><span class="line">cargo login abcdefghijklmnopqrstuvwxyz012345</span><br></pre></td></tr></table></figure></div>
<p>这句命令会把你的令牌存入<code>~/.cargo/credentials</code></p>
</li>
<li><p>为包添加元数据</p>
<p>在<code>Cargo.toml</code>里面可以配置包名,描述,版本等等信息,填写示例如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[package]</span><br><span class="line">name = "guessing_game"</span><br><span class="line">version = "0.1.0"</span><br><span class="line">authors = ["Your Name <[email protected]>"]</span><br><span class="line">edition = "2018"</span><br><span class="line">description = "A fun game where you guess what number the computer has chosen."</span><br><span class="line">license = "MIT OR Apache-2.0"</span><br></pre></td></tr></table></figure></div>
</li>
<li><p>发布:执行<code>cargo publish</code>即可完成发布</p>
</li>
<li><p>更新:只需要更改<strong>toml</strong>里的<strong>version</strong>字段,再重新<strong>publish</strong>就能完成新版本的更新了</p>
</li>
<li><p>撤回:执行<code>cargo yank --vers 1.0.1</code>即可撤回指定版本的包;如果操作失误了,也可以撤回你的撤回:<code>cargo yank --vers 1.0.1 --undo</code>;但需要注意的是,撤回不会删除任何代码,只能阻止别人的代码下载或指定这个版本的包为依赖</p>
</li>
</ol>
<h1 id="Cargo工作空间"><a href="#Cargo工作空间" class="headerlink" title="Cargo工作空间"></a>Cargo工作空间</h1><p>工作空间的概念可以把项目的代码划分成若干个包,在互相关联的同时更方便的协同开发。</p>
<h2 id="创建工作空间"><a href="#创建工作空间" class="headerlink" title="创建工作空间"></a>创建工作空间</h2><p>这时候我们需要新建一个文件夹,名为<strong>add</strong>,直接新建一个文件夹即可,不需要<code>cargo new</code>命令创建。</p>
<p>然后,我们再自己建一个<code>Cargo.toml</code>文件,填入以下内容</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[workspace]</span><br><span class="line"></span><br><span class="line">members = [</span><br><span class="line"> "adder",</span><br><span class="line">]</span><br></pre></td></tr></table></figure></div>
<p>这个toml文件不像我们之前碰到的toml一样有一堆字段,他只包含一个工作空间字段,声明了工作空间下的成员有一个<strong>adder</strong>,这个<strong>adder</strong>我们还没创建,现在来创建一下。</p>
<p>使用cargo new字段创建一个二进制包:<code>cargo new adder</code></p>
<p>这个时候我们就已经可以在add目录下使用<code>cargo build</code>构建整个工作空间了。</p>
<h2 id="在工作空间里创建第二个包"><a href="#在工作空间里创建第二个包" class="headerlink" title="在工作空间里创建第二个包"></a>在工作空间里创建第二个包</h2><p>我们可以用<code>cargo new add-one --lib</code>来创建一个代码包,当然,我们同时也得在toml文件加上</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[workspace]</span><br><span class="line"></span><br><span class="line">members = ["adder", "add-one"]</span><br></pre></td></tr></table></figure></div>
<p>然后,我们再在<strong>add-one</strong>里的<strong>lib.rs</strong>写上我们可以供外部调用的接口函数,如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">pub fn add_one(x: i32) -> i32 {</span><br><span class="line"> x + 1</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>那我们在adder里要怎么调用add-one里的接口呢?</p>
<p>工作空间是不会自动为你假设出依赖关系的,所以需要自己在toml里显式的指定,例如我们要在adder里调用add-one的函数,就<strong>要在adder的toml里添加以下字段</strong>:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[dependencies]</span><br><span class="line"></span><br><span class="line">add-one = { path = "../add-one" }</span><br></pre></td></tr></table></figure></div>
<p>这一部分指定了adder需要依赖的包,名字是什么,对应的包路径在哪。显式指定后,<a href="http://我们就可以在adder的main.rs/">我们就可以在<strong>adder</strong>的<strong>main.rs</strong></a>里编写代码,并<strong>使用add-one里的接口</strong>了,如下:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">use</span> add_one;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">num</span> = <span class="number">10</span>;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"Hello, world! {} plus one is {}!"</span>, num, add_one::<span class="title function_ invoke__">add_one</span>(num));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>最后,我们再在根目录add下build编译并运行。</p>
<p>运行和往常有所不同,因为一个工作空间可能有很多个二进制包,所以我们需要指定运行哪个二进制包,加上<code>-p</code>参数即可,如:<code>cargo run -p adder</code></p>
<p>然后就可以看到正常的输出了</p>
<h2 id="在工作空间中依赖外部包"><a href="#在工作空间中依赖外部包" class="headerlink" title="在工作空间中依赖外部包"></a>在工作空间中依赖外部包</h2><p>和以前一样,也是在toml里指定即可,例如这里我们给add-one引入一个rand包,如下:</p>
<div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="code"><pre><span class="line">[dependencies]</span><br><span class="line"></span><br><span class="line">rand = "0.3.14"</span><br></pre></td></tr></table></figure></div>
<p>再执行cargo build,就可以自动的下载安装rand包了。</p>
<hr>
<p>这里值得一提的是,Cargo.lock文件只在add根目录下有,这是为了保证工作空间下所有包的依赖都是同一版本的,一是节约了磁盘空间,二是确保工作空间下的包互相之间是兼容的,避免奇奇怪怪的问题。</p>
<h2 id="工作空间下进行测试"><a href="#工作空间下进行测试" class="headerlink" title="工作空间下进行测试"></a>工作空间下进行测试</h2><p>和往常一样编写测试代码,然后在根目录下执行cargo test即可自动完成所有测试。</p>
<p>如:</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">add_one</span>(x: <span class="type">i32</span>) <span class="punctuation">-></span> <span class="type">i32</span> {</span><br><span class="line"> x + <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">#[cfg(test)]</span></span><br><span class="line"><span class="keyword">mod</span> tests {</span><br><span class="line"><span class="keyword">use</span> super::*;</span><br><span class="line"></span><br><span class="line"><span class="meta">#[test]</span></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">it_works</span>() {</span><br><span class="line"><span class="built_in">assert_eq!</span>(<span class="number">3</span>, <span class="title function_ invoke__">add_one</span>(<span class="number">2</span>));</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">$ cargo test</span><br><span class="line">warning: <span class="keyword">virtual</span> workspace defaulting to `resolver = <span class="string">"1"</span>` despite one or more workspace members being on edition <span class="number">2021</span> which implies `resolver = <span class="string">"2"</span>`</span><br><span class="line">note: to keep the current resolver, specify `workspace.resolver = <span class="string">"1"</span>` <span class="keyword">in</span> the workspace root<span class="symbol">'s</span> manifest</span><br><span class="line">note: to <span class="keyword">use</span> the edition <span class="number">2021</span> resolver, specify `workspace.resolver = <span class="string">"2"</span>` <span class="keyword">in</span> the workspace root<span class="symbol">'s</span> manifest</span><br><span class="line">note: <span class="keyword">for</span> <span class="title class_">more</span> details see https:<span class="comment">//doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions</span></span><br><span class="line"> Compiling add-one v0.<span class="number">1.0</span> (E:\Code\rust\learning\add\add-one)</span><br><span class="line"> Compiling adder v0.<span class="number">1.0</span> (E:\Code\rust\learning\add\adder)</span><br><span class="line"> Finished test [unoptimized + debuginfo] <span class="title function_ invoke__">target</span>(s) <span class="keyword">in</span> <span class="number">0.29</span>s</span><br><span class="line"> Running unittests src\lib.<span class="title function_ invoke__">rs</span> (target\debug\deps\add_one-<span class="number">48</span>bf46fc7c463809.exe)</span><br><span class="line"></span><br><span class="line">running <span class="number">1</span> test</span><br><span class="line">test tests::it_works ... ok</span><br><span class="line"></span><br><span class="line">test result: ok. <span class="number">1</span> passed; <span class="number">0</span> failed; <span class="number">0</span> ignored; <span class="number">0</span> measured; <span class="number">0</span> filtered out; finished <span class="keyword">in</span> <span class="number">0.00</span>s</span><br><span class="line"></span><br><span class="line"> Running unittests src\main.<span class="title function_ invoke__">rs</span> (target\debug\deps\adder-<span class="number">174</span>daa247398ed5b.exe)</span><br><span class="line"></span><br><span class="line">running <span class="number">0</span> tests</span><br><span class="line"></span><br><span class="line">test result: ok. <span class="number">0</span> passed; <span class="number">0</span> failed; <span class="number">0</span> ignored; <span class="number">0</span> measured; <span class="number">0</span> filtered out; finished <span class="keyword">in</span> <span class="number">0.00</span>s</span><br><span class="line"></span><br><span class="line"> Doc-tests add-one</span><br><span class="line"></span><br><span class="line">running <span class="number">0</span> tests</span><br><span class="line"></span><br><span class="line">test result: ok. <span class="number">0</span> passed; <span class="number">0</span> failed; <span class="number">0</span> ignored; <span class="number">0</span> measured; <span class="number">0</span> filtered out; finished <span class="keyword">in</span> <span class="number">0.00</span>s</span><br></pre></td></tr></table></figure></div>
<h1 id="使用cargo-install来安装可执行程序"><a href="#使用cargo-install来安装可执行程序" class="headerlink" title="使用cargo install来安装可执行程序"></a>使用cargo install来安装可执行程序</h1><p><code>cargo install</code>的主要作用是用来安装别人分享的二进制包的,可以直接在命令行使用别人写好的工具。安装会装在<strong>rust安装的根目录下的bin文件夹</strong>里,所以如果要直接使用的话,需要配置一下环境变量,把这个目录加进去。</p>
<p>例如:<code>cargo install ripgrep</code></p>
<p>具体就没什么好演示的了,这是一个用来搜索文件的小工具。</p>
<h1 id="使用自定义命令扩展Cargo的功能"><a href="#使用自定义命令扩展Cargo的功能" class="headerlink" title="使用自定义命令扩展Cargo的功能"></a>使用自定义命令扩展Cargo的功能</h1><p>cargo xxx可以运行xxx这个二进制文件,所以如果你用cargo install安装了某个扩展,也可以用cargo xxx来运行</p>
]]></content>
<categories>
<category>编程/语言</category>
</categories>
<tags>
<tag>Rust</tag>
</tags>
</entry>
<entry>
<title>【Rust 学习记录】6. 枚举与模式匹配</title>
<url>/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/</url>
<content><![CDATA[<h1 id="定义枚举"><a href="#定义枚举" class="headerlink" title="定义枚举"></a>定义枚举</h1><p>例子:IP地址,只有 IPV4, IPV6 两个模式。</p>
<p>所以我们可以通过枚举类型的方式来描述 IP 地址的类型。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">IpAddKind</span>{</span><br><span class="line"> IPV4,</span><br><span class="line"> IPV6</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">four</span> = IpAddKind::IPV4;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">six</span> = IpAddKind::IPV6;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里也就两个点:</p>
<ol>
<li>使用 <code>enum</code> 关键字就可以完成一个枚举类型的定义。</li>
<li>访问枚举类型的值是通过命名空间的方式来实现的,而不是点运算符。</li>
</ol>
<p>接下来再谈论一个实际的问题:这里的枚举类型只定义了两个类别,没有办法对应实际的IP地址,怎么办?</p>
<p>一般情况下,我们首先想到的肯定是用结构体,一个字段存储类型,一个字段存储地址对吧。但是 Rust 有一个非常方便的特性:<strong>关联的数据可以直接嵌入到枚举类型里</strong>!</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">IpAddKind</span>{</span><br><span class="line"> <span class="title function_ invoke__">IPV4</span>(<span class="type">String</span>),</span><br><span class="line"> <span class="title function_ invoke__">IPV6</span>(<span class="type">String</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">local</span> = IpAddKind::<span class="title function_ invoke__">IPV4</span>(<span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"127.0.0.1"</span>));</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">loopback</span> = IpAddKind::<span class="title function_ invoke__">IPV6</span>(<span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"::1"</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>厉害吧。不过好像结构体也能做是吧?不完全是,枚举类型有一个结构体做不到的功能。</p>
<p>枚举类型可以为每个类型值指定一个类型,例如 IPV4 通常是四个数字来表示,而 IPV6 则不同,那么我们可以使用四个数字来描述 IPV4 ,使用字符串来描述 IPV6</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">IpAddKind</span>{</span><br><span class="line"> <span class="title function_ invoke__">IPV4</span>(<span class="type">u8</span>, <span class="type">u8</span>, <span class="type">u8</span>, <span class="type">u8</span>),</span><br><span class="line"> <span class="title function_ invoke__">IPV6</span>(<span class="type">String</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这个时候,把类型和内容拆分成两个字段来描述的结构体,就没有办法实现了,只能固定一种类型。</p>
<p>另外,IPV4和IPV6因为很常用,所以在标准库里其实就有定义好了一套枚举类型。它的定义方法是这样的。</p>
<p><img
lazyload
src="/images/loading.svg"
data-src="/images/Snipaste_2023-03-30_21-21-30.jpg"
alt="img"
></p>
<p>也就是先用两个结构体描述一下具体的 IPV4和IPV6 ,然后再定义一个枚举类型,把这两个结构体嵌入到枚举类型里。</p>
<p>另外,枚举类型还有对于结构体另外的优势是,<strong>我们可以轻松的定义一个用于处理多个数据类型的函数</strong>。</p>
<p>例如,我们可以像上面官方一样,用两个结构体去描述 IPV4和IPV6,但如果我们要定义一个函数,同时处理IP地址的话,就不知道该指定传入参数的类型是 IPV4还是IPV6了,但我们定义一个枚举类型 IpAddr ,就可以轻松的定义函数的传入类型为 IpAddr,然后可以同时处理两个结构体的数据。</p>
<h1 id="Option——一个常用的枚举类型"><a href="#Option——一个常用的枚举类型" class="headerlink" title="Option——一个常用的枚举类型"></a>Option——一个常用的枚举类型</h1><p>Option 枚举类型定义了值可能不存在的情况,或者可以说是其他语言的空值 <code>Null</code> 。本来就很常用了,但这个类型在 Rust 里更有用,<strong>因为编译器可以自动检查我们是不是妥善的处理了所有应该被处理的情况</strong>。</p>
<p>Rust 并没有像其他语言一样,有空值 <code>Null</code> 或者 <code>None</code> 的概念。书上说这是个错误的设计方法,因为当你定义了一个空值,在后续可能没有给他赋予其他值就进行使用的话,就会触发问题。这种设计理念可以帮助人们更方便的去实现一套系统,但也给系统埋下了更多的隐患。</p>
<p>Rust 结合了一下这个理念,觉得空值是有意义的,触发空值问题的本质是实现的措施的问题,所以提供了一个具有类似概念的枚举类型——Option. 标准库里的定义是这样的</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Option</span><T>{</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(T),</span><br><span class="line"> <span class="literal">None</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p><code>Option</code> 是被预导入的,因为它很有用,所以我们不用 use 来导入它。所以我们可以不用指定命名空间,使用 <code>Some</code> 或者 <code>None</code> 关键词, 是后面学的语法,用来代表任意类型</p>
<p>我们可以用这个方法来定义一些变量</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">some_number</span> = <span class="title function_ invoke__">Some</span>(<span class="number">5</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">some_string</span> = <span class="title function_ invoke__">Some</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">absent_number</span> = <span class="literal">None</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这段编译是不通过的,这也体现了 Option 相对于普通空值的优势。</p>
<p>我们来简单的通过几个例子来了解一下 Option 的设计理念</p>
<ol>
<li>Option和 T 是两个不同的类型</li>
</ol>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">a</span> = <span class="title function_ invoke__">Some</span>(<span class="number">5</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">b</span> = <span class="number">5</span>;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, a+b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>可以看到这段代码里,虽然 a, b 同样都是 i32 但一个是被<code>Some</code>持有,那它们就是不同的类型,编译器无法理解不同类型相加,所以这就意味着什么。当我们对于一个可能有值,可能没值的变量,我们就要去使用 <code>Option</code> 枚举,一旦使用了<code>Option</code>枚举,我们再<strong>要实际使用它的时候,就要显式的把这个</strong> <code>Some</code> 类型的变量转换到 i32 的变量,再去使用。相当于强迫你去对这个变量编写一段代码,这样就避免了你不经过处理就使用,结果使用到空值的情况。</p>
<p>当然,因为是枚举类型,我们也可以方便的用 <code>match</code> 来处理不同的情况,例如有值时怎么样,没值时怎么样等等。接下来就开始介绍一下 <code>match</code></p>
<h1 id="match运算符"><a href="#match运算符" class="headerlink" title="match运算符"></a>match运算符</h1><p><code>match</code> 是一个很好用的运算符,它除了提高可读性外,也方便编译器对你的代码进行安全检查,确保你对所有可能的情况都进行了处理。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Coin</span>{</span><br><span class="line"> Penny,</span><br><span class="line"> Nickel,</span><br><span class="line"> Dime,</span><br><span class="line"> Quarter,</span><br><span class="line">}</span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">value_in_cents</span>(coin: Coin) <span class="punctuation">-></span> <span class="type">u8</span> {</span><br><span class="line"> <span class="keyword">match</span> coin {</span><br><span class="line"> Coin::Penny => <span class="number">1</span>,</span><br><span class="line"> Coin::Nickel => <span class="number">5</span>,</span><br><span class="line"> Coin::Dime => <span class="number">10</span>,</span><br><span class="line"> Coin::Quarter => <span class="number">25</span>,</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">my_coin</span> = Coin::Dime;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"The value of my coin is {} cents"</span>, <span class="title function_ invoke__">value_in_cents</span>(my_coin));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这就是一个简单的,输入硬币类型,返回硬币价值的代码。相对于 <code>if-else</code> 表达式,match 的第一个优势自然就是可读性。你 if-else 需要想方设法凑一个 bool 值,可能用字符串,可能用字典,可能用数字下标,可能用结构体什么的,但 match 枚举类型就不用,可以为任意你想要的类型定义一个名字,直接用,直接返回任意值,结构还更紧凑好看。</p>
<p>另一个优势就是,match 运算符可以匹配枚举变量的部分值。</p>
<p>美国的25美分硬币里,很多个州都有不同的设计,也只有25美分的硬币有这个特点,所以我们可以给25美分加一个值:州,对应不同的设计</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="meta">#[derive(Debug)]</span></span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">UsState</span>{</span><br><span class="line"> Alabama,</span><br><span class="line"> Alaska,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">Coin</span>{</span><br><span class="line"> Penny,</span><br><span class="line"> Nickel,</span><br><span class="line"> Dime,</span><br><span class="line"> <span class="title function_ invoke__">Quarter</span>(UsState),</span><br><span class="line">}</span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">value_in_cents</span>(coin: Coin) <span class="punctuation">-></span> <span class="type">u8</span> {</span><br><span class="line"> <span class="keyword">match</span> coin {</span><br><span class="line"> Coin::Penny => <span class="number">1</span>,</span><br><span class="line"> Coin::Nickel => <span class="number">5</span>,</span><br><span class="line"> Coin::Dime => <span class="number">10</span>,</span><br><span class="line"> Coin::<span class="title function_ invoke__">Quarter</span>(state) => {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"The state of coin is {:?}"</span>, state);</span><br><span class="line"> <span class="number">25</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">my_coin</span> = Coin::<span class="title function_ invoke__">Quarter</span>(UsState::Alaska);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"The value of my coin is {} cents"</span>, <span class="title function_ invoke__">value_in_cents</span>(my_coin));</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这里我们用到了之前的 Debug 注解,让编译器自动格式化枚举类型的输出,然后在match匹配里,提取了 <code>Quarter</code> 枚举类型里的值,命名为 <code>state</code>,然后输出。</p>
<h2 id="匹配Option"><a href="#匹配Option" class="headerlink" title="匹配Option"></a>匹配Option</h2><p>接下来我们就可以综合一下上面学到的东西,用match来处理一下空和非空的情况了。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">pluse_one</span>(x: <span class="type">Option</span><<span class="type">i32</span>>) <span class="punctuation">-></span> <span class="type">Option</span><<span class="type">i32</span>> {</span><br><span class="line"> <span class="keyword">match</span> x {</span><br><span class="line"> <span class="literal">None</span> => <span class="literal">None</span>,</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(i) => <span class="title function_ invoke__">Some</span>(i + <span class="number">1</span>),</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">num</span> = <span class="title function_ invoke__">Some</span>(<span class="number">5</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">num2</span> = <span class="title function_ invoke__">pluse_one</span>(<span class="literal">None</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>这就是一个典型的例子:</p>
<ol>
<li>当为空的时候,什么也不干</li>
<li>当不为空的时候,取出值,进行处理</li>
</ol>
<p>这也就是 match 的相对于 if-else 的优势:可以以一个更简单,更紧凑,可读性更高的方式,进行模式匹配,进行对应值的处理。接下来就介绍 match 在安全性方面的优势:让编译器帮你检查是否处理完了所有情况。</p>
<h2 id="必须穷举所有可能"><a href="#必须穷举所有可能" class="headerlink" title="必须穷举所有可能"></a>必须穷举所有可能</h2><p>我们来试着漏处理为空值的情况。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">pluse_one</span>(x: <span class="type">Option</span><<span class="type">i32</span>>) <span class="punctuation">-></span> <span class="type">Option</span><<span class="type">i32</span>> {</span><br><span class="line"> <span class="keyword">match</span> x {</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(i) => <span class="title function_ invoke__">Some</span>(i + <span class="number">1</span>),</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">num</span> = <span class="title function_ invoke__">Some</span>(<span class="number">5</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>编译器马上报错:<code>non-exhaustive patterns: None not covered</code> 你没有覆盖处理空值!</p>
<p>这一段代码同时体现了 match 的优势和 Option 实现空值的优势。也就是你必须要处理所有情况,保证没有一点逻辑遗漏的地方,Option也依赖于 match 的这个特性,强迫你处理空值的情况,杜绝了大部分程序员只写一部分 if-else 结果就因为漏了部分情况没处理,导致程序 crash 的问题。</p>
<h2 id="通配符"><a href="#通配符" class="headerlink" title="_ 通配符"></a>_ 通配符</h2><p>但有时候我明明不用处理所有情况,但每次都要写上很麻烦怎么办?没事,Rust 作为一个现代语言还是准备了一些语法糖,那就是通配符 <code>_</code> 相当于 if-else 里面的 else。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">pluse_one</span>(x: <span class="type">Option</span><<span class="type">i32</span>>) <span class="punctuation">-></span> <span class="type">Option</span><<span class="type">i32</span>> {</span><br><span class="line"> <span class="keyword">match</span> x {</span><br><span class="line"> <span class="title function_ invoke__">Some</span>(i) => <span class="title function_ invoke__">Some</span>(i + <span class="number">1</span>),</span><br><span class="line"> _ => <span class="literal">None</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">num</span> = <span class="title function_ invoke__">Some</span>(<span class="number">5</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p><code>_</code> 可以匹配所有类型,所以得放最后,用于处理前面没有被处理过的情况。</p>
<p>但有时候我指向对一种情况处理怎么办?还是有点麻烦吧,Rust 还准备了 <code>if let</code> 语句</p>
<h1 id="简单控制流-if-let"><a href="#简单控制流-if-let" class="headerlink" title="简单控制流 if-let"></a>简单控制流 if-let</h1><p>我们就继续用美分硬币来举例吧,之前写的代码方便些。例如我们只想知道这个硬币是不是 Penny。</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Coin</span>{</span><br><span class="line"> Penny,</span><br><span class="line"> Nickel,</span><br><span class="line"> Dime,</span><br><span class="line"> Quarter,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">my_coin</span> = Coin::Penny;</span><br><span class="line"> <span class="keyword">match</span> my_coin{</span><br><span class="line"> Coin::Penny => <span class="built_in">println!</span>(<span class="string">"Lucky Penny!"</span>),</span><br><span class="line"> _ => <span class="built_in">println!</span>(<span class="string">"Not a penny"</span>),</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p>用 match 的话,就是要定义一个硬币,匹配这个硬币,再写个通配符来检测其他情况,标准流程是吧。但这样写或许繁琐了些,if let 就是这么一个一步到位的语法</p>
<div class="code-container" data-rel="Rust"><figure class="iseeu highlight rust"><table><tr><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Coin</span>{</span><br><span class="line"> Penny,</span><br><span class="line"> Nickel,</span><br><span class="line"> Dime,</span><br><span class="line"> Quarter,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">my_coin</span> = Coin::Penny;</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> <span class="variable">Coin</span>::Penny = my_coin {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"Lucky penny!"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></div>
<p><code>if let Coin::Penny</code> 就是要匹配的值,<code>my_coin</code>也就是你传入的值,用等号分隔开,如果两者相等的话,执行花括号内的语句。</p>
<p>当然,也可以用<code>else</code></p>