Unity中C#代码规范

此篇Unity中C#代码规范,是经过长期在项目中总结的一些经验和规则,公大家学习参考。

规范目的

  • 一个软件的生命周期中,80%的花费在于维护;
  • 几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护;
  • 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码。为了执行规范,每个软件开发人员必须一致遵守编码规范;
  • 使用统一编码规范的主要原因,是使应用程序的结构和编码风格标准化,以便于阅读和理解这段代码;
  • 好的编码约定可使源代码严谨、可读性强且意义清楚,与其它语言约定相一致,并且尽可能的直观。
  • 推荐使用字体 Noto Sans Mono CJK sc - Regular

排版

  • 规则1
    • 程序块要采用缩进风格编写,缩进的空格数为4个,不允许使用TAB缩进。
    • 缩进使程序更易阅读,使用空格缩进可以适应不同操作系统与不同开发工具。
1
2
3
4
5
6
7
//示例
public class PlayerData
{
public string playerName;

public bool playerSex;
}
  • 规则2
    • 左大括号( { ) 应该位于声明语声明句末尾的下一行,并独占一行,右大括号( } )应该代码块末尾,并独占一行,同时与引用它们的语句左对齐。在函数体的开始、类和接口的定义、以及if、for、do、while、switch、case语句中的程序或者static、synchronized等语句块中都要采用如上的缩进方式。
1
2
3
4
5
//示例
if (a > b)
{
DoStart();
}
  • 规则3
    • 较长的语句、表达式或参数(大于80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
1
2
3
4
5
6
//示例
stepTimer = Timer.Register(
_data.duration_Ms,
onComplete: () => { NextStep(); },
isLooped: false,
useRealTime: true);
  • 规则4
    • 不允许把多个短语句写在一行中,即一行只写一条语句。
1
2
3
4
5
6
7
8
//示例

//错误例子
Object o = new Object(); Object b = null;

//正确例子
Object o = new Object();
Object b = null;
  • 规则5
    • iffordowhilecaseswitchdefault等语句自占一行,且iffordowhileswitch等语句的执行语句无论多少都要加括号{ }case的执行语句中如果定义变量必须加括号{ }
1
2
3
4
5
6
7
8
9
//示例
if (a > b)
{
doStart();
}
case x:
{
int i = 9;
}
  • 规则6
    • 相对独立的程序块之间、变量之后必须加空行。
1
2
3
4
5
6
7
//示例
if(a > b)
{
DoStart();
}
//此处是空行
return;
  • 规则7
    • 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如.),后不应加空格。
1
2
3
4
5
6
7
//示例
if (a == b)
{
objectA.doStart();
}

a *= 2;
  • 规则8 类属性和类方法不要交叉放置,不同存取范围的属性或者方法也尽量不要交叉放置。
1
2
3
4
5
6
7
8
9
10
//示例
类定义
{
公有属性定义
保护属性定义
私有属性定义
公有方法定义
保护方法定义
私有方法定义
}
  • 规则9 修饰词按照指定顺序书写:[Attribute] [访问权限] [static] [final]
1
2
//示例:
[SerializeField] private static final String str = "abc";

