Skip to content
On this page

编程范式

Intro

编程语言是一个很自由的工具,学习的过程中,我们往往更关注于语言的词法元素、语法特性等等,了解使用这门编程语言如果解决日常的编程问题,甚至会使用不同的语言去解决相同的算法问题等等。

不同的编程语言固然有不同的特性,但是大体上来说语言背后的思想或者套路总是相似的,所以就会发现同样的算法使用某一部分语言所实现的效果,或多或少都有那么一点相似性,这种套路或者思想便可称为编程范式。

依据日常的划分和解释,编程范式可以划分为了指令式、过程式、函数式、逻辑式、面向对象、元编程、泛型、声明式等等众多的类型,部分类型下面还有子类型,例如最常见的面向对象就包含基于类、基于原型等等。

如今的编程语言几乎没有单纯范式的,基本都是多范式,例如C# 就支持结构化、面向对象、函数式、泛型、元编程等多种编程范式,这其中还是因为编程范式并不是编程语言的指导规范和执行标准,只是一个编程方法的心法套路而已。

所有的编程范式、设计模式、设计原则等形式方法的东西,都没有提供创造,而是提供了限制。 这些限制让你不得不遵循某个特定的形式去做出实现。

虽然不遵循这种指导和形式也能实现相应的功能,甚至会更加快捷,但是并不一定好维护、也不一定通用,甚至不一定稳定。

学习编程的过程就是不断学习限制的过程,软件工程也是一个充满约束和规定的工程学科。这种限制和约束也正是软件可以长期稳定运行的基本保证。

所以从最基本的目的来说,学习编程范式可以你让少写BUG。

由于编程范式分类并不统一,所以对于它的解读也往往没有标准的定论和明确的边界,例如在《架构整洁之道》中,就只认定了结构化编程(1968年)、面向对象编程(1966年)和函数式编程(1936年)这三种的编程范式。

我们也从这这个角度去深入的了解这三种编程范式。

在《架构整洁之道》中的终极定义是:
● 结构化编程是对程序控制权的直接转移的限制。
● 面向对象编程是对程序控制权的间接转移的限制。
● 函数式编程是对程序中赋值操作的限制。

结构化编程

结构化编程始于20世纪60年代的结构化程序设计运动,程序员们意识到指令式代码难以阅读和理解,尤其是goto语句的滥用。
虽然在现在的部分编程语言中还可以看到goto,但是其影响力远不如从前了。现在所能看到的对于goto评价,更多的则是禁术、饱受诟病、尽量避免等等。

c
#include <stdio.h>

int main(void) {
  int num;
  
  printf("Enter a number: ");
  scanf("%d", &num);
  
  if (num < 0) {
    goto error;
  }
  
  printf("The number is positive\\n");
  
  goto end;
  
error:
  printf("The number is negative\\n");
  
end:
  return 0;
}

goto的滥用导致程序的可读性变差,甚至不可维护。
1966年科拉多·伯姆及朱塞佩·贾可皮尼发表论文,指出任何还有goto指令的程序可以改为完全不使用goto指令的程序,其中证明了人们可以使用顺序结构、分支结构、循环结构这三种结构构造出任何程序。

c
#include <stdio.h>

int main(void) {
  int num;
  
  printf("Enter a number: ");
  scanf("%d", &num);
  
  if (num < 0) {
    printf("The number is negative\\n");
  } else {
    printf("The number is positive\\n");
  }
  
  return 0;
}

1968年迪杰斯特拉发布了一篇名为《GOTO 语句有害论》的文章,由此结构化编程开始盛行。
结构化编程范式,可以将模块递归降解拆分为更小的单元。这就意味着,我们可以将一个大型问题拆分成一系列高级函数的组合,而这些高级函数各自有可以继续拆分成为一系列低级函数,如此无限递归。更重要的是,每个被拆分出来的函数也都可以用结构化编程范式来书写。

简单的说,结构化编程就是以模块结构的形式去组织代码。
回过头来再看 “结构化编程是对程序控制权的直接转移的限制。”这句话。
限制了goto语句的使用,基本就杜绝了程序控制权的任意转移,程序的流程控制更加的清晰,容易理解和维护。

面向对象编程

和编程范式一样,面向对象的准确定义和其本义存在着不少的争论。

常见的说法,像是“数据与函数的组合”,或者是“对真实世界进行建模的方式”,但是都没法很好的解释面向对象理论的诞生。

问题又回到了1960年代,软件领域的面临的最大的危机就是软件的可维护性。

  • 1960年Simula 67语言引入对象作为编程实体,引入了“类” 和 “实例” 等概念。
  • 70年代Smalltalk语言实现了完全动态的对象,可以被创建、修改和销毁,同时引入了继承性的思想。
  • 80年代,C++诞生,面向对象程序设计成为主导思想。
  • 随后,大部分的编程语言都加入了面向对象的特性,90年代 Java诞生。

从整个发展过程来看,面向对象的思想也是在不断完善和改进的,我们能够确认的是,它的确是解决代码可维护性的一个重要方法。

由面向对象衍生出来的设计原则和设计模式,如今成为了重要的编程理念。

细说常见的面向对象的三大特性:

封装 :

