代码风格
代码风格一般包含标识符的命名风格、注释风格及排版风格。一致的编码习惯与风格,会使代码更容易阅读、理解,更容易维护。
命名
有意义地、恰当地命名在编程中是一个较难的事。好的命名特征有:能清晰地表达意图,避免造成误导。 少用缩写,但常见词以及业务线的领域词汇都是允许的,比如 response:resp,request:req,message:msg。 使用仓颉语言编程建议各种形式的名字使用统一的命名风格,具体如下:
类别 | 命名风格 | 形式 |
---|---|---|
包名和文件名 | unix_like:单词全小写,用下划线分割 | aaa_bbb |
接口,类,结构体,枚举和类型别名 | 大驼峰:首字母大写,单词连在一起,不同单词间通过单词首字母大写分开,可包含数字 | AaaBbb |
变量,函数,函数参数 | 小驼峰:首字母小写,单词连在一起,不同单词间通过单词首字母大写分开。例外:测试函数可有下划线 _ ,循环变量,try-catch 中的异常变量,允许单个小写字母 | aaaBbb |
let 全局变量, static let 成员变量 | 建议全大写,下划线分割 | AAA_BBB |
泛型类型变量 | 单个大写字母,或单个大写字母加数字,或单个大写字母接下划线、大写字母和数字的组合,例如 E, T, T2, E_IN, E_OUT, T_CONS | A |
下表是一些易混淆的单个字符,当作为标识符时,需留意:
易混淆的字符 | 易误导的字符 |
---|---|
O (大写的 o), D (大写的 d) | 0 (zero) |
I (大写的 i), l (小写 L) | 1 (one) |
Z (大写的 z) | 2 (two) |
S (大写的 s) | 5 (five) |
b (小写的 B) | 6 (six) |
B (大写的 b) | 8 (eight) |
q(小写的 Q) | 9 (nine) |
h (小写的 H) | n (小写的 N) |
m (小写的 M) | rn (小写的 RN) |
_ (下划线) | 连续多个时很难分辨究竟有几个 |
另外,在使用大、小驼峰命名风格时若遇到 JSON,HTTP 等首字母缩略词,
应将整个缩略词看做普通单词处理,服从命名风格的大小写规定,而不要维持全大写的写法。
如大驼峰风格中:XmlHttpRequest
,小驼峰风格中:jsonObject
。
包名和文件名
G.NAM.01 包名采用全小写单词,允许包含数字和下划线
【级别】建议
【描述】
- 包名字母全小写,如果有多个单词使用下划线分隔;
- 包名允许有数字,例如 org.apache.commons.lang3;
- 带限定前缀的包名必须和当前包与源代码根目录的相对路径对应,建议以 Internet 域名反转的规则开头,再加上产品名称和模块名称。
【正例】
域名 | 包名 |
---|---|
my_product.example.com | com.example.my_product |
my_product.example.org | org.example.my_product |
G.NAM.02 源文件名采用全小写加下划线风格
【级别】建议
【描述】
- 文件名不采用驼峰的原因是:不同系统对文件名大小写处理不同(如 Windows 系统不区分大小写,但是 Unix/Linux, Mac 系统则默认区分)。
- 如果文件只包含一个包外部可见的顶层元素,那么选择该顶层元素的名称,以此命名。否则,选择能代表主要内容的元素名称作为文件名。源文件名称使用全小写加下划线风格。
【正例】
// my_class.cj
public class MyClass {
// CODE
}
【反例】
// MyClass.cj 文件名不符合:使用了驼峰命名
public class MyClass {
// CODE
}
接口、类、struct、enum 和类型别名
G.NAM.03 接口、类、struct、enum 类型和 enum 构造器、类型别名、采用大驼峰命名
【级别】建议
【描述】
- 类型定义通常是名词或名词短语,其中接口名还可以是形容词或形容词短语,都应采用大驼峰命名。
- enum 构造器采用大驼峰命名风格。
- 测试类命名时推荐以被测试类名开头,并以 Test 结尾。例如,HashTest 或 HashIntegrationTest。
- 建议异常类加
Exception
/Error
后缀。
例外:
在 UI 场景下,一些配置需要用 enum 的成员构造来实现,html 里的一些配置习惯用小驼峰,对于领域内有约定的场景,允许例外。
【正例】
// 符合:类名使用大驼峰
class MarcoPolo {
// CODE
}
// 符合:enum 类型和 enum 构造器使用大驼峰
enum ThreadState {
New | Runnable | Blocked | Terminated
}
// 符合:接口名使用大驼峰
interface TaPromotable {
// CODE
}
// 符合:类型别名使用大驼峰
type Point2D = (Float64, Float64)
// 符合:抽象类名使用大驼峰
abstract class AbstractAppContext {
// CODE
}
【反例】
// 不符合:类名使用小驼峰
class marcoPolos {
// CODE
}
// 不符合:enum 类型名使用小驼峰
enum timeUnit {
Year | Month | Day | Hour
}
函数
G.NAM.04 函数名称应采用小驼峰命名
【级别】建议
【描述】
-
函数名称采用小驼峰命名风格。例如,sendMessage 或 stopServer。
格式如下:
- 建议优先将 field 对外的接口实现成属性,而不是 getXXX/setXXX,会更简洁。
- 布尔属性名建议加 is 或 has, 例如:isEmpty。
- 函数名称建议使用以下格式:has + 名词 / 形容词 ()、动词 ()、动词 + 宾语 ()。
- 回调函数(callback)允许介词 + 动词形式命名,如: onCreate, onDestroy, toString 其中动词主要用在动作的对象自身上,如 document.print()。
-
下划线可能出现在单元测试函数名称中,用于分隔名称的逻辑组件,每个组件都使用小驼峰命名法。例如,一种典型的模式是
<methodUnderTest>_<state>
,又例如pop_emptyStack
,命名测试函数没有唯一的正确方法。
【正例】
// 符合:函数名使用小驼峰
func addExample(start: Int64, size: Int64) {
return start + size
}
// 符合:函数名使用小驼峰
func printAdd(add: (Int64, Int64) -> Int64): Unit {
println(add(1, 2))
}
【反例】
// 不符合:函数名使用大驼峰
func GenerateChildren(page: Int64) {
println(page.toString())
}
变量
G.NAM.05 const 变量的名称采用全大写
【级别】建议
【描述】
const 变量表示在编译时完成求值,并且在运行时不可改变的变量,使用下划线分隔的全大写单词来命名。
【反例】:
// 不符合 : const 变量没有使用下划线分隔的全大写单词命名
const MAXUSERNUM = 200
class Weight {
static const GramsPerKg = 1000
}
【正例】
// 符合 : const 变量使用下划线分隔的全大写单词命名
const MAX_USER_NUM = 200
class Weight {
static const GRAMS_PER_KG = 1000
}
G.NAM.06 变量的名称采用小驼峰
【级别】建议
【描述】
变量、属性、函数参数、pattern 等均采用小驼峰命名风格。
例外:
-
泛型类型变量,允许单个大写字母,或单个大写字母加数字,或单个大写字母接下划线、大写字母和数字的组合,例如 E, T, T2, E_IN, E_OUT, T_CONS
-
函数内使用的数值常量,不要使用魔鬼数字,用 let 声明有意义的局部变量代替,此时局部变量名可以使用全大写下划线的风格命名,强调是常量
不应该取 NUM_FIVE = 5 或 NUM_5 = 5 这样的 “魔鬼常量”。如果被粗心大意地改为 NUM_5 = 50 或 55 等,很容易出错。
【反例】:
// 不符合:变量名使用无意义单个字符
var i: Array<Item> = ...
// 不符合:类型参数使用小写字母
class Map<key, val> { ... }
【正例】
// 变量名使用小驼峰命名
let menuItems: Array<Item> = ...
let names: Array<String> = ...
let menuItemsArray: Array<Item> = ...
let menuItems: Set<Item> = ...
let rememberedSet: Set<Address> = ...
let waitingQue: Queue<Thread> = ...
let lookupTable: Array<Int64> = ...
let word2WordIdMap: Map<Stirng, Integer> = ...
class MyPage <: Page {
var pageNo = StateInt64(1) // 实例成员变量使用小驼峰命名
var imagePath = StateArray(images) // 实例成员变量使用小驼峰命名
init() {
...
}
}
// 参数名使用小驼峰命名
func getColumnMoreDataColumn(pageType: String, idxColumn: Int64, outIndex: Int64) {
...
}
// 类型参数使用大写字母
class Map<KEY, VAL> { ... }
// 类型参数使用大写字母与数字
class Pair<T1, T2> { ... }
格式
尽管有些编程的排版风格因人而异,但是我们强烈建议和要求在同一个项目中使用统一的编码风格,以便所有人都能够轻松的阅读和理解代码,增强代码的可维护性。
编码格式
G.FMT.01 源文件编码格式(包括注释)必须是 UTF-8
【级别】要求
【描述】
对于源文件,应统一采用 UTF-8 进行编码。仓颉编译器目前仅支持 UTF-8 编码。
文件
G.FMT.02 一个源文件按顺序包含版权、package、import、顶层元素四类信息,且不同类别之间用空行分隔
【级别】建议
【描述】
一个源文件会包含以下几个可选的部分,应按顺序组织,且每个部分之间用空行隔开:
- 许可证或版权信息;
- package 声明,且不换行;
- import 声明,且每个 import 不换行;
- 顶层元素。
【正例】
// 第一部分,版权信息
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
*/
// 第二部分,package 声明
package com.huawei.myproduct.mymodule
// 第三部分,import 声明
from std import collection.HashMap // 标准库
// 第四部分,public 元素定义
public class ListItem <: Component {
// ...
}
// 第五部分,internal 元素定义
class Helper {
// CODE
}
G.FMT.03 import 包应该按照包所归属的组织或分类进行分组
【级别】建议
【描述】
说明:import 导入包根据归属组织或分类进行分组:本公司 (例如华为公司 com.huawei.),其它商业组织 (com.),其它开源第三方、net/org 开源组织、标准库。两个分组之间使用空行分隔。
【正例】
import com.huawei.*; // 华为公司
import com.google.common.io.Files; // 其它商业组织
import harmonyos.*; // 开源
import mitmproxy.*; // 其它开源第三方
import textual.*; // 开源
import net.sf.json.*; // 开源组织
import org.linux.apache.server.SoapServer; // 开源组织
from std import io.*; // 标准库
from std import socket.*
G.FMT.04 一个类、接口或 struct 的声明部分应当按照静态变量、实例变量、构造函数、成员函数的顺序出现,且用空行分隔
【级别】建议
【描述】
一个类或接口的声明部分应当按照以下顺序出现:
- 静态变量
- 实例变量
- 构造函数,如果有主构造函数,则主构造函数在其它构造函数之前
- 属性
- 成员函数
实例变量、构造函数,均按访问修饰符从大到小排列:public、protected、private。静态变量要按初始化顺序来,可以不遵循按访问修饰符从大到小排列的建议。
说明:
- 对于自注释字段之间可以不加空行;
- 非自注释字段应该加注释且字段间空行隔开;
- enum 没有成员变量,按照 enum 构造器,属性,成员函数的顺序出现;
行宽
G.FMT.05 行宽不超过 120 个窄字符
【级别】建议
【描述】
一个宽字符占用两个窄字符的宽度。除非另有说明,否则任何超出此限制的行都应该换行,如 [换行](# 换行) 一节中所述。
每个 Unicode 代码点都计为一个字符,即使其显示宽度大于或小于一个字符。如果使用 全角字符,可以选择比此规则建议的位置更早地换行。
字符的 “宽” 与“窄”由它的 east asian width Unicode 属性 定义。通常,窄字符也称 “半角” 字符,ASCII 字符集中的所有字符,包括字母(如:a
、A
)、数字(如:0
、3
)、标点(如',
'、'{
')、空格,都是窄字符;
宽字符也称 “全角” 字符,汉字(如:中
、文
)、中文标点(','
、'、
')、全角字母和数字(如 A
、3
)等都是宽字符,算 2 个窄字符。
例外:
package
和import
声明;- 对于换行导致内容截断,不方便查找、拷贝的字符(如长 URL、命令行等)可以不换行。
换行
换行是将代码分隔到多个行的行为,否则它们都会堆积到同一行。
如需换行,建议在以下位置进行换行:
- 函数参数列表(包括形参和实参),两个参数之间,在逗号后换行;
- 函数返回类型,在
:
后换行; - 泛型参数列表(包括泛型形参和泛型实参),两个泛型参数之间,在逗号后换行;
- 泛型约束条件列表,两个约束条件之间,在
&
或逗号后换行; - 类型声明时,父类和实现接口列表,在
&
后换行; - lambda 表达式的
=>
后换行; - 如果需要在二元操作符
+
、-
号的位置换行,应在操作符之后换行以避免歧义;
举个例子,下面这个复杂的函数声明,建议在箭头(^
)所指的位置进行换行:
public func codeStyleDemo<T, M>(param1: String, param2: String): String where T <: String, M <: String { ... }
// ^ ^ ^ ^
G.FMT.06 表达式中不要插入空白行
【级别】建议
【描述】
- 当表达式过长,或者可读性不佳时,需要在合适的地方换行。表达式换行后的缩进要跟上一行的表达式开头对齐。
- 可以在表达式中间插入注释行
- 不要在表达式中间插入空白行
【正例】
func foo(s: string) {
s
}
func bar(s: string) {
s
}
/* 符合,表达式中可以插入注释进行说明 */
main() {
let s = "Hello world"
/* this is a comment */
|> foo
|> bar
}
【反例】
func foo(s: string) {
s
}
func bar(s: string) {
s
}
/* 不符合,表达式中不应插入空白行 */
main() {
let s = "Hello world"
|> foo
|> bar
}
G.FMT.07 一行只有一个声明或表达式
【级别】建议
【描述】声明或表达式应该单独占一行,更加利于阅读和理解代码。
【反例】
// 不符合:多个变量声明需要分开放在多行
var length = 0; var result = false
// 不符合: 多个表达式需分开放在多行
result = true; result
【正例】
// 符合:多个变量声明需要分开放在多行
var length = 0
var result = false
// 符合: 多个表达式分开放在多行
result = true
result
缩进
G.FMT.08 采用一致的空格缩进
【级别】建议
【描述】
建议使用空格进行缩进,每次缩进 4 个空格。避免使用制表符(\t
)进行缩进。
对于 UI 等多层嵌套使用较多的产品,可以统一使用 2 个空格缩进。
【正例】
class ListItem {
var content: Array<Int64> // 符合:相对类声明缩进 4 个空格
init(
content: Array<Int64>, // 符合:函数参数相对函数声明缩进 4 个空格
isShow!: Bool = true,
id!: String = ""
) {
this.content = content
}
}
大括号
G.FMT.09 使用统一的大括号换行风格
【级别】建议
【描述】
选择并统一使用一种大括号换行风格,避免多种风格并存。
对于非空块状结构,大括号推荐使用 K&R 风格:
- 左大括号不换行;
- 右大括号独占一行,除非后面跟着同一表达式的剩余部分,如
do-while
表达式中的while
,或者if
表达式中的else
和else if
等。
【正例】
enum TimeUnit { // 符合:跟随声明放行末,前置 1 空格
Year | Month | Day | Hour
} // 符合:右大括号独占一行
class A { // 符合:跟随声明放行末,前置 1 空格
var count = 1
}
func fn(a: Int64): Unit { // 符合:跟随声明放行末,前置 1 空格
if (a > 0) { // 符合:跟随声明放行末,前置 1 空格
// CODE
} else { // 符合:右大括号和 else 在同一行
// CODE
} // 符合:右大括号独占一行
}
// lambda 函数
let add = { base: Int64, bonus: Int64 => // 符合: lambda 表达式中非空块遵循 K&R 风格
print("符合 news")
base + bonus
}
【反例】
func fn(count: Int64)
{ // 不符合:左大括号不应该单独一行
if (count > 0)
{ // 不符合:左大括号不应该单独一行
// CODE
} // 不符合:右大括号后面还有跟随的 else,应该和 else 放同一行
else {
print("count <= 0")} // 不符合:右大括号后面没有跟随的部分,应该独占一行
}
例外: 对于空块既可遵循前面的 K&R 风格,也可以在大括号打开后立即关闭,产品应考虑使用统一的风格。
【正例】
open class Demo {} // 符合: 空块,左右大括号在同一行
空行和水平空格
G.FMT.10 用空格突出关键字和重要信息
【级别】建议
【描述】
水平空格应该突出关键字和重要信息。单个空格应该分隔关键字与其后的左括号、与其前面的右大括号,出现在二元操作符 / 类似操作符的两侧。行尾和空行不应用空格 space。总体规则如下:
-
建议 加空格的场景:
- 条件表达式(if 表达式),循环表达式(for-in 表达式,while 表达式和 do-while 表达式),模式匹配表达式(match 表达式)和 try 表达式中关键字与其后的左括号,或与其前面的右括号之间
- 赋值运算符(包括复合)前后,例如
=
、*=
等 - 逗号
,
、enum 定义中的|
符号、变量声明 / 函数定义 / 命名参数传值中的冒号:
之后,例如let a: Int64
- 二元操作符、泛型约束的
&
符号、声明父类父接口或实现 / 扩展接口的<:
符号、range
操作符步长的冒号:
前后两侧,例如base + offset
,Int64 * Int64
等 - lambda 表达式中的箭头前后,例如
{str => str.length()}
- 条件表达式、循环表达式等场景下的
)
与{
之间加空格,例如:if (i > 0) {
。 - 函数、类型等声明的
{
之前加空格,例如:class A {
-
不建议 加空格的场景:
- 成员访问操作符(
instance.member
)前后, 问号操作符?
前后 range
操作符(0..num
、0..=num
这 2 种区间)前后- 圆括号、方括号内两侧
- 一元操作符前后,例如
cnt++
- 函数声明或者函数调用的左括号之前
- 逗号
,
和变量声明 / 函数定义 / 命名参数传值中的冒号:
之前 - 下标访问表达式中,
[
和它之前的 token 之间
- 成员访问操作符(
推荐示例如下:
var isPresent: Bool = false // 符合:变量声明冒号之后有一个空格
func method(isEmpty!: Bool): RetType { ... } // 符合:函数定义(命名参数 / 返回类型)中的冒号之后有一个空格
method(isEmpty: isPresent) // 符合: 命名参数传值中的冒号之后有一个空格
0..MAX_COUNT : -1 // 符合: range 操作符区间前后没有空格,步长冒号前后两侧有一个空格
var hundred = 0
do { // 符合:关键字 do 和后面的括号之间有一个空格
hundred++
} while (hundred < 100) // 符合:关键字 while 和前面的括号之间有一个空格
func fn(paramName1: ArgType, paramName2: ArgType): ReturnType { // 符合:圆括号和内部相邻字符之间不出现空格
...
for (i in 1..4) { // 符合:range 操作符左右两侧不留空格
...
}
}
let listOne: Array<Int64> = [1, 2, 3, 4] // 符合:方括号和圆括号内部两侧不出现空格
let salary = base + bonus // 符合:二元操作符左右两侧留空格
x++ // 符合:一元操作符和操作数之间不留空格
G.FMT.11 减少不必要的空行,保持代码紧凑
【级别】建议
【描述】
减少不必要的空行,可以显示更多的代码,方便代码阅读。建议:
- 根据上下内容的相关程度,合理安排空行;
- 类型定义和顶层函数定义与前后顶层元素之间至少空一行;
- 函数内部、类型定义内部、表达式内部,不使用连续空行;
- 不使用连续 3 个或更多空行;
- 大括号内的代码块行首之前和行尾之后不要加空行。
【反例】
class MyApp <: App {
let album = albumCreate()
let page: Router
// 空行
// 空行
// 空行
init() { // 不符合:类型定义内部使用连续空行
this.page = Router("album", album)
}
override func onCreate(): Unit {
println( "album Init." ) // 不符合:大括号内部首尾存在空行
}
}
修饰符
G.FMT.12 修饰符关键字按照一定优先级排列
【级别】建议
【描述】
以下是推荐的所有修饰符排列的优先级:
public/protected/private
open/abstract/static/sealed
override/redef
unsafe/foreign
const/mut
另外,因为 sealed 已经蕴含了 public 和 open 的语义,不推荐 sealed 与 public 或 open 同时使用。
注释
注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释。
注释内容要简洁、明了、无二义性,信息全面且不冗余。
注释跟代码一样重要。
CangJie 中注释的语法有以下几种:单行注释(//
)、多行注释(/*...*/
)。本节介绍如何规范使用这些注释。
文件头注释
G.FMT.13 文件头注释应该包含版权许可
【级别】建议
【描述】
文件头注释必须放在 package 和 import 之前,必须包含版权许可信息,如果需要在文件头注释中增加其他内容,可以在后面以相同格式补充。版本许可不应该使用单行样式的注释,必须从文件顶头开始。如果包含 “关键资产说明 “类注释,则应紧随其后。
版权许可内容及格式必须如下:
中文版:
/*
* 版权所有 (c) 华为技术有限公司 2019-2021
*/
英文版:
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
*/
关于版本说明,应注意:
-
2019-2021 根据实际需要可以修改。
2019 是文件首次创建年份,而 2021 是文件修改最后年份。二者可以一样,如 "2021-2021"。
对文件有重大修改时,必须更新后面年份,如特性扩展,重大重构等。
-
版权说明可以使用华为子公司。
如:版权所有 (c) 海思半导体 2012-2020
或英文: Copyright (c) Hisilicon Technologies Co., Ltd. 2019-2021. All rights reserved.
代码注释
G.FMT.14 代码注释放于对应代码的上方或右边
【级别】建议
【描述】
-
代码上方的注释与被注释的代码行间无空行,保持与代码一样的缩进。
-
代码右边的注释,与代码之间至少留有 1 个空格。代码右边的注释,无需插入空格使其强行对齐。
选择并统一使用如下风格:
var foo = 100 // 放右边的注释 var bar = 200 /* 放右边的注释 */
-
当右置的注释超过行宽时,请考虑将注释至于代码上方。同一个 block 中的代码注释不用插入空格使其强行对齐。
【正例】
class Column <: Div { var reverse: Bool = false var justifyContent: JustifyContent = JustifyContent.flexStart var alignItems: AlignItems = AlignItems.stretch // 注释和代码间留一个空格 var alignSelf: AlignSelf = AlignSelf.auto // 上下两行注释无需插入空格强行对齐 init() { ... } }
-
if else if
为了更清晰,考虑注释放在else if
同行或者在块内都行,但不是在else if
之前,避免以为注释是关于它所在块的。【反例】
var nr = 100 if (nr % 15 == 0) { println("fizzbuzz") // 当 nr 只能被 3 整除,不能被 5 整除不符合。 } else if (nr % 3 == 0) { println("fizz") }
上述错误示例的注释是
if
分支的还是else if
分支的,容易造成误解。【正例】
var nr = 100 if (nr % 15 == 0) { println("fizzbuzz") } else if (nr % 3 == 0) { // 当 nr 只能被 3 整除,不能被 5 整除不符合。 println("fizz") }