超全面的Swift编码规范(推荐)
前言
关于Swift的代码的相关规范,不同的开发者都有自己相应的规范,可能还是很多人根本就没有规范。为了保证同一个公司同一个项目组中代码美观并且一致,这里写下这份Swift编程规范指南。该指南首要目标是让代码紧凑,可读性更高且简洁。
代码格式
使用四个空格进行缩进
每行最多160个字符,这样可以避免一行过长(Xcode->Preferences->TextEditing->Pageguideatcolumn:设置成160即可)
确保每个文件结尾都有空白行
确保每行都不以空白符作为结尾(Xcode->Preferences->TextEditing->Automaticallytrimtrailingwhitespace+Includingwhitespace-onlylines)
左大括号不用另起一行
classSomeClass{
funcsomeMethod(){
ifx==y{
/*...*/
}elseifx==z{
/*...*/
}else{
/*...*/
}
}
/*...*/
}
要在逗号后面加空格
letarray=[1,2,3,4,5];
二元运算符(+,==,或>)的前后都需要添加空格,左小括号和右小括号前面不需要空格。
letvalue=20+(34/2)*3
if1+1==2{
//TODO
}
funcpancake->Pancake{
/**dosomething**/
}
遵守Xcode内置的缩进格式,当声明的一个函数需要跨多行时,推荐使用Xcode默认格式。
//Xcode针对跨多行函数声明缩进
funcmyFunctionWithManyParameters(parameterOne:String,
parameterTwo:String,
parameterThree:String){
//Xcode会自动缩进
print("\(parameterOne)\(parameterTwo)\(parameterThree)")
}
//Xcode针对多行if语句的缩进
ifmyFirstVariable>(mySecondVariable+myThirdVariable)
&&myFourthVariable==.SomeEnumValue{
//Xcode会自动缩进
print("Hello,World!")
}
当调用一个函数有多个参数时,每个参数另起一行,比函数名多一个缩进。
functionWithArguments( firstArgument:"Hello,Iamastring", secondArgument:resultFromSomeFunction() thirdArgument:someOtherLocalVariable)
当遇到需要处理的数组或字典内容较多需要多行显示时,需要把[和]类似方法体里面的括号,方法体里的闭合也要做类似的处理。
functionWithBunchOfArguments(
someStringArgument:"helloIamastring",
someArrayArgument:[
"dadadadaaaadaaaadadadadaaaadaaaadadadadaaaadaaaa",
"stringoneiscrazy-whatisitthinking?"
],
someDictionaryArgument:[
"dictionarykey1":"somevalue1,butalsosomemoretexthere",
"dictionarykey2":"somevalue2"
],
someClosure:{parameter1in
print(parameter1)
})
尽量避免出现多行断言,可使用本地变量或其他策略
//推荐
letfirstCondition=x==firstReallyReallyLongPredicateFunction()
letsecondCondition=y==secondReallyReallyLongPredicateFunction()
letthirdCondition=z==thirdReallyReallyLongPredicateFunction()
iffirstCondition&&secondCondition&&thirdCondition{
//你要干什么
}
//不推荐
ifx==firstReallyReallyLongPredicateFunction()
&&y==secondReallyReallyLongPredicateFunction()
&&z==thirdReallyReallyLongPredicateFunction(){
//你要干什么
}
命名
使用帕斯卡拼写法(又名大骆驼拼写法,首字母大写)为类型命名(如struct,enum,class,typedef,associatedtype等)。
使用小骆驼拼写法(首字母小写)为函数,方法,常亮,参数等命名。
首字母缩略词在命名中一般来说都是全部大写,例外的情形是如果首字母缩略词是一个命名的开始部分,而这个命名需要小写字母作为开头,这种情形下首字母缩略词全部小写。
//"HTML"是变量名的开头,需要全部小写"html" lethtmlBodyContent:String="Hello,World!
" //推荐使用ID而不是Id letprofileID:Int=1 //推荐使用URLFinder而不是UrlFinder classURLFinder{ /*...*/ }
使用前缀k+大骆驼命名法为所有非单例的静态常量命名。
classClassName{
//基元常量使用k作为前缀
staticletkSomeConstantHeight:CGFloat=80.0
//非基元常量也是用k作为前缀
staticletkDeleteButtonColor=UIColor.redColor()
//对于单例不要使用k作为前缀
staticletsharedInstance=MyClassName()
/*...*/
}
命名应该具有描述性个清晰性的。
//推荐
classRoundAnimatingButton:UIButton{/*...*/}
//不推荐
classCustomButton:UIButton{/*...*/}
不要缩写、简写或单个字母命名。
//推荐
classRoundAnimatingButton:UIButton{
letanimationDuration:NSTimeInterval
funcstartAnimating(){
letfirstSubview=subviews.first
}
}
//不推荐
classRoundAnimating:UIButton{
letaniDur:NSTimeInterval
funcsrtAnmating(){
letv=subviews.first
}
}
如果原有命名不能明显表明类型,则属性命名内要包括类型信息。
//推荐
classConnectionTableViewCell:UITableViewCell{
letpersonImageView:UIImageView
letanimationDuration:NSTimeInterval
//作为属性名的firstName,很明显是字符串类型,所以不用在命名里不用包含String
letfirstName:String
//虽然不推荐,这里用Controller代替ViewController也可以。
letpopupController:UIViewController
letpopupViewController:UIViewController
//如果需要使用UIViewController的子类,如TableViewController,CollectionViewController,SplitViewController,等,需要在命名里标名类型。
letpopupTableViewController:UITableViewController
//当使用outlets时,确保命名中标注类型。
@IBOutletweakvarsubmitButton:UIButton!
@IBOutletweakvaremailTextField:UITextField!
@IBOutletweakvarnameLabel:UILabel!
}
//不推荐
classConnectionTableViewCell:UITableViewCell{
//这个不是UIImage,不应该以Image为结尾命名。
//建议使用personImageView
letpersonImage:UIImageView
//这个不是String,应该命名为textLabel
lettext:UILabel
//animation不能清晰表达出时间间隔
//建议使用animationDuration或animationTimeInterval
letanimation:NSTimeInterval
//transition不能清晰表达出是String
//建议使用transitionText或transitionString
lettransition:String
//这个是ViewController,不是View
letpopupView:UIViewController
//由于不建议使用缩写,这里建议使用ViewController替换VC
letpopupVC:UIViewController
//技术上讲这个变量是UIViewController,但应该表达出这个变量是TableViewController
letpopupViewController:UITableViewController
//为了保持一致性,建议把类型放到变量的结尾,而不是开始,如submitButton
@IBOutletweakvarbtnSubmit:UIButton!
@IBOutletweakvarbuttonSubmit:UIButton!
//在使用outlets时,变量名内应包含类型名。
//这里建议使用firstNameLabel
@IBOutletweakvarfirstName:UILabel!
}
当给函数参数命名时,要确保函数能够理解每个参数的目的。
代码风格
综合
尽可能多使用let,少使用var。
当需要遍历一个集合变形成另一个集合时,推荐使用函数flatMap,filter,和reduce。
//推荐
letstringOfInts=[1,2,3].flatMap{String($0)}
//["1","2","3"]
//不推荐
varstringOfInts:[String]=[]
forintegerin[1,2,3]{
stringOfInts.append(String(integer))
}
//推荐
letevenNumbers=[4,8,15,16,23,42].filter{$0%2==0}
//[4,8,16,42]
//不推荐
varevenNumbers:[Int]=[]
forintegerin[4,8,15,16,23,42]{
ifinteger%2==0{
evenNumbers(integer)
}
}
如果变量类型可以依靠判断得出,不建议声明变量时指明类型。
如果一个函数有多个返回值,推荐使用元组而不是inout参数,如果这个元组在多个地方都会使用,建议使用typealias来定义这个元组,而如果返回的元组有三个或者三个以上的元素,建议使用结构体或类。
funcpirateName()->(firstName:String,lastName:String){
return("Guybrush","Threepwood")
}
letname=pirateName()
letfirstName=name.firstName
letlastName=name.lastName
当使用委托和协议时,请注意避免出现循环引用,基本上是在定义属性的时候使用weak修饰。
在闭包里使用self的时候需要注意避免出现循环引用,使用捕获列表可以避免这一点。
functionWithClosure(){[weakself](error)->Voidin
//方案1
self?.doSomething()
//或方案2
guardletstrongSelf=selfelse{
return
}
strongSelf.doSomething()
}
switch模块中不用显式使用break。
断言流程控制的时候不要使用小括号。
//推荐
ifx==y{
/*...*/
}
//不推荐
if(x==y){
/*...*/
}
在写枚举类型的时候,尽量简写。
//推荐 imageView.setImageWithURL(url,type:.person) //不推荐 imageView.setImageWithURL(url,type:AsyncImageView.Type.person)
在使用类方法的时候不用简写,因为类方法和枚举类型不一样,不能轻易地推导出上下文。
//推荐 imageView.backgroundColor=UIColor.whiteColor() //不推荐 imageView.backgroundColor=.whiteColor()
不建议使用self,除非必须得要。
在写一个方法的时候,需要衡量这个方法将来是否会被重写,如果不是请用final关键字修饰,这样组织方法被重写。一般来说final修饰符可以优化编译速度,在合适的时候大胆的使用它吧。需要注意的是,在一个公开发布的代码库中使用final和在本地项目中使用final的影响差别很大。
在使用一些语句如else,catch等紧随代码块关键字的时候,确保代码块和关键字在同一行。
ifsomeBoolean{
//你想要什么
}else{
//你不想做什么
}
do{
letfileContents=tryreadFile("filename.txt")
}catch{
print(error)
}
访问控制修饰符
如果需要把访问修饰符放到第一个位置。
//推荐 privatestaticletkMyPrivateNumber:Int //不推荐 staticprivateletkMyPrivateNumber:Int
访问修饰符不应该单独另起一行,应和访问修饰符描述的对象保持在同一行。
//推荐
publicclassPirate{
/*...*/
}
//不推荐
public
classPirate{
/*...*/
}
默认的访问修饰符是internal,可省略不写。
当一个变量需要被单元测试访问时,需要声明为internal类型来使用@testableimport{ModuleName}。如果一个变量实际上是private类型,而因为单元测试需要被声明为internal类型,确定添加合适的注释文档来解释为什么这么做。这里添加注释推荐使用-warning:标记语法。
/** 这个变量是private名字 -warning:定义为internal而不是private为了`@testable`. */ letpirateName="LeChuck"
自定义操作符
不推荐使用自定义操作符,如果需要创建函数代替。
在重写操作符之前,请慎重考虑是否有充分的理由一定要在全局范围内创建新的操作符,而不是使用其他策略。
你可以重载现有的操作符来支持新的类型(特别是==),但是新定义的必须保留操作符原来的含义,比如==必须用来测试是否相等并返回布尔值。
switch语句和枚举
使用swift语句时,如果选项是有限集合时,不要使用default,相反的,把一些不用的选项放到底部,并用break关键词阻止其执行。
应为swift中的switch选项默认是包含break的,所以不需要使用break关键字。
case语句应和switch语句左对齐,并在标准的default上面。
当定义的选项有关联值时,确保关联值有恰当的名称,而不只是类型。
enumProblem{
caseattitude
casehair
casehunger(hungerLevel:Int)
}
funchandleProblem(problem:Problem){
switchproblem{
case.attitude:
print("AtleastIdon'thaveahairproblem.")
case.hair:
print("Yourbarberdidn'tknowwhentostop.")
case.hunger(lethungerLevel):
print("Thehungerlevelis\(hungerLevel).")
}
}
推荐尽可能使用fallthrough。
如果default的选项不应该触发,可以抛出错误或断言类似的做法。
funchandleDigit(digit:Int)throws{
case0,1,2,3,4,5,6,7,8,9:
print("Yes,\(digit)isadigit!")
default:
throwError(message:"Thegivennumberwasnotadigit.")
}
可选类型
唯一使用隐式拆包可选型的场景是结合@IBOutlets,在其他场景使用非可选类型和常规可选类型,即使有的场景你确定有的变量使用的时候永远不会为nil,但这样做可以保持一致性和程序更加健壮。
不要使用as!和try!,除非万不得已。
如果对于一个变量你不打算声明为可选类型,但当需要检查变量值是否为nil,推荐使用当前值和nil直接比较,而不推荐使用iflet的语法。并且nil在前变量在后。
//推荐
ifnil!=someOptional{
//你要做什么
}
//不推荐
iflet_=someOptional{
//你要做什么
}
不要使用unowned,unowned和weak修饰变量基本上等价,并且都是隐式拆包(unowned在引用计数上有少许性能优化),由于不推荐使用隐式拆包,也不推荐使用unowned变量。
//推荐
weakvarparentViewController:UIViewController?
//不推荐
weakvarparentViewController:UIViewController!
unownedvarparentViewController:UIViewController
guardletmyVariable=myVariableelse{
return
}
协议
在实现协议的时候,有两种方式来组织你的代码:
使用//MAKR:注释来实现分割协议和其他代码。
使用extension在类/结构体已有代码外,但在同一个文件内。
请注意extension内的代码不能被子类重写,这也意味着测试很难进行。如果这是经常发生的情况,为了代码一致性最好统一使用第一种办法。否则使用第二种办法,其可以代码分割更清晰。使用而第二种方法的时候,使用//MARK:依然可以让代码在Xcode可读性更强。
属性
对于只读属性,提供getter而不是get{}。
varcomputedProperty:String{
ifsomeBool{
return"I'mamightypirate!"
}
return"I'msellingthesefineleatherjackets."
}
对于属性相关方法get{},set{},willSet,和didSet,确保缩进相关代码块。
对于willSet/didSet和set中的旧值和新值虽然可以自定义名称,但推荐使用默认标准名称newValue/oldValue。
varcomputedProperty:String{
get{
ifsomeBool{
return"I'mamightypirate!"
}
return"I'msellingthesefineleatherjackets."
}
set{
computedProperty=newValue
}
willSet{
print("willsetto\(newValue)")
}
didSet{
print("didsetfrom\(oldValue)to\(newValue)")
}
}
在创建常量的时候,使用static关键字修饰。
classMyTableViewCell:UITableViewCell{
staticletkReuseIdentifier=String(MyTableViewCell)
staticletkCellHeight:CGFloat=80.0
}
声明单例属性可以通过下面方式进行:
classPirateManager{
staticletsharedInstance=PirateManager()
/*...*/
}
闭包
如果参数的类型很明显,可以在函数名里可以省略参数类型,但明确声明类型也是允许的。代码的可读性有时候是添加详细的信息,而有时候部分重复,根据你的判断力做出选择吧,但前后要保持一致性。
//省略类型
doSomethingWithClosure(){responsein
print(response)
}
//明确指出类型
doSomethingWithClosure(){response:NSURLResponsein
print(response)
}
//map语句使用简写
[1,2,3].flatMap{String($0)}
如果使用捕捉列表或有具体的非Void返回类型,参数列表应该在小括号内,否则小括号可以省略。
//因为使用捕捉列表,小括号不能省略。
doSomethingWithClosure(){[weakself](response:NSURLResponse)in
self?.handleResponse(response)
}
//因为返回类型,小括号不能省略。
doSomethingWithClosure(){(response:NSURLResponse)->Stringin
returnString(response)
}
如果闭包是变量类型,不需把变量值放在括号中,除非需要,如变量类型是可选类型(Optional?),或当前闭包在另一个闭包内。确保闭包里的所以参数放在小括号中,这样()表示没有参数,Void表示不需要返回值。
letcompletionBlock:(success:Bool)->Void={
print("Success?\(success)")
}
letcompletionBlock:()->Void={
print("Completed!")
}
letcompletionBlock:(()->Void)?=nil
数组
基本上不要通过下标直接访问数组内容,如果可能使用.first或.last,因为这些方法是非强制类型并不会奔溃。推荐尽可能使用foriteminitems而不是foriin0..n。
不是使用+=或+操作符给数组添加新元素,使用性能较好的.append()或appendContentsOf(),如果需要声明数组基于其他数组并保持不可变类型,使用letmyNewArray=[arr1,arr2].flatten(),而不是letmyNewArray=arr1+arr2。
错误处理
假设一个函数myFunction返回类型声明为String,但是总有可能函数会遇到error,有一种解决方案是返回类型声明为String?,当遇到错误的时候返回nil。
funcreadFile(withFilenamefilename:String)->String?{
guardletfile=openFile(filename)else{
returnnil
}
letfileContents=file.read()
file.close()
returnfileContents
}
funcprintSomeFile(){
letfilename="somefile.txt"
guardletfileContents=readFile(filename)else{
print("不能打开\(filename).")
return
}
print(fileContents)
}
实际上如果预知失败的原因,我们应该使用Swift中的try/catch。
定义错误对象结构体如下:
structError:ErrorType{
publicletfile:StaticString
publicletfunction:StaticString
publicletline:UInt
publicletmessage:String
publicinit(message:String,file:StaticString=#file,function:StaticString=#function,line:UInt=#line){
self.file=file
self.function=function
self.line=line
self.message=message
}
}
使用案例:
funcreadFile(withFilenamefilename:String)throws->String{
guardletfile=openFile(filename)else{
throwError(message:“打不开的文件名称\(filename).")
}
letfileContents=file.read()
file.close()
returnfileContents
}
funcprintSomeFile(){
do{
letfileContents=tryreadFile(filename)
print(fileContents)
}catch{
print(error)
}
}
其实项目中还是有一些场景更适合声明为可选类型,而不是错误捕捉和处理,比如在获取远端数据过程中遇到错误,nil作为返回结果是合理的,也就是声明返回可选类型比错误处理更合理。
整体上说,如果一个方法有可能失败,并且使用可选类型作为返回类型会导致错误原因湮没,不妨考虑抛出错误而不是吃掉它。
使用guard语句
总体上,我们推荐使用提前返回的策略,而不是if语句的嵌套。使用guard语句可以改善代码的可读性。
//推荐
funceatDoughnut(atIndexindex:Int){
guardindex>=0&&index=0&&index
在解析可选类型时,推荐使用guard语句,而不是if语句,因为guard语句可以减少不必要的嵌套缩进。
//推荐
guardletmonkeyIsland=monkeyIslandelse{
return
}
bookVacation(onIsland:monkeyIsland)
bragAboutVacation(onIsland:monkeyIsland)
//不推荐
ifletmonkeyIsland=monkeyIsland{
bookVacation(onIsland:monkeyIsland)
bragAboutVacation(onIsland:monkeyIsland)
}
//禁止
ifmonkeyIsland==nil{
return
}
bookVacation(onIsland:monkeyIsland!)
bragAboutVacation(onIsland:monkeyIsland!)
当解析可选类型需要决定在if语句和guard语句之间做选择时,最重要的判断标准是是否让代码可读性更强,实际项目中会面临更多的情景,如依赖2个不同的布尔值,复杂的逻辑语句会涉及多次比较等,大体上说,根据你的判断力让代码保持一致性和更强可读性,如果你不确定if语句和guard语句哪一个可读性更强,建议使用guard。
//if语句更有可读性
ifoperationFailed{
return
}
//guard语句这里有更好的可读性
guardisSuccessfulelse{
return
}
//双重否定不易被理解-不要这么做
guard!operationFailedelse{
return
}
如果需要在2个状态间做出选择,建议使用if语句,而不是使用guard语句。
//推荐
ifisFriendly{
print("你好,远路来的朋友!")
}else{
print(“穷小子,哪儿来的?")
}
//不推荐
guardisFriendlyelse{
print("穷小子,哪儿来的?")
return
}
print("你好,远路来的朋友!")
你只应该在在失败情形下退出当前上下文的场景下使用guard语句,下面的例子可以解释if语句有时候比guard语句更合适–我们有两个不相关的条件,不应该相互阻塞。
ifletmonkeyIsland=monkeyIsland{
bookVacation(onIsland:monkeyIsland)
}
ifletwoodchuck=woodchuckwherecanChuckWood(woodchuck){
woodchuck.chuckWood()
}
我们会经常遇到使用guard语句拆包多个可选值,如果所有拆包失败的错误处理都一致可以把拆包组合到一起(如return,break,continue,throw等)。
//组合在一起因为可能立即返回
guardletthingOne=thingOne,
letthingTwo=thingTwo,
letthingThree=thingThreeelse{
return
}
//使用独立的语句因为每个场景返回不同的错误
guardletthingOne=thingOneelse{
throwError(message:"UnwrappingthingOnefailed.")
}
guardletthingTwo=thingTwoelse{
throwError(message:"UnwrappingthingTwofailed.")
}
guardletthingThree=thingThreeelse{
throwError(message:"UnwrappingthingThreefailed.")
}
文档/注释
文档
如果一个函数比O(1)复杂度高,你需要考虑为函数添加注释,因为函数签名(方法名和参数列表)并不是那么的一目了然,这里推荐比较流行的插件VVDocumenter.不论出于何种原因,如果有任何奇淫巧计不易理解的代码,都需要添加注释,对于复杂的类/结构体/枚举/协议/属性都需要添加注释。所有公开的函数/类/变量/枚举/协议/属性/常数也都需要添加文档,特别是函数声明(包括名称和参数列表)不是那么清晰的时候。
在注释文档完成后,你应检查格式是否正确。
注释文档规则如下:
一行不要超过160个字符(和代码长度限制雷同)。
即使文档注释只有一行,也要使用模块化格式(/*/)。
注释模块中的空行不要使用*来占位。
确定使用新的–parameter格式,而不是就得Usethenew-:param:格式,另外注意parameter是小写的。
如果需要给一个方法的参数/返回值/抛出异常添加注释,务必给所有的添加注释,即使会看起来有部分重复,否则注释会看起来不完整,有时候如果只有一个参数值得添加注释,可以在方法注释里重点描述。
对于负责的类,在描述类的使用方法时可以添加一些合适的例子,请注意Swift注释是支持MarkDown语法的。
/**
##功能列表
这个类提供下一下很赞的功能,如下:
-功能1
-功能2
-功能3
##例子
这是一个代码块使用四个空格作为缩进的例子。
letmyAwesomeThing=MyAwesomeClass()
myAwesomeThing.makeMoney()
##警告
使用的时候总注意以下几点
1.第一点
2.第二点
3.第三点
*/
classMyAwesomeClass{
/*...*/
}
在写文档注释时,尽量保持简洁。
其他注释原则
//后面要保留空格。
注释必须要另起一行。
使用注释//MARK:-xoxo时,下面一行保留为空行。
classPirate{
//MARK:-实例属性
privateletpirateName:String
//MARK:-初始化
init(){
/*...*/
}
}
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。