变量的命名入门

大家先来试着理解一下这段代码:

var todoList = new TodoList();
todoList.Todos = new List<Todo>();

var todo = new Todo() { Id = 0, Finished = false, Content = "测试" };

todoList.Todos.Add(todo)

todo.Finished = true;

代码本身很简单,就算不用去看 TodoList 类和 Todo 类的定义,也是可以读懂以上代码的。

那这是如何做到的呢?

答案就是:进行合适的命名。

而达到以上代码的效果的主要是变量的命名。

因为以上出现的类都是 Model 类,而 Model 则是用来描述业务是什么,而 Model 类中大部分代码则是变量。
比如 这是一个 TodoList App (待办事项),其中:

  • 有待办事项列表
  • 待办事项列表有待办事项
  • 待办事项可以完成和未完成。
  • 待办事项可以编辑文本。

以上几点一般是我们接收到任务并理解业务之后梳理出来的。如果设计成代码结构,结构大致如下:

  • 待办事项列表
  • 待办事项
  • 完成/未完成
  • 文本

很自然 Model 的代码就写出来了,代码如下。

TodoList.cs

public class TodoList
{
	public List<Todo> Todos;
}

Todo.cs

public class Todo
{
	public int Id;

	public bool Finished;

	public string Content;

}

为了让以上类更容易地在其他文件中理解,笔者在命名上做了哪些工作呢?

####TodoList 类中的 Todos 变量:

  • Todos 是复数,说明其类型可能是数组、List 或者其他的集合。
  • Todos 首字母大写,说明访问权限是 public 类型的。
  • Todos 名字本身是一个名词,解释成中文则是:要做的事(待办事项),本身准确地传达了变量的意思。

####Todo 类

  • Id,和 Todos 一样,传达了访问权限,和意思(标识),但是没有传达其类型。不过,不管是 int 还是 string 都无所谓。
  • Finished,也是一样的,public 权限、意思(完成了)。同样也没有传达其类型,不过这里可以这样理解,Finish 是动词,加上 ed 则是过去式,有时态说明有状态,这个状态只有两个,完成和未完成,所以是 bool 类型,这是笔者的一个习惯,当然也可以命名为 IsFinish,等等。
  • Content,中文意思是内容,也就是文本内容,一般为 string 类型,因为首字母大写,所以也是 public 权限。

简单几行代码,就可以传达这么多信息,所以 命名是一门艺术

#####小结

在对一个变量进行命名的时候,无非要思考以下三点:

  • 描述
  • 权限
  • 类型

权限和类型的表达很容易,因为关于这方面的方法论(套路)是固定的,可以很容易掌握的。

比较考验功底的是变量的描述,是需要长期进行训练的。

描述、权限、类型

接下来以这三点为主,分别进行方法论的介绍。

先从最简单的来,最简单的则是 权限 的表达

权限

权限指的是访问权限,在 C# 中有 public、private、protected、internal 类型。

这里在进行变量命名时分为两种。

  • 可外部访问的 public 和 internal。
  • 和不可被外部访问的 private 和 protected。

权限的表达,笔者自己的习惯是,可被外部访问的变量采用首字母大写。

比如:

public string Name;
internal static int ReferenceCount;

而不可被外部访问的变量采用 m 前缀。

比如:

private string mShowedText;
protected int mMgrId;

这是笔者长期的一个习惯,也是笔者自己维护的 QFramework 的命名规范。

所以关于权限的表达非常简单,依靠代码规范就能搞定了,规范怎么定就怎么用。

这一点是最最最容易达到的。

如果各位还没有开始在意命名这件事情,从权限开始上手是最容易的。

类型

变量是有类型的,最好呢是把类型能够表达出来。

类型的表达,当然这也可以依照某些代码规范来解决的。

比如匈牙利式的代码规范:

private int m_nAge;   // n 代表数量
private string m_strName // str 代表 string

不过这种规范需要适应一段时间,这里笔者不太推荐新手用这种规范解决类型表达问题。

而是采用一种比较简洁、优雅的方式,来表达变量的类型。

比如:

public int Age;

Age 年龄,很容易想到是 int 类型。

public string Name;

Name 名字很容易想到是 string 类型。

public int PetCount;
public int PetNumber;