通常认为的封装来自于访问限制修饰符(private,protect,public)之类,有些语言则使用特定的命名方式来区分不同的访问限制,这些都是在语言层面一些约定俗成。

但是从代码层面来看,封装就意味着其对外不可知,只要达到这个效果,无论是否采用面向对象的方法,都可以称之为封装。

csharp
public class Student
{
    public string Name { get; set;}
		public int Age { get; set; }
		public int StudentId { get; set;}
}

public class Student
{
    private string name;
    private int age;
    private int studentId;

    // 属性:姓名
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    // 属性:年龄
    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    // 属性:学号
    public int StudentId
    {
        get { return studentId; }
        set { studentId = value; }
    }
}

继承 :

继承通常的作用是为了代码复用。在面向对象的方法中,经常出现在子类继承父类的方法,父类实现过之后,作为子类可以直接使用,而不必重复实现,同时也降低的维护的成本。

类似的方法还有泛型。

csharp
public class Dog 
{
     public void Bark() { ...... }
}
// 中华田园犬
public class ChineseRuralDog : Dog 
{
}
// 柯基犬
public class CorgiDog : Dog 
{
}

多态:

和继承相反,多态则是呈现不同子类同一行为的不同表现,表现差异性。

多态最强大的地方就是可以实现依赖反转的效果。高层代码只需要依赖上层抽象,而不需要关心底层实现。

csharp
public class Dog 
{
     public virtual void Bark() { ...... }
}
// 中华田园犬
public class ChineseRuralDog : Dog 
{
    public override void Bark() 
		{
				Console.WriteLine("汪汪汪");
		}
}
// 柯基犬
public class CorgiDog : Dog 
{
		public override void Bark() 
		{
				Console.WriteLine("woof woof woof");
		}
}

面向对象的核心就是抽象性。
简单的说,面向对象编程就是以对象的形式去组织代码。

然后再看"面向对象编程是对程序控制权的间接转移的限制。"这句话。

程序控制权的间接转移就是指通过对象间的方法调用来实现程序的执行流程,得益于面向对象对于状态和函数的封装,通过继承和多态使得程序的控制转移更加的安全和方便。

函数式编程

函数式编程的原理甚至早于编程本身。

  • 最早在20世纪30年代,邱奇发明了lambda演算。
  • 1937年的时候图灵证明了lambda演算和图灵机是等价的计算模型。
  • 20世纪50年代,麦卡锡开发了最早的函数式编程语言LISP。

在函数式编程语言中,函数是一等公民,也就意味着函数可以输入参数,也可以作为输出的返回值,也可以作为变量,被修改和分配给一个变量。

纯函数:

给定相同的输入总是会返回相同的输出,不会产生副作用。
这样就可以使代码可预测、可理解、可维护,更容易进行并发编程和测试。

不可变性:

函数内不存在可变量。也就意味着,函数执行期间的变量是不可变更的。
其实所有的竞争问题、死锁问题、并发问题都是由可变量导致的。如果变量不会发生变化,那么这些问题就迎刃而解了。

高阶函数:

接受函数作为参数或者返回函数作为结果的函数。其实就是函数一等公民的通常形式。 将函数作为参数,其实将部分的函数的控制权进行了抽象,就可以写出更加抽象灵活的代码。

C# 中实现高阶函数的示例如下:

csharp
public delegate int Transformer(int v);

public int Square(int s) => s * s;

public int[] Transform(int[] sources,Transformer transformer) {
	var result = new int[sources.Length];
	for (int i = 0; i < sources.Length; i++)
	{
		result[i]= transformer(sources[i]);
	}
	return result;
}

var lst = Enumerable.Range(0,10).ToArray();
Transform(lst,Square);

Transform(lst,s=>s*3);

通常使用泛型委托可以简化以上代码:

csharp
public int[]  Transform2(int[] sources, Func<int,int> transformer) 
	=> sources.Select(transformer).ToArray();

var lst2 = Enumerable.Range(0,10).ToArray();
Transform(lst2,s=> s*s);

简单的说,函数式编程就是以函数的形式组织代码。
然后再看“函数式编程是对程序中赋值操作的限制。”这句话。

函数式编程从根本定义上限制了赋值操作的存在,所有的函数、变量都是可以进行形式化的推导和证明的。其好处在于可以避免变量赋值带来的竞态问题,极大概率的减少相关bug的存在。而其坏处也在于没有了赋值语句,就需要分配大量的内存用于变量保存,这就要求编程语言本身需要有个强大的GC机制。

More

程序的本质是什么?

Programs = Algorithms + Data Structures
Algorithm = Logic + Control

程序 = 算法 + 数据结构
算法 = 逻辑 + 控制

我们的算法逻辑由两部分组成,一部分是业务逻辑,对应着业务逻辑代码,另一部分是控制逻辑,对应着控制程序的代码。业务关心的只有业务逻辑部分的代码,不关心控制代码。

控制部分用来描述如何使用逻辑。最粗略的看法可以认为“控制”是解决问题的策略,而不会改变算法的意义,因为算法的意义是由逻辑决定的。对同一个逻辑,使用不同控制,所得到的算法,本质是等价的,因为它们解决同样的问题,并得到同样的结果。

有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!

参考文献

Date: 2022/12/30

Authors: 郭强

Tags: 基础原理