注释

  • 代码注释约定

    • 所有的方法和函数都应该以描述这段代码的功能的一段简明注释开始(方法是干什么)。这种描述不应该包括执行过程细节(它是怎么做的),因为这常常是随时间而变的,而且这种描述会导致不必要的注释维护工作,甚至成为错误的注释。代码本身和必要的嵌入注释将描述实现方法。
    • 当参数的功能不明显且当过程希望参数在一个特定的范围内时,也应描述传递给过程的参数。被过程改变的函数返回值和全局变量,特别是通过引用参数的那些,也必须在每个过程的起始处描述它们。
    • 注释的内容要清楚、明了,含义准确,防止注释二义性。
    • 修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
    • 避免在注释中使用缩写,特别是不常用缩写。在使用缩写时或之前,应对缩写进行必要的说明。
    • 不允许在一行代码或表达式的中间插入注释。
    • 源程序注释量必须在30%以上。
    • 由于每个文件的代码注释不一定都可以达到30%,建议以一个系统内部模块作为单位进行检查。
  • 规则1

    • 每个类开头必须要有以下注释,在using代码块之下。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /// <summary>
      /// 一句话简述
      /// 作用:(对此类的详细描述)
      /// 作者:(创建者的中文名字)
      /// 编写日期:(模块创建日期,格式:YYYY-MM-DD)
      /// 适用Unity版本:(2021.3.27f1c2 - 2022.3.3f1c1)
      /// 脚本适用平台:(Android iOS)
      /// 脚本使用注意事项:(例如:序列帧文件名称必须从0开始计数,即 图片名_0000 类似命名)
      /// TODO:(还需要完成的功能)
      /// </summary>
  • 规则2

    • 每个方法开头必须要有以下注释
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      /// <summary>
      /// 方法名称
      /// 作用:(对这个方法的作用进行简单描述,包括所有参数的简单描述)
      /// 作者:(方法创建者的中文名字)
      /// 编写日期:(方法创建日期,格式:YYYY-MM-DD)
      /// </summary>
      /// <param name="_str1">参数_str1的详细说明</param>
      /// <param name="_str2">参数_str2的详细说明</param>
      /// <returns>返回值说明</returns>
      /// <exception>异常说明</exception>
      /// <remarks>
      /// 该方法详细文字说明
      /// </remarks>
      /// <example>
      /// <code>调用此方法的代码示例</code>
      /// </example>
      public int TestClass(string _str1, string _str2)
      {
      return 0;
      }
  • 规则3

    • 其他人对代码做出修改后,要在“作者”一行中加入自己的名字(方法或类都是如此)。
    • 在“作用”一行的详细描述中,可以使用@since YYYY-MM-DD形式来表示从哪个日期开始,做出了什么样的修改,或添加了什么新的功能。
    • 在“作用”一行的详细描述中,可以使用@deprecated YYYY-MM-DD形式来表示从哪个日期开始,弃用了哪些功能。
    • 使用@see 类名/方法名形式来表示相关的类或者方法
    • 使用@Override来表示重载父类的方法,并对重载进行文字说明
1
2
3
4
5
6
7
8
9
/// <summary>
/// 方法名称
/// 作用:@since 2023-05-15 添加了对AB包进行二次加密和解密的功能
/// @deprecated 2023-05-17 弃用了之前过于简单的加密方式
/// @Override 对ResetCharacter方法进行的重载,这里是重置新添加的中立生物的状态
/// 作者:(方法创建者的中文名字)
/// 编写日期:(方法创建日期,格式:YYYY-MM-DD)
/// </summary>
/// ……
  • 规则4

    • 类和方法中声明的变量、属性、字段,其上一行均要有//形式的注释说明。
      1
      2
      3
      //示例:
      //说明这个变量的具体作用
      [SerializeField] private static final String str = "abc";
  • 规则5

    • 在代码的功能、意图层次上进行注释,提供有用、额外的信息。
1
2
3
4
5
6
7
8
//示例
//如下注释意义不大。
// 如果 receiveFlag 为真
if (receiveFlag)

//而如下的注释则给出了额外有用的信息。
// 如果从连结收到消息
if (receiveFlag)
  • 规则6

    • 对关键变量的定义和分支语句(条件分支、循环语句等)必须编写注释。
    • 这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
  • 规则7

    • 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。中文注释中需使用中文标点。方法和类描述的第一句话尽量使用简洁明了的话概括一下功能,然后加以句号。接下来的部分可以详细描述。

命名

  • 规则1
    • 类、接口、变量、函数等正确的命名以及合理地组织代码的结构,使代码成为自注释的,清晰准确的命名,可增加代码可读性,并减少不必要的注释。
    • 类名、方法名、枚举名使用意义完整的英文描述,并使用大驼峰命名法(Pascal)。
    • 接口名开头使用大写英文字母I,加意义完整的英文描述,并使用大驼峰命名法(Pascal)。
    • 变量、字段使用小驼峰命名法 (Camel)。
    • 局部变量、方法入参使用小驼峰命名法 (Camel),并且变量名前要加英文半角下划线 (_)。
1
2
3
4
5
6
7
8
9
10
//示例
public class PlayerData
{
private string playerName;

public void InitPlayer(string _playerName)
{
playerName = _playerName;
}
}
  • 规则2
    • 存取属性的方法采用setter 和 getter方法,动作方法采用动词和动宾结构。