宠物数量,很容易想到是 int 类型。

public bool Completed;

Completed 完成了,是 bool 类型。

而这部分的详细内容会在接下来的一个小节中详细介绍。

####描述

关于变量的描述的内容就比较多了,但是原则很简单,就两个字:准确。

为变量命名时最重要的考虑事项是,该名字要完全、准确地描述出该变量所代表的事物。——《代码大全》

如何达到准确呢?这部分也会在接下来的一个小节中介绍。

在本小节,先给大家对变量所用的词类型进行简单的分类:

  • 事物
  • string
  • Name
  • NickName
  • int
  • Age
  • XXXCount
  • Times
  • 状态
  • 过去式
  • Finished
  • 进行时
  • Waiting
  • 一些不推荐使用的(IsXXX):
  • IsFinished
  • IsFinish
  • IsWaiting
  • 形容词
  • active
  • 其他
  • C# 中特有的 委托、事件 等。
  • OnXXXFinished
  • OnBeforeDestroy
  • OnLoadDone
  • OnLoadStart

所以在之后详细讲述变量的描述表达部分会根据这三点来进行介绍。

####小结

读者读到这里,权限的表达是最容易做到的,如果没有意识到的童鞋们,可以在工作中开始权限的表达训练了。

权限的表达比较适合作为变量名训练的的第一步。

关于权限部分的内容就到此完结了,下文中不再赘述。

而剩下的类型和描述用两个大章节进行详细介绍。

我们先搞定相对较容易的类型的表达,这样好让大家掌握并快速在工作中实践。

###类型的表达

####关于集合(List、Array、Dictionary 等等)

List、Array 可以使用名词的复数来搞定。

比如:

var todos = new List<Todo>();
var todos = new Todo[]{};

也可以将集合类型作为后缀表达

var todoList = new List<Todo>();
var todoArray = new List<Todo>();

Dictionary

因为是 key-value 类型的集合,可以使用 xxx4yyy方式命名(4 = for),这是笔者个人习惯,不强制。

var todo4Id = new Dictionary<string,Todo>();

也可以使用类型作为后缀。

var todoDict = new Dictionary<string,Todo>();

但是 key-value 类型使用 类型作为后缀表达效果较差,所以推荐 xxx4yyy 方式。

其他的集合类型,比如 Queue、Stack 等,也同样可以采用类型后缀的方式。

var panelStack = new Stack<UIPanel>();
var msgQueue = new Queue<Msg>();

Stack 和 Queue 是技术概念,所以一般会在框架/插件/工具层使用,在业务层使用得较少。

集合类型部分介绍到这里。

数值/数据类型、string/char 等 (事物)

数值类型(int/flout/double):

int 类型常见的后缀词(限定词):

Number

var todoNumber = todos.Length;

Index

var todoIndex = todos.IndexOf(todo);

Count

var todoCount = todos.Count;

int 类型常见的前缀词:

num

var numTodos = todos.Length;  // number of todos 缩写,不太常用。

float/double 类型常见的后缀词
Average、Sum 等。

var ageAverage = ageSum / peopleCount;

这种类型的后缀词和前缀词在计算时经常会用到。

这里叫做计算限定词。
下面列出常用的计算限定词:

Count、Number、Num、Index、Total、Sum、Average、Max、Min、Record 等。

《代码大全》建议,除了 Num 之外其他的都建议放到后缀

还有一些数值类型的名字本身就表达了其类型。

age(int)、times(int) 等。
数据类型(string/char):

数据类型没什么好说的,只能尽量选取名字本身就表达其类型的这种名字,而这部分的内容算是描述的表达部分,这里先列出一部分:

username(string)、password(string)、name(string)、content(string)、title(string)、description(string)、id(int/string) 等。

bool、枚举(状态)

bool 类型:
  • 使用动词 + 时态。
  • done
  • finished
  • completed
  • updating
  • enabled
  • 使用 Is + 名词。
  • isEnemy
  • 形容词
  • active

给 bool 类型取名的原则很简单,只要表示其状态只有两种就好。
比如 GameObject 要么为 激活状态 (active 为 true) ,要么为 未激活状态 (active 为 false)。符合这样的就是一个很好的 bool 变量名,不可能有第三个状态的。

