写好一个程序

写好一个程序最重要的是学会规范编程 规范编程可以提高代码的可读性、可维护性和可扩展性 换句话说,规范编程可以帮助别人(甚至是自己)快速理解代码的含义,让生产效率变快,同时也可以使软件维护成本降低,或者更快的和其他人对接

在规范编程时需要注意以下几个点:

可以看看这串有关求最大公约数的代码:

#include<iostream>
class
C1
{public:
	int
	func1(int s,int k) {
			while
	(k !=
	1) { int kyh = k;
			k =s    % k; s
	        = kyh;
	    }
	    return a;}};
	  int main(){
		  C1 qwert;std::cout <<c1. func1(10,
		  15)
		  << endl;
	return 0;
		  		  }

和这个:

#include <iostream>

class GCD {
public:
	int getGCD(int a, int b) {
		// Euclidean algorithm
		while (b != 0) {
			int temp = b;
			b = a % b;
			a = temp;
		}
		
		return a;
	}
};

int main(){
	// Initalize GCD class
	GCD gcd;
	// Output GCD of 10 and 15
	std::cout << gcd.getGCD(10, 15) << endl;
	return 0;
}

之间的区别 虽然两者都可以成功被电脑识别编译,运行的结果也是一样的,但是相较于前者,后者更容易被理解

那如何来实现这样的效果呢

缩进 & 空格 & 格式化 - Beautifying

格式化代码(beautify)是在书写代码的过程中对代码板书的调整 可以使用适当的缩进和空格来组织代码排版,使代码块清晰可辨

常见的格式化:

首先,成员、变量、或函数命名需要保证有意义 注意:尽量不要使用拼音作为名称!!!请使用正确的英文作为名称 正确命名: DoneWorkProgress 错误案例: DoWorkProgress (容易产设歧义,到底是要做的工作还是做完的工作) DoWorkProgess (Progress拼写有误) WanChengGongZuoLiang (如果后期需要外国人参与制作,ta有可能会读不懂,就算是中国人也得先拼读一下才能知道是什么意思) 完成工作量 (不要使用Unicode字符,可能会导致编码错误变成乱码) var111 (不要使用毫无意义的变量名,不然只有上帝和你看得懂,然后你忘了…)

假设现在需要命名变量: Foo Bar 一般来说,命名可以使用以下几种:

格式 名称 描述
foobar 平坦式(Flat Case) 全小写
FOOBAR 大写式(Upper Case) 全大写
fooBar 小驼峰(Camel Case) 首字母小写,其余每个单词开头字母大写
FooBar 大驼峰(Pascal Case) 每个单词开头字母大写
foo_bar 蛇形(Snake Case) 全小写,每个单词用_连接
FOO_BAR 大蛇形(Upper Snake Case) 全大写,每个单词用_连接
foo_Bar 小蛇峰形(Camel Snake Case) 首字母小写,其余每个单词开头字母大写,每个单词用_连接
Foo_Bar 大蛇峰形(Pascal Snake Case) 首字母大写,其余每个单词开头字母大写,每个单词用_连接
foo-bar 烤串式(Kebab Case) 全小写,每个单词用-连接
FOO-BAR 火车式(Train Case) 全大写,每个单词用-连接
Foo-Bar 未知 首字母大写,其余每个单词开头字母大写,每个单词用-连接

命名常用的方式是小驼峰式(Camel Case)大驼峰(Pascal Case)蛇形(Snake Case)大蛇形(Upper Snake Case),和烤串式(Kebab Case)

对于不同编程语言,有不同的规则:

C / C++

Tip: C#的命名一般遵循微软发布的.NET语言指南:Capitalization Conventions - Framework Design Guidelines | Microsoft Learn

Go

使用小驼峰或大驼峰命名

Java

在写代码的时候需要留下注释,帮助后人了解代码的功能 注释不仅要体现代码的功能,还要让读者理解代码在程序中的位置和作用:

def func(arg1, arg2):
	"""
	功能简介...
	参数介绍:
	- arg1: 介绍...
	- arg2: 介绍...
	返回值:
	介绍...
	示例:
	...
	"""
	
	# 函数体
	
	func2(1, arg2) # 重点标注逻辑难懂,或者容易引起混淆的代码

部分人习惯在注释符号前后加一个空格,这样可以让注释看起来不是很紧凑 在有大片注释的时候可以用缩进:

string str = "";                // declare & initalize str
int n;                          // declare n
cin >> n;                       // input n
for (int i = 0; i < n; i++) {   // repeat n times
	int c;
	cin >> c;
	if (c >= 'A' && c <= 'Z')
		str += ('a' - 'A') + c; // make c lower case if it is upper
	else str += c;              // or directly append it to str
}
cout << str;                    // outout str

还有一部分习惯用这种好看的注释方式:

/* This is a comment
 * 每条多行注释开始有一个*号
 * ...
 * 要结束了
 * ...
 */

注:大部分IDE拥有快速制作这种注释的快捷键

注意需要及时更新注释

文档 - Documenting

在发布软件后,一般会编写文档,一方面是帮助用户使用软件,另一方面可以帮助开发者更快上手编写代码