1
2
3
4
5
6
7
8
9
10
11
12
//格式:
//get + 非布尔属性名()
//is + 布尔属性名()
//set + 属性名()
//动词()
//动词 + 宾语()
//示例:
public String getType();
public boolean isFinished();
public void setVisible(boolean);
public void show();
public void addKeyListener(Listener);
  • 规则3
    • 属性名使用意义完整的英文命名,其访问器使用小写名称并在前面加英文半角下划线 (_)。
1
2
3
4
5
6
7
8
9
10
11
12
//示例
public string CharacterName
{
get
{
return _characterName;
}
set
{
_characterName = value;
}
}
  • 规则4
    • 常量名使用全大写的英文描述,英文单词之间用下划线分隔开,并且使用 static final修饰。
1
2
3
//示例: 
public static final int MAX_VALUE = 1000;
public static final String DEFAULT_START_DATE = "2001-12-08";
  • 规则5
    • 如果函数名超过15个字母,可采用以去掉元音字母的方法或者以行业内约定俗成的缩写方式缩写函数名。
1
2
//示例:
getCustomerInformation() 改为 getCustomerInfo()
  • 规则6

    • namespace的命名,应按照功能范围划分并命名,防止产生冲突。
    • 部门内部产品使用“部门名称 + 模块名称”的方式命名。
    • 上线产品使用“产品名称 + 模块名称”的方式命名。
  • 规则7

    • 准确的使用访问修饰符。
    • 不允许有类似标记为public的属性、变量等,只在该类内部进行访问的现象。
调用方的位置 public protected internal protected internal private protected private
在类内 ✔️️ ✔️ ✔️ ✔️ ✔️ ✔️
派生类(相同程序集) ✔️ ✔️ ✔️ ✔️ ✔️
非派生类(相同程序集) ✔️ ✔️ ✔️
派生类(不同程序集) ✔️ ✔️ ✔️
非派生类(不同程序集) ✔️
  • 规则8
    • 建议2 含有集合意义的属性命名,尽量包含其复数的意义。
1
2
3
//示例:
public List<GameObject> characters;
public GameObject[] orderItems;
  • 规则8
    • 1到3个字母缩写的单词,用全大写字母,大于3个字母的缩写单词,用大驼峰命名法
1
2
3
//示例:
public string IOHelper;
public string HttpClient;

编码

  • 规则1
    • 一个文件不要定义两个类(并非指内部类),方便程序的阅读与代码的维护。

Git/SVN提交规范

  • 提交模板

    1
    2
    3
    4
    [类型] 类型关键字
    [标题] 此次提交是进行哪些改动简述
    [描述] 对此次提交进行详细的文字描述
    [Bug修复情况] 此行为可选填,下面会说明如何填写
  • 提交类型,在提交更改时,第一行要写的关键字,可以较快的辨别该提交是那种类型的

  • 提交类型可以有多个,例如一个修复BUG是修改了FBX模型,那么[类型]就是Fix Art3D_Model,两个类型之间用英文半角空格进行间隔

关键字 说明
Settings 对项目配置进行了改变️
Docs 对文档进行了修改
Feat 加了新的功能
Fix 修复了BUG
Art_UI 对UI素材进行了修改
Art2D_Sprite 对2D精灵类型的素材进行了修改
Art3D_Model 对3D类型是美术文件进行了修改,包括FBX模型、动作、材质以及模型对应的贴图,要在描述中进行详细说明
Scene 对场景文件进行了修改
Refactor 既不是修复bug也不是添加特征的代码重构
Pref 提高性能的更改,包括代码、各类美术素材设置、项目设置等,要在描述中进行详细说明
Revert 回滚,要写明回滚到哪个提交节点上,以及原因
  • BUG修复情况说明,如果此次提交为修复BUG,要写明之前的BUG是什么样子的,例如网络访问错误还是人物操作抖动等,如果是修改issue的话,要引用该issue

  • 示例

    1
    2
    3
    4
    [类型] Fix Art3D_Model
    [标题] 修复了男性3号NPC在挥手时抖动的问题
    [描述] 男性3号NPC在挥手时抖动,是因为模型导出时连带的动画文件关键帧平滑有问题,已查明是blender的问题,使用maya重新导出,并在unity中进行了对应的动画压缩和代码调整
    [Bug修复情况]已经修复了男性3号NPC在挥手时抖动的问题 issue #1234

参考链接

命名准则(微软)

代码风格(谷歌)

《编写可读代码的艺术》

《Effective C#》

《编写高质量代码 改善C#程序的157个建议》