再比如,一个任务要么完成(finished 为 true),要么为未完成(finished 为 false)。

枚举

枚举部分要分两个部分讲,一个是定义部分,一个是变量部分。
枚举的定义:

// 事件
public enum UIMainPanelEvent
{
	// 事件监听
	OnModelDataChanged,
	
	// 指令 (动宾短语、动词)
	UpdateView
}

// 状态 (与 bool 类型一样)
public enum ResLoadState
{
	LoadBegan,
	Loading,
	LoadEnded,
}

// 结果 (名词、形容词)
public enum ResLoadResult
{
	Success,
	Fail,
}

定义部分和 bool 类型很像,比如 ModelDataChanged 就是 动词 + 时态的方式。

Unity 特有的 GameObject、Transform、Button、Image 等

GameObject heroObj;
GameObject heroGameObj;
Transform  heroTrans;
Button btnEnterMainPanel;
Image logoImage;

以上为常见的前缀、和后缀。

关于类型的表达介绍就到这里。

小结

到这里我们对类型的表达进行了详尽的介绍。
笔者关于类型表达的方法论也都全部介绍完了,大家可以笔者的方法论为准进行练习,在此基础上进行不断思考改进称为自己的方法论。

思考什么呢?
思考如何让别人(或自己)在别的文件中使用变量就能猜到其类型呢?
所以笔者介绍的方法论不是重点也不是权威,重点是不断进行思考。

####描述的表达
终于到了重头戏了。
描述的表达是能够考验一个开发者功底的工作。

对于 Model 类的变量命名。

  • 要想有一个好的描述,必须非常了解业务的。
  • 因为只有非常了解业务了,才能找到非常准确的描述。

就像本 Chat 的开头的入门那样:

  • 业务最起码应该是梳理过一次的,梳理成自己能理解。
  • 梳理过一次之后要再进行一次结构化的梳理,这样比较好转化为代码。
  • 结构化梳理之后才能定义 Model 类和其变量。

以上对各位的要求会比较高,所以笔者这里建议,大家熟练掌握了了权限和类型的表达后在进行描述的练习。

而在这之前,本 Chat 也只是介绍了 Model 的变量命名。
这是因为 Model 的变量定义是相对容易的。但是我们的工作不只有定义 Model,还有好多其他的类需要我们定义,而定义类多少要定义一些变量。而本 Chat 不能覆盖所有的变量命名场景,所以会给出一些准则和练习步骤来给大家参考。

####练习步骤一: 代码部分全部为英文。

在 Unity 中少数的需要显示在编辑器上提供参数调整的变量可以用一些中文汉字。而剩余的最好是全部使用英文。
这里最好是在编码时随手备着一个查词软件,可以通过键盘快捷键快速查询的。要养成这个习惯。到最好发现,在某一个领域或某种业务常用的词汇就那些,自己都记住了,很少会再编码时使用查词软件。

####练习步骤二: 业务命名而不是技术命名

这个标题是笔者自己理解的,《代码大全》的描述是变量名应该描述的是 what 而不是 how。也就是变量是什么而不是怎么做。
因为业务概念是现实的模拟,而技术概念只是手段。所以笔者理解为为了业务命名而不是为技术命名。

要理解此准则非常简单。摘抄《代码大全》的一段话给大家理解:

一个好记的名字反映的通常都是问题,而不是解决方案。一个好名字通常表达的是“什么”(what),而不是“如何”(how)。一般而言,如果一个名字反映了计算的某些方面而不是问题本身,那么它反映的就是“how”而非“what”了。请避免选取这样的名字,而应该在名字中反映出问题本身。

一条员工数据记录可以称作 inputRecord 或者 employeeData。inputRecord 是一个反映输入、记录这些计算概念的计算机术语。employeeData 则指的是问题领域,与计算世界无关。与此类似,对一个用于表示打印状态的位域来说,bitFlag 就要比 printerReady 更具计算机特征。在财务软件里,calculateValue 的计算痕迹(how) 也要比 sum (what) 明显。

书中原文解释的很清楚了,不再多说了。

####练习步骤三:分清楚何时用技术命名

