zoukankan      html  css  js  c++  java
  • 编程范式游记(1) 起源

    现在很多的文章和演讲都在谈架构,很少有人再会谈及编程范式。然而, 这些基础性和本质性的话题,却是非常非常重要的。一方面,我发现在一些语言争论上,有很多人对编程语言的认识其实并不深;另一方面,通过编程语言的范式,我们不但可以知道整个编程语言的发展史,而且还能提高自己的编程技能,写出更好的代码。

    这一经历可能有些漫长,途中也会有各式各样的语言的代码。但是我保证这一历程对于一个程序员来说是非常有价值的,因为你不但可以对主流编程语言的一些特性有所了解,而且当我们到达终点的时候,你还能了解到编程的本质是什么。这一系列文章中有各种语言的代码,其中有 C、C++、Python、Java、Scheme、Go、JavaScript、Prolog 等。
    所以,如果要能跟上本文的前因后果,你要对这几门比较主流的语言多少有些了解。而且,你需要在一线编写一段时间(大概 5 年以上吧)的代码,可能才能体会到这一系列文章的内涵。

    第一部分:泛型编程,第 1~3 章,讨论了从 C 到 C++ 的泛型编程方法,并系统地总结了编程语言中的类型系统和泛型编程的本质。
    第二部分:函数式编程,第 4 章和第 5 章,讲述了函数式编程用到的技术,及其思维方式,并通过 Python 和 Go 修饰器的例子,展示了函数式编程下的代码扩展能力,以及函数的相互和随意拼装带来的好处。
    第三部分:面向对象编程,第 6~8 章,讲述与传统的编程思想的相反之处,面向对象设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,列举了面向对象编程的优缺点,基于原型的编程范式,以及 Go 语言的委托模式。
    第四部分:编程本质和逻辑编程,第 9~11 章,先探讨了编程的本质:逻辑部分才是真正有意义的,控制部分只能影响逻辑部分的效率,然后结合 Prolog 语言介绍了逻辑编程范式,最后对程序世界里的编程范式进行了总结,对比了它们之间的不同。
    

    下面我们来说说什么是编程范式。编程范式的英语是 Programming Paradigm,范即模范之意,范式即模式、方法,是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照“方法学”一词)。编程语言发展到今天,出现了好多不同的代码编写方式,但不同的方式解决的都是同一个问题,那就是如何写出更为通用、更具可重用性的代码或模块。如果你准备好了,就和我一起来吧。

    先从 C 语言开始

    为了讲清楚这个问题,我需要从 C 语言开始讲起。因为 C 语言历史悠久,而几乎现在看到的所有编程语言都是以 C 语言为基础来拓展的,不管是 C++、Java、C#、Go、Python、PHP、Perl、JavaScript、Lua,还是 Shell。

    自 C 语言问世 40 多年以来,其影响了太多太多的编程语言,到现在还一直被广泛使用,不得不佩服它的生命力。但是,我们也要清楚地知道,大多数 C Like 编程语言其实都是在改善 C 语言带来的问题。

    那 C 语言有哪些特性呢?我简单来总结下:

    1.C 语言是一个静态弱类型语言,在使用变量时需要声明变量类型,但是类型间可以有隐式转换;
    2.不同的变量类型可以用结构体(struct)组合在一起,以此来声明新的数据类型;
    3.C 语言可以用 typedef 关键字来定义类型的别名,以此来达到变量类型的抽象;
    4.C 语言是一个有结构化程序设计、具有变量作用域以及递归功能的过程式语言;
    5.C 语言传递参数一般是以值传递,也可以传递指针;
    6.通过指针,C 语言可以容易地对内存进行低级控制,然而这加大了编程复杂度;
    7.编译预处理让 C 语言的编译更具有弹性,比如跨平台。
    

    C 语言的这些特性,可以让程序员在微观层面写出非常精细和精确的编程操作,让程序员可以在底层和系统细节上非常自由、灵活和精准地控制代码。然而,在代码组织和功能编程上,C 语言的上述特性,却不那么美妙了。

    从 C 语言的一个简单例子说起
    我们从 C 语言最简单的交换两个变量的 swap 函数说起,参看下面的代码:

    void swap(int* x, int* y)
    {
      int tmp = *x;
      *x = *y;
      *y = tmp;
    }
    

    你可以想一想,这里为什么要传指针?这里是 C 语言指针,因为如果你不用指针的话,那么参数变成传值,即函数的形参是调用实参的一个拷贝,函数里面对形参的修改无法影响实参的结果。为了要达到调用完函数后,实参内容的交换,必须要把实参的地址传递进来,也就是传指针。这样在函数里面做交换,实际变量的值也被交换了。

    然而,这个函数最大的问题就是它只能给 int 值用,这个世界上还有很多类型包括 double、float,这就是静态语言最糟糕的一个问题。

    数据类型与现实世界的类比

    与现实世界类比一下,数据类型就好像螺帽一样,有多种接口方式:平口的、十字的、六角的等,而螺丝刀就像是函数,或是用来操作这些螺丝的算法或代码。我们发现,这些不同类型的螺帽(数据类型),需要我们为之适配一堆不同的螺丝刀。

    而且它们还有不同的尺寸(尺寸就代表它是单字节的,还是多字节的,比如整型的 int、long,浮点数的 float 和 double),这样复杂度一下就提高了,最终导致电工(程序员)工作的时候需要带下图这样的一堆工具。

    这就是类型为编程带来的问题。要解决这个问题,我们还是来看一下现实世界。
    你应该见过下面图片中的这种经过优化的螺丝刀,上面手柄是一样的,拧螺丝的动作也是一样的,只是接口不一样。每次我看到这张图片的时候就在想,这密密麻麻的看着有 40 多种接口,不知道为什么人类世界要干出这么多的花样,你们这群人类究竟是要干什么啊。

    我们可以看到,无论是传统世界,还是编程世界,我们都在干一件事情,什么事呢?那就是通过使用一种更为通用的方式,用另外的话说就是抽象和隔离,让复杂的“世界”变得简单一些。

    然而,要做到抽象,对于 C 语言这样的类型语言来说,首先要拿出来讲的就是抽象类型,这就是所谓的泛型编程。

    另外,我们还要注意到,在编程世界里,对于 C 语言来说,类型还可以转换。编译器会使用一切方式来做类型转换,因为类型转换有时候可以让我们编程更方便一些,也让相近的类型可以做到一点点的泛型。

    然而,对于 C 语言的类型转换,是会出很多问题的。比如说,传给我一个数组,这个数组本来是 double 型的,或者是 long 型 64 位的,但是如果把数组类型强转成 int,那么就会出现很多问题,因为这会导致程序遍历数组的步长不一样了。

    比如:一个 double a[10] 的数组,a[2] 意味着 a + sizeof(double) * 2。如果你把 a 强转成 int,那么 a[2] 就意味着 a + sizeof(int) * 2。我们知道 sizeof(double) 是 8,而 sizeof(int) 是 4。于是访问到了不同的地址和内存空间,这就导致程序出现严重的问题。

    C 语言的泛型

    一个泛型的示例 - swap 函数

    好了,我们再看下,C 语言是如何实现泛型的。C 语言的类型泛型基本上来说就是使用void 关键字或是使用宏定义。下面是一个使用了void泛型版本的 swap 函数。

    void swap(void* x, void* y, size_t size)
    {
         char tmp[size];
         memcpy(tmp, y, size);
         memcpy(y, x, size);
         memcpy(x, tmp, size);
    }
    

    上面这个函数几乎完全改变了 int 版的函数的实现方式,这个实现方式有三个重点:

    1.函数接口中增加了一个size参数。为什么要这么干呢?因为,用了 void* 后,类型被“抽象”掉了,编译器不能通过类型得到类型的尺寸了,所以,需要我们手动地加上一个类型长度的标识。
    
    2.函数的实现中使用了memcpy()函数。为什么要这样干呢?还是因为类型被“抽象”掉了,所以不能用赋值表达式了,很有可能传进来的参数类型还是一个结构体,因此,为了要交换这些复杂类型的值,我们只能使用内存复制的方法了。
    
    3.函数的实现中使用了一个temp[size]数组。这就是交换数据时需要用的 buffer,用 buffer 来做临时的空间存储。
    

    于是,新增的size参数,使用的memcpy内存拷贝以及一个 buffer,这增加了编程的复杂度。这就是 C 语言的类型抽象所带来的复杂度的提升。
    在提升复杂度的同时,我们发现还有问题,比如,我们想交换两个字符串数组,类型是char*,那么,我的swap()函数的x和y参数是不是要用void**了?这样一来,接口就没法定义了。

    除了使用 void* 来做泛型,在 C 语言中,还可以用宏定义来做泛型,如下所示:

    #define swap(x, y, size) {\
      char temp[size]; \
      memcpy(temp, &y, size); \
      memcpy(&y,   &x, size); \
      memcpy(&x, temp, size); \
    }
    

    但用宏带来的问题就是编译器做字符串替换,因为宏是做字符串替换,所以会导致代码膨胀,导致编译出的执行文件比较大。不过对于 swap 这个简单的函数来说,用void*和宏替换来说都可以达到泛型。

    但是,如果我们不是 swap,而是 min() 或 max() 函数,那么宏替换的问题就会暴露得更多一些。比如,对于下面的这个宏:

    #define min(x, y)  ((x)>(y) ? (y) : (x))
    

    其中一个最大的问题,就是有可能会有重复执行的问题。如:

    min(i++, j++) 对于这个案例来说,我们本意是比较完后,对变量做累加,但是,因为宏替换的缘故,这会导致变量i或j被累加两次。
    
    min(foo(), bar()) 对于这个示例来说,我们本意是比较 foo() 和 bar() 函数的返回值,然而,经过宏替换后,foo() 或 bar() 会被调用两次,这会带来很多问题。
    

    另外,你会不会觉得无论是用哪种方式,这种“泛型”是不是太宽松了一些,完全不做类型检查,就是在内存上对拷,直接操作内存的这种方式,感觉是不是比较危险,而且就像一个定时炸弹一样,不知道什么时候,在什么条件下就爆炸了。

    从上面的两个例子,我们可以发现,无论哪种方式,接口都变得复杂了——加入了size,因为如果不加入size的话,那么我们的函数内部就需要自己检查size。然而,void* 这种地址的方式是没法得到size的。

    而宏定义的那种方式,虽然不会把类型给隐藏掉,可以使用像 sizeof(x) 这样的方式得到 size。但是如果类型是 char*,那么,使用sizeof方式只能提到指针类型的size,而不是值的size。另外,对于不同的类型,比如说double和int,那应该用谁的size呢?是不是先转一下型呢?这些都是问题。

    于是,这种泛型,让我们根本没有办法检查传入参数的size,导致我们只能增加接口复杂度,加入一个size参数,然后把这个问题抛给调用者了。

    一个更为复杂的泛型示例 - Search 函数

    如果我们把这个事情变得更复杂,写个search函数,再传一个int数组,然后想搜索target,搜到返回数组下标,搜不到返回-1。

  • 相关阅读:
    js 函数柯里化和闭包的使用
    人员轨迹运动效果
    D3绘制柱状图
    D3选择元素和绑定数据
    h5--uni.setNavigationBarColor 动态修改顶部背景颜色
    友链
    canvas 整个透明
    JS将某个数组分割为N个对象一组(如,两两一组,三三一组等)
    小程序正则表达式
    微信小程序--设置和获取剪切板内容
  • 原文地址:https://www.cnblogs.com/ukzq/p/15641825.html
Copyright © 2011-2022 走看看