@@ -557,3 +557,323 @@ void compute()
557
557
#tip[对于没有返回值(返回类型为 `void`)的函数,可以省略 `return`。]
558
558
559
559
#warn[对于有返回值的函数,必须写 return 语句,否则程序出错。]
560
+
561
+ = 函数式编程
562
+
563
+ == 为什么需要函数?
564
+
565
+ ``` cpp
566
+ int main() {
567
+ std::vector<int> a = {1, 2, 3, 4};
568
+ int sum = 0;
569
+ for (int i = 0; i < a.size(); i++) {
570
+ sum += a[i];
571
+ }
572
+ fmt::println("sum = {}", sum);
573
+ return 0;
574
+ }
575
+ ```
576
+
577
+ 这是一个计算数组求和的简单程序。
578
+
579
+ 但是,他只能计算数组 a 的求和,无法复用。
580
+
581
+ 如果我们有另一个数组 b 也需要求和的话,就得把整个求和的 for 循环重新写一遍:
582
+
583
+ ``` cpp
584
+ int main() {
585
+ std::vector<int> a = {1, 2, 3, 4};
586
+ int sum = 0;
587
+ for (int i = 0; i < a.size(); i++) {
588
+ sum += a[i];
589
+ }
590
+ fmt::println("sum of a = {}", sum);
591
+ std::vector<int> b = {5, 6, 7, 8};
592
+ sum = 0;
593
+ for (int i = 0; i < a.size(); i++) {
594
+ sum += b[i];
595
+ }
596
+ fmt::println("sum of b = {}", sum);
597
+ return 0;
598
+ }
599
+ ```
600
+
601
+ 这就出现了程序设计的大忌:代码重复。
602
+
603
+ #fun[例如,你有吹空调的需求,和充手机的需求。你为了满足这两个需求,购买了两台发电机,分别为空调和手机供电。第二天,你又产生了玩电脑需求,于是你又购买一台发电机,专为电脑供电……]
604
+
605
+ 重复的代码不仅影响代码的*可读性*,也增加了*维护*代码的成本。
606
+
607
+ + 看起来乱糟糟的,信息密度低,让人一眼看不出代码在干什么的功能
608
+ + 很容易写错,看走眼,难调试
609
+ + 复制粘贴过程中,容易漏改,比如这里的 `sum += b[i]` 可能写成 `sum += a[i]` 而自己不发现
610
+ + 改起来不方便,当我们的需求变更时,需要多处修改,比如当我需要改为计算乘积时,需要把两个地方都改成 `sum *=`
611
+ + 改了以后可能漏改一部分,留下 Bug 隐患
612
+ + 敏捷开发需要反复修改代码,比如你正在调试 `+=` 和 `-=` 的区别,看结果变化,如果一次切换需要改多处,就影响了调试速度
613
+
614
+ === 狂想:没有函数的世界?
615
+
616
+ 如果你还是喜欢“一本道”写法的话,不妨想想看,完全不用任何标准库和第三方库的函数和类,把 `fmt::println` 和 `std::vector` 这些函数全部拆解成一个个系统调用。那这整个程序会有多难写?
617
+
618
+ ``` cpp
619
+ int main() {
620
+ #ifdef _WIN32
621
+ int *a = (int * )VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
622
+ #else
623
+ int *a = (int * )mmap(NULL, 4 * sizeof(int), PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
624
+ #endif
625
+ a[0] = 1;
626
+ a[1] = 2;
627
+ a[2] = 3;
628
+ a[3] = 4;
629
+ int sum = 0;
630
+ for (int i = 0; i < 4; i++) {
631
+ sum += a[i];
632
+ }
633
+ char buffer[64];
634
+ buffer[0] = 's';
635
+ buffer[1] = 'u';
636
+ buffer[2] = 'm';
637
+ buffer[3] = ' ';
638
+ buffer[4] = '=';
639
+ buffer[5] = ' '; // 例如,如果要修改此处的提示文本,甚至需要修改后面的 len 变量...
640
+ int len = 6;
641
+ int x = sum;
642
+ do {
643
+ buffer[len++] = '0' + x % 10;
644
+ x /= 10;
645
+ } while (x);
646
+ buffer[len++] = '\n';
647
+ #ifdef _WIN32
648
+ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buffer, len, NULL, NULL);
649
+ #else
650
+ write(1, buffer, len);
651
+ #endif
652
+ int *b = (int * )a;
653
+ b[0] = 4;
654
+ b[1] = 5;
655
+ b[2] = 6;
656
+ b[3] = 7;
657
+ int sum = 0;
658
+ for (int i = 0; i < 4; i++) {
659
+ sum += b[i];
660
+ }
661
+ len = 6;
662
+ x = sum;
663
+ do {
664
+ buffer[len++] = '0' + x % 10;
665
+ x /= 10;
666
+ } while (x);
667
+ buffer[len++] = '\n';
668
+ #ifdef _WIN32
669
+ WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buffer, len, NULL, NULL);
670
+ #else
671
+ write(1, buffer, len);
672
+ #endif
673
+ #ifdef _WIN32
674
+ VirtualFree(a, 0, MEM_RELEASE);
675
+ #else
676
+ munmap(a);
677
+ #endif
678
+ return 0;
679
+ }
680
+ ```
681
+
682
+ 不仅完全没有可读性、可维护性,甚至都没有可移植性。
683
+
684
+ 除非你只写应付导师的“一次性”程序,一旦要实现复杂的业务需求,不可避免的要自己封装函数或类。网上所有鼓吹“不封装”“设计模式是面子工程”的反智言论,都是没有做过大型项目的。
685
+
686
+ === 设计模式追求的是“可改”而不是“可读”!
687
+
688
+ 很多设计模式教材片面强调*可读性*,仿佛设计模式就是为了“优雅”“高大上”“美学”?使得很多人认为,“我这个是自己的项目,不用美化给领导看”而拒绝设计模式。实际上设计模式的主要价值在于*方便后续修改*!
689
+
690
+ #fun[例如 B 站以前只支持上传普通视频,现在叔叔突然提出:要支持互动视频,充电视频,视频合集,还废除了视频分 p,还要支持上传短视频,竖屏开关等……每一个叔叔的要求,都需要大量程序员修改代码,无论涉及前端还是后端。]
691
+
692
+ 与建筑、绘画等领域不同,一次交付完毕就可以几乎永久使用。而软件开发是一个持续的过程,每次需求变更,都导致代码需要修改。开发人员几乎需要一直围绕着软件代码,不断的修改。调查表明,程序员 90% 的时间花在*改代码*上,*写代码*只占 10%。
693
+
694
+ #fun[软件就像生物,要不断进化,软件不更新不维护了等于死。而若你的代码逐渐变得臃肿难以修改,无法适应新需求,那你的代码就像已经失去进化能力的生物种群,如《三体》世界观中“安置”到澳大利亚保留区里“绝育”的人类,被淘汰只是时间问题。]
695
+
696
+ 如果我们能在*写代码*阶段,就把程序准备得*易于后续修改*,那就可以在后续 90% 的*改代码*阶段省下无数时间。
697
+
698
+ 如何让代码易于修改?前人总结出一系列常用的写法,这类写法有助于让后续修改更容易,各自适用于不同的场合,这就是设计模式。
699
+
700
+ 提升可维护性最基础的一点,就是避免重复!
701
+
702
+ 当你有很多地方出现重复的代码时,一旦需要涉及修改这部分逻辑时,就需要到每一个出现了这个逻辑的代码中,去逐一修改。
703
+
704
+ #fun[例如你的名字,在出生证,身份证,学生证,毕业证,房产证,驾驶证,各种地方都出现了。那么你要改名的话,所有这些证件都需要重新印刷!如果能把他们合并成一个“统一证”,那么只需要修改“统一证”上的名字就行了。]
705
+
706
+ 不过,现实中并没有频繁改名字的需求,这说明:
707
+
708
+ - 对于不常修改的东西,可以容忍一定的重复。
709
+ - 越是未来有可能修改的,就越需要设计模式降重!
710
+
711
+ 例如数学常数 PI = 3.1415926535897,这辈子都不可能出现修改的需求,那写死也没关系。如果要把 PI 定义成宏,只是出于“记不住”“写起来太长了”“复制粘贴麻烦”。所以对于 PI 这种不会修改的东西,降重只是增加*可读性*,而不是*可修改性*。
712
+
713
+ #tip[但是,不要想当然!需求的千变万化总是超出你的想象。]
714
+
715
+ 例如你做了一个“愤怒的小鸟”游戏,需要用到重力加速度 g = 9.8,你想当然认为 g 以后不可能修改。老板也信誓旦旦向你保证:“没事,重力加速度不会改变。”你就写死在代码里了。
716
+
717
+ 没想到,“愤怒的小鸟”老板突然要求你加入“月球章”关卡,在这些关卡中,重力加速度是 g = 1.6。
718
+
719
+ 如果你一开始就已经把 g 提取出来,定义为常量:
720
+
721
+ ``` cpp
722
+ struct Level {
723
+ const double g = 9.8;
724
+
725
+ void physics_sim() {
726
+ bird.v = g * t; // 假装这里是物理仿真程序
727
+ pig.v = g * t; // 假装这里是物理仿真程序
728
+ }
729
+ };
730
+ ```
731
+
732
+ 那么支持月球关卡,只需要修改一处就可以了。
733
+
734
+ ``` cpp
735
+ struct Level {
736
+ double g;
737
+
738
+ Level(Chapter chapter) {
739
+ if (chapter == ChapterMoon) {
740
+ g = 1.6;
741
+ } else {
742
+ g = 9 .8 ;
743
+ }
744
+ }
745
+
746
+ void physics_sim() {
747
+ bird.v = g * t; // 无需任何修改,自动适应了新的非常数 g
748
+ pig.v = g * t; // 无需任何修改,自动适应了新的非常数 g
749
+ }
750
+ };
751
+ ```
752
+
753
+ #fun[小彭老师之前做 zeno 时,询问要不要把渲染管线节点化,方便用户动态编程?张猩猩就是信誓旦旦道:“渲染是一个高度成熟领域,不会有多少修改需求的。”小彭老师遂写死了渲染管线,专为性能极度优化,几个月后,张猩猩羞答答找到小彭老师:“小彭老师,那个,渲染,能不能改成节点啊……”。这个故事告诉我们,甲方的信誓旦旦放的一个屁都不能信。]
754
+
755
+ === 用函数封装
756
+
757
+ 函数就是来帮你解决代码重复问题的!要领:
758
+
759
+ *把共同的部分提取出来,把不同的部分作为参数传入。*
760
+
761
+ ``` cpp
762
+ void sum(std::vector<int> const &v) {
763
+ int sum = 0;
764
+ for (int i = 0; i < v.size(); i++) {
765
+ sum += v[i];
766
+ }
767
+ fmt::println("sum of v = {}", sum);
768
+ }
769
+
770
+ int main() {
771
+ std::vector<int> a = {1, 2, 3, 4};
772
+ sum(a);
773
+ std::vector<int> b = {5, 6, 7, 8};
774
+ sum(b);
775
+ return 0;
776
+ }
777
+ ```
778
+
779
+ 这样 main 函数里就可以只关心要求和的数组,而不用关心求和具体是如何实现的了。事后我们可以随时把 sum 的内容偷偷换掉,换成并行的算法,main 也不用知道。这就是*封装*,可以把重复的公共部分抽取出来,方便以后修改代码。
780
+
781
+ #fun[sum 函数相当于,当需要吹空调时,插上空调插座。当需要给手机充电时,插上手机充电器。你不需要关心插座里的电哪里来,“国家电网”会替你想办法解决,想办法优化,想办法升级到绿色能源。你只需要吹着空调给你正在开发的手机 App 优化就行了,大大减轻程序员心智负担。]
782
+
783
+ === 要封装,但不要耦合
784
+
785
+ 但是!这段代码仍然有个问题,我们把 sum 求和的结果,直接在 sum 里打印了出来。sum 里写死了,求完和之后只能直接打印,调用者 main 根本无法控制。
786
+
787
+ 这是一种错误的封装,或者说,封装过头了。
788
+
789
+ #fun[你把手机充电器 (fmt::println) 焊死在了插座 (sum) 上,现在这个插座只能给手机充电 (用于直接打印) 了,不能给笔记本电脑充电 (求和结果不直接用于打印) 了!尽管通过更换充电线 (参数 v),还可以支持支持安卓 (a) 和苹果 (b) 两种手机的充电,但这样焊死的插座已经和笔记本电脑无缘了。]
790
+
791
+ === 每个函数应该职责单一,别一心多用
792
+
793
+ 很明显,“打印”和“求和”是两个独立的操作,不应该焊死在一块。
794
+
795
+ sum 函数的本职工作是“数组求和”,不应该附赠打印功能。
796
+
797
+ sum 计算出求和结果后,直接 return 即可。
798
+
799
+ 如何处理这个结果,是调用者 main 的事,正如“国家电网”不会管你用他提供的电来吹空调还是玩游戏一样,只要不妨碍到其他居民的正常用电。
800
+
801
+ ``` cpp
802
+ int sum(std::vector<int> const &v) {
803
+ int sum = 0;
804
+ for (int i = 0; i < v.size(); i++) {
805
+ sum += v[i];
806
+ }
807
+ return sum;
808
+ }
809
+
810
+ int main() {
811
+ std::vector<int> a = {1, 2, 3, 4};
812
+ fmt::println("sum of a = {}", sum(a));
813
+ std::vector<int> b = {5, 6, 7, 8};
814
+ fmt::println("sum of b = {}", sum(b));
815
+ return 0;
816
+ }
817
+ ```
818
+
819
+ 这就是设计模式所说的*职责单一原则*。
820
+
821
+ === 二次封装
822
+
823
+ 假设我们要计算一个数组的平均值,可以再定义个函数 average,他可以基于 sum 实现:
824
+
825
+ ``` cpp
826
+ int sum(std::vector<int> const &v) {
827
+ int sum = 0;
828
+ for (int i = 0; i < v.size(); i++) {
829
+ sum += v[i];
830
+ }
831
+ return sum;
832
+ }
833
+
834
+ int average(std::vector<int> const &v) {
835
+ return sum(v) / v.size();
836
+ }
837
+
838
+ int main() {
839
+ std::vector<int> a = {1, 2, 3, 4};
840
+ fmt::println("average of a = {}", average(a));
841
+ std::vector<int> b = {5, 6, 7, 8};
842
+ fmt::println("average of b = {}", average(b));
843
+ return 0;
844
+ }
845
+ ```
846
+
847
+ == 为什么需要函数式?
848
+
849
+ == 函数对象
850
+
851
+ == bind
852
+
853
+ ``` cpp
854
+ int hello(int x, int y) {
855
+ fmt::println("hello({}, {})", x, y);
856
+ return x + y;
857
+ }
858
+
859
+ int main() {
860
+ fmt::println("main 调用 hello(2, 3) 结果:{}", hello(2, 3));
861
+ fmt::println("main 调用 hello(2, 4) 结果:{}", hello(2, 4));
862
+ fmt::println("main 调用 hello(2, 5) 结果:{}", hello(2, 5));
863
+ return 0;
864
+ }
865
+ ```
866
+
867
+ ``` cpp
868
+ int hello(int x, int y) {
869
+ fmt::println("hello({}, {})", x, y);
870
+ return x + y;
871
+ }
872
+
873
+ int main() {
874
+ fmt::println("main 调用 hello2(3) 结果:{}", hello2(3));
875
+ fmt::println("main 调用 hello2(4) 结果:{}", hello2(4));
876
+ fmt::println("main 调用 hello2(5) 结果:{}", hello2(5));
877
+ return 0;
878
+ }
879
+ ```
0 commit comments