一般编写文档使用Markdown语言,你现在正在看的这份文章就是由Markdown制作的

可以考虑编写README或者搭建网站 (很多博客或者文档的网站都是由免费的GitHub Pages托管,包括这篇文章的网站)

文档一般分为两个种类,内部文档外部文档 内部文档一般是内部员工用来理解源代码的 (一些接口或类的用法),这种文档一般包含:

graph TD
    A[河里的未进化的水] --> B(抽水管道)
    B --> C(自来水厂净化装置)
    C --> D(加压泵)
    D --> E(抽水管道)
	E --> F(水龙头)
	F --> G[废水]
	G --> H(河流)
	H --> A

在这个过程中,每一个环节都是一个模块,这种模块化的设计方式有以下优点:

模块化的设计在面向对象语言和面向过程语言中很重要,程序员通过设计函数或类实现模块化:

单元测试 - Unit Test

单元测试 (也叫模块测试) 是针对模块的测试,一般来说在修改过一次模块代码后都会运行一次单元测试,确保每个模块达到对它预期的要求:

int CombineNumbers(int a, int b) {
    return a+b;
}

在这个例子中我们定义了一个CombineNumbers函数,用来将两个数字相加,假设这个函数就是需要测试的模块 CombineNumbers的单元测试应该是这样的

[TestMethod]
void CombineNumbers_CombineTwoNumbers_ReturnsSum() {
    Assert.AreEqual(CombineNumbers(5, 10), 15);
    Assert.AreEqual(CombineNumbers(1000, -100), 900);
}

Assert是你测试框架中的一部分 如果Assert.IsEqual的两个参数不相等,就无法通过单元测试,抛出异常导致程序测试失败 这样可以确保每次编译Release时,CombineNumbers在输入5, 10时会输出15,在输入1000和-100时会输出900。如果算法无法达到预期效果就不会被编译成功

一般编写单元测试时需要遵守以下BCDE原则:

单元测试有以下优点:

优秀的单元测试需要包含:

优秀的异常处理案例:

BSoD Windows最知名的错误提示: BSoD(蓝屏死机),会在系统层驱动或系统发生问题时显示。BSoD上标注了错误原因、系统的采取措施、和可能的处理方式,同时给出了错误代码,方便用户向开发者询问 (虽然没有多少人会去真正读它

反面教材《任务成功地失败了》:

Task Failed Successfully

简单的C#异常处理

try{
	// 运行代码
}
catch (Exception ex){
	MessageBox.Show(
		"在...期间遇到了问题: " + // 说明问题是在哪里发生的
		ex.Message +            // 给出错误原因
		"\n可能是因为..." +      // 给出可能的原因
		"\n请尝试...",          // 给出可能的解决方法
		"发生错误", MessageBoxButtons.OK, MessageBoxIcon.Error //弹窗标题,图标和按钮
	);
}

注意:一定要在可能出现异常的地方加上try

Tip: 现代IDE或者官方文档一般会说明方法可能抛出的异常: Visual Studio Dialog Official Documentation IDE和文档写明方法StreamReader.ReadToEnd可能抛出OutOfMemoryExceptionIOException

版本控制 - Version Control

版本控制也被称为源代码管理,或源控制 版本控制相当于每做一个功能就备份一次代码,这样如果哪个功能做废了可以一键回退;同时版本控制可以保证团队合作的源不冲突,换句话说: 假设你正在制作一个多人合作的项目,你需要你的同事A做一个模块,然后你会在前端接入这个模块,于是你向A发了一份你正在编写的源代码,A在收到后立刻就开始工作了 但是第二天你发现,之前写的若干个前端代码调用的接口不对,你需要修改代码才能让他们运作 你很快就修好了前端的代码,同时A也写好了接口,但是此时你们发现一个问题:

但如果是使用的Git进行开发,可以直接合并两个分支,假设A的版本叫patch-1,你的分支叫patch-2,那么只需要几行指令就可以了:

git checkout main    # Checkout主分支,也就是未修改的版本
git merge patch-1    # 合并两个分支,并进行一次commit,会自动进行合并
git merge patch-2    # 合并
git push origin main # 将新的main推送到存储库

在Git图像中是这样的:

gitGraph
	commit id: "之前的提交"
	commit id: "之前的提交2"
	branch patch-1
	checkout patch-1
	commit id: "新增接口的提交(A"
	checkout main
	branch patch-2
	checkout patch-2
	commit id: "修改前端的提交(你"
	checkout main
	merge patch-1 id: "合并(A的分支"
	merge patch-2 id: "合并(你的分支"
	commit id: "之后的提交"

patch-1是A的分支,patch-2是你的分支

如上文提到的,可以用Git这个工具进行版本控制,然后快速实现合并的操作

版本控制是多人合作或者大型项目重要的一环,通过创建远程仓库可以将代码托管在云端,一些网站比如最有名的GitHubGitLab等都是负责托管Git仓库的


总结

在写好一个程序这个问题上,代码层面的规范编写和程序层面的规范编写同样重要,如果两部分都能达到相应的要求,就能写好一个程序

参考文献