这里要说一点注意的,要分清楚在哪个层。举一个笔者框架中的 SerializeHelper (序列化器),序列化是技术概念,是“怎么做?” (how),但是它可以做游戏数据的存储等操作。那么它为什么不叫做 DataSaver/DataLoader 这些名字呢?

这是因为 SerializeHelper 这个类不在业务层,而是在框架层。框架本身提供的更多的是工具。

小结

练习步骤给出了 1、2、3,很容易上手,命名这件事本身比较容易掌握,难得是初学者非常不在意。老手呢一般趟坑躺多了自然就会意识到。而初学者再去趟一下这些坑有点浪费时间,不如一开始就意识到命名这件事的重要性。

到此呢,权限、类型、描述的练习步骤和原则(准则)都给了。接下来简单聊聊,为什么要命名和如何更容易地落实命名这件事到工作中呢?

###为什么要命名?

代码的本质是信息,其承载着信息,这个信息可以被编译器理解,也要被人理解。

首先你,代码要被编译器理解,这是最基本的要求,也代表着开发者能和计算机沟通,让计算机帮忙处理任务。

其次呢,是要被人理解。

这些人可能是:

  • 自己(日常)
  • 与自己朝夕相处时间最长的就是这些代码了,如果代码非常容易读懂并且整洁,每天的工作状态都会不一样。
  • 在 debug 时候,还是要去读代码的,好的代码是更容易被阅读的,容易被阅读 debug 所花费的时间就会减少。
  • 增加功能,也还是一样需要阅读代码的,以上同理。
  • 使自己对项目的理解更深刻、清晰。
  • 同事(协作)
  • 同样代码被容易阅读,同事的效率也有所提升,也会得到同事的认可,
  • leader(晋升)
  • 很多团队有代码 review 的文化,好的代码更容易获得上级和同事的认可。
  • 得到同事和上级的认可,晋升就是很自然的事情了。
  • 来接手项目的人(离职)
  • 离职的时间也会被节省,不会被拖着问这是什么那是什么。
  • 所有开发者(开源)
  • 节省非常多人的时间,时间就是生命。
  • 读者(教学,正在读本 chat 的各位则是这个范围的)

最终受益最多的还是自己,而投入的只不过每个变量多花费了几秒的思考时间而已。

总之协作这个词有一个协字,可以理解成妥协的协,也可以理解成协助的协。

给变量合适的命名既是一种妥协也是一种协助。

这就是为什么笔者单开一个 chat 来写一个大家认为如此简单的事情。

###如何落地变量的命名

单写这一小节是希望大家不是只读一读这篇 chat 就完事了,而是要把变量的命名这件事情落实到自己产出的代码上去。

在工作中养成一个习惯的阻碍有很多。
最常见的原因就是项目时间紧。

这个问题很好解决,只要训练的时机根据实际情况进行调整就好了。

对于初学者或者项目非常紧急的开发者,在第一遍写逻辑的时候,变量可以先随便命名(按照自己的习惯),但是只要当前的测试点验证通过或者功能点完成,就应该回过头把变量的名字好好想下,进行重新命名。一般 IDE 都带重构功能,只要改一个地方,其他代码部分会全部跟着更改。

这样做的目的是为了防止同一时间做两件事情。

  • 逻辑编写
  • 变量命名

对于命名新手来说,同一时间做两件事情会很吃力。
本身逻辑编写就很吃力了,再加上需要思考的变量命名,会更加吃力。
所以对于命名新手要求同一时间只思考一件事情。

对于时间非常紧急的开发者,如果进行命名的思考会影响项目的进度,那还是等项目不急的时候再落实就好了。

只要能做到,在进行逻辑编写时,只考虑如何完成或者如何实现就好了。
在变量命名时,只专注于如何取个合适的名字,只考虑变量的描述、类型、权限就好。
如下:

  • 变量的命名是否准确描述?
  • 变量的命名是否表达了类型?
  • 变量的命名是否表示了权限?

这样坚持一段时间后,就能做到同一时间就可以做两件事情。

初学者们只要记住一点就够了:

  • 合适的命名可以不在创建变量的时候完成。

但是最终目标还是将进行合适的命名这件事情养成习惯,在创建变量的同时就已经为变量取了一个非常合适的名字。

最后,非常感谢 QFramework 的 边上海 边前辈对此专栏的勘误和提供的一些非常有价值的建议。