1. 引言
在Android應(yīng)用的開發(fā)語言上,是從Java再發(fā)展到Kotlin的十拣,所以Kotlin語言的開發(fā)習(xí)慣中不可避免會(huì)帶有Java的痕跡,所以很多關(guān)于Kotlin的語法糖的使用夭问,容易被忽略。而對(duì)于語法糖捧杉,有的人覺得很香,也有人覺得不值一提味抖,各有各想法灰粮。
此文,僅以表述個(gè)人對(duì)于Kotlin語法糖的一些優(yōu)勢(shì)總結(jié):
- 使代碼更簡(jiǎn)潔專注
- 使代碼更貼近閱讀習(xí)慣
- 使代碼更具維護(hù)性
- 使代碼更加安全
2. 使代碼更簡(jiǎn)潔專注
以啟動(dòng)線程執(zhí)行業(yè)務(wù)為例熔脂,
Java代碼:
new Thread() {
@Override
public void run() {
System.out.println("hello");
}
}.start();
Kotlin代碼可以照搬Java方式:
object : Thread() {
override fun run() {
println("hello")
}
}.start()
Kotlin里啟動(dòng)線程還有另一種語法糖:
thread {
println("hello")
}
特別注意:注意是thread
而不是Thread
,如果是后者其實(shí)調(diào)用了 Thread(Runnable target)
的構(gòu)造方法霞揉。
不難看出晰骑,Kotlin里的這個(gè)語法糖非常簡(jiǎn)潔,可以使得開發(fā)者在無論在寫代碼還是代碼閱讀上秽荞,都只需要專注于線程中的執(zhí)行邏輯,而Java代碼則顯得繁瑣蚂会。
中間的Kotlin代碼以Java方式實(shí)現(xiàn),功能上沒有任何問題耗式,因?yàn)镴ava的代碼實(shí)現(xiàn)方式趁猴,在Kotlin中始終可以使用儡司。只不過這時(shí)候代碼結(jié)構(gòu)也與Java完全一致,相當(dāng)于吃了Kotlin語法卻沒吃到糖捕犬,不過既然吃語法酵镜,為何不吃糖?
或許有人疑惑垢粮,這種方式看似簡(jiǎn)潔了靠粪,但是如果只想創(chuàng)建線程,又不想立即啟動(dòng)占键,好像就不能這么寫了?
其實(shí)也可以:
val xThread = thread(start = false) {
println("hello")
}
這樣便不會(huì)立即啟動(dòng)線程君仆,又拿到了線程對(duì)象的引用啸澡,自然可以根據(jù)需要在適當(dāng)?shù)臅r(shí)候再調(diào)用start()
啟動(dòng)線程了氮帐。
其實(shí)嗅虏,Kotlin里的寫法之所以如此簡(jiǎn)潔而且又不局限皮服,是因?yàn)檫@里的thread
是個(gè)函數(shù)参咙,聲明如下:
public fun thread(
start: Boolean = true,
isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null,
name: String? = null,
priority: Int = -1,
block: () -> Unit
): Thread
之所以Kotlin啟動(dòng)線程簡(jiǎn)潔且不局限蕴侧,其實(shí)是結(jié)合多種語法糖的綜合結(jié)果:函數(shù)缺省參數(shù)、函數(shù)類型净宵、拖尾的lambda表達(dá)式裹纳。
最后剃氧,放一起更直觀地對(duì)比Kotlin代碼中以Java方式實(shí)現(xiàn)以及語法糖方式實(shí)現(xiàn)的啟動(dòng)線程的區(qū)別:
object : Thread() {
override fun run() {
println("hello")
}
}.start()
thread {
println("hello")
}
不妨想想阻星,上下兩種方式,在代碼閱讀維護(hù)的時(shí)候滥酥,何種方式的代碼更好?
下面的語法糖果不重要么恨狈?確實(shí)不重要呛讲,因?yàn)榫退悴怀赃@個(gè)糖,Java方式的Kotlin代碼也可以實(shí)現(xiàn)贝搁,但是吃下這顆糖,可以使代碼更簡(jiǎn)潔專注啊弦讽,何樂而不為膀哲?
畢竟,是語法糖某宪,不甜的話,能叫糖么兴喂?
這里僅討論語法糖本身內(nèi)容,Kotlin中的函數(shù)類型設(shè)計(jì)不僅限于語法糖衣迷,更多內(nèi)容可參照:
函數(shù)類型壶谒,一個(gè)更好的選擇
3. 使代碼更貼近閱讀習(xí)慣
本節(jié)將以Java的靜態(tài)函數(shù)和Kotlin的拓展函數(shù)類作為對(duì)比。
以前Java代碼中让禀,經(jīng)常會(huì)這樣判斷一個(gè)字符串是否為null或空字符串:
if (TextUtils.isEmpty(str)) {
System.out.println("str is null or empty");
}
在Kotlin中,可以這樣:
if (str.isNullOrEmpty()) {
println("str is null or empty")
}
Java里的靜態(tài)函數(shù)和Kotlin里的拓展函數(shù)更直觀的對(duì)比:
TextUtils.isEmpty(str) // Java靜態(tài)函數(shù)
str.isNullOrEmpty() // Kotlin拓展函數(shù)
按照從左往右的閱讀習(xí)慣堆缘,拓展函數(shù)更符合閱讀習(xí)慣。
如上面例子的目標(biāo)其實(shí)是:判斷字符串是否為null或空字符串录平。
所以str.isNullOrEmpty()
更符合閱讀思維缀皱,從左到右閱讀出來的信息即為上述意義斗这,而Java中的TextUtils.isEmpty(str)
一行從左到右閱讀起來則是文字工具中判斷是否為空方法函數(shù)調(diào)用,函數(shù)參數(shù)再傳入被判斷的字符串啤斗。
以前Java里是沒得選表箭,因?yàn)镴ava中沒有拓展函數(shù),所以要用靜態(tài)方法來實(shí)現(xiàn)钮莲;但Kotlin有拓展函數(shù)這個(gè)語法糖了免钻,所以可以用更符合閱讀習(xí)慣的方式進(jìn)行封裝和調(diào)用了。
簡(jiǎn)單作個(gè)類比崔拥,現(xiàn)金支付類比于Java靜態(tài)函數(shù)极舔、手機(jī)支付類比于Kotlin拓展函數(shù),有得選的情況下链瓦,相信大多數(shù)人會(huì)選擇手機(jī)支付(Kotlin拓展函數(shù))更為方便拆魏,但沒得選的情況下(商家不支持手機(jī)支付,類似于沒有Kotlin語言支持時(shí))慈俯,那么現(xiàn)金支付已足夠支持完成交易渤刃。
手機(jī)支付也好,現(xiàn)金支付也罷贴膘,本質(zhì)上都是貨幣交易的載體卖子;類似的Kotlin拓展函數(shù)也好,Java靜態(tài)函數(shù)也罷揪胃,本質(zhì)上都是JVM命令的編譯與執(zhí)行。
手機(jī)支付的存在并不是為了否定現(xiàn)金支付阳似,同理Kotlin拓展函數(shù)的存在也不是為了否定Java靜態(tài)函數(shù)的意義撮奏,只是Kotlin的拓展函數(shù)提供了更貼近閱讀習(xí)慣的使用方式泽疆。
4. 使代碼更具維護(hù)性
本節(jié)以Kotlin中的屬性訪問器(getter/setter)設(shè)計(jì)為例殉疼。
Java里關(guān)于屬性有下述經(jīng)典寫法:
public class JavaDemoBean {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
假定需要對(duì)data數(shù)據(jù)統(tǒng)一增加后綴"@test",所以getData()
里稍微改下:
public String getData() {
return data + "@test";
}
但是眠砾,這里有個(gè)問題褒颈,因?yàn)轭悆?nèi)部是可以直接訪問私有的data數(shù)據(jù)的,所以類內(nèi)部直接訪問屬性data的地方都需要統(tǒng)一使用getData()
才能達(dá)到效果淤井,而且后續(xù)新增代碼時(shí)扔直接訪問私有變量而造成額外的代碼評(píng)審和維護(hù)成本。
相對(duì)的漩绵,Kotlin中的getter/setter訪問器方法可解決上述情形的痛點(diǎn):
class KotlinDemoBean {
var data = ""
get() = "${field}@test"
}
這樣止吐,無論是類內(nèi)訪問還是類以外的地方調(diào)用,訪問屬性的時(shí)候都自動(dòng)加上了后綴不同,這樣就滿足了當(dāng)前需求:每次訪問數(shù)據(jù)值時(shí)都會(huì)給真實(shí)的數(shù)據(jù)值加上了相應(yīng)后綴服鹅。
更簡(jiǎn)潔,且更好維護(hù)澜倦。
注:內(nèi)存里儲(chǔ)存的對(duì)象仍將是不帶后綴的字符串藻治,只是每次讀取屬性時(shí)拼接了后綴桩卵,這時(shí)候?qū)傩缘哪缓笞侄卧趯傩栽L問器以外的地方將無法讀取。
這里稍微延展下钩乍,Java方式里寥粹,可以同時(shí)通過私有變量直接訪問到字段原本值和加上后綴的內(nèi)容,但是Kotlin的上述寫法則無法做到讀取字段原本值阔拳。其實(shí)Kotlin里要做到同時(shí)訪問兩者也可以,不過相對(duì)麻煩點(diǎn)货裹,這里用幕后屬性方式作為示例:
class KotlinDemoBean {
private var _data: String = ""
var data: String
get() = "${_data}@test"
set(value) {
_data = value
}
}
這樣的話,如果想訪問不帶后綴的屬性墓阀,則使用_data
,想使用自動(dòng)加后綴的屬性,則使用data
溢十。
如果不需要訪問原來值张弛,則是前面非常簡(jiǎn)潔的代碼,相對(duì)地刻剥,需求更多時(shí)再產(chǎn)生更多的代碼是合理的。
更多細(xì)節(jié):Kotlin在默認(rèn)情況下類外部調(diào)用屬性總是通過訪問器間接讀寫的,想要如Java那樣直接暴露Kotlin類的屬性字段而不需要訪問器函數(shù)封裝撵术,注解@JvmField 又是一種選擇嫩与。
總而言之埃篓,Kotlin的屬性訪問器語法糖設(shè)計(jì)同窘,只是在原來Java的基礎(chǔ)上裤纹,使得代碼維護(hù)性變得更好了呕童。
5. 使代碼更加安全
Kotlin與Java的差異中,空安全總是最為突出的一個(gè)點(diǎn)茫蛹,提供的語法糖也是最為繁雜的婴洼。
在Java中粉捻,關(guān)于判空杏头,一般會(huì)這樣寫:
public class JavaExample {
private Dialog dialog;
public void showDialog() {
if (dialog != null) {
dialog.setTitle("abc");
dialog.show();
}
}
}
Kotlin中如果照搬Java寫法寓娩,則會(huì)出現(xiàn)下面這種代碼:
class KotlinExample {
private var dialog: Dialog? = null
fun showDialog() {
if (dialog != null) {
dialog!!.setTitle("abc")
dialog!!.show()
}
}
}
注意,最后調(diào)用dialog的對(duì)象方法的時(shí)候缰犁,如果沒有操作符!!
上述代碼將提示編譯錯(cuò)誤。
是不是很奇怪?為什么在調(diào)用方法前已經(jīng)進(jìn)行了判空蕴茴,為什么這里還是要加!!
才能通過編譯?
其實(shí),不加!!
時(shí)Android Studio已經(jīng)提示具體原因了:
簡(jiǎn)單來說糠雨,就是Kotlin檢測(cè)到在調(diào)用對(duì)象方法時(shí)dialog屬性可能已經(jīng)被改變才睹。
更具體的說,由于dialog是可空且是可變的解恰,所以可能在判空條件成立后,調(diào)用對(duì)象方法之前已經(jīng)被另一線程置空,所以即使進(jìn)行了判空后再調(diào)用對(duì)象方法羞酗,仍可能產(chǎn)生空指針。
所以卫枝,這時(shí)候在Kotlin里照搬Java的代碼風(fēng)格就顯得不合適了马篮。
Kotlin里正確的條件判空姿勢(shì)浑测,應(yīng)該是下面這種用法:
fun showDialog() {
val curDialog = dialog
if (curDialog != null) {
curDialog.setTitle("abc")
curDialog.show()
}
}
本質(zhì)上是利用局部變量進(jìn)行判空夭委。
為什么對(duì)局部變量進(jìn)行判空有效而對(duì)成員變量進(jìn)行判空則仍編譯錯(cuò)誤厕氨?
這時(shí)候要提及JVM堆棧內(nèi)存的概念了进每,JVM中,堆內(nèi)存是所有線程共享的命斧,棧內(nèi)存僅為某線程運(yùn)行時(shí)所私有田晚,而所有對(duì)象都創(chuàng)建在堆內(nèi)存上,Java中的變量只是對(duì)象的一個(gè)引用(指針值)国葬,而類中的成員變量在堆內(nèi)存中分配贤徒,而局部變量則在棧變量分配,所以成員變量判空后再執(zhí)行對(duì)象方法時(shí)可能已經(jīng)被另一線程所置空汇四,而局部變量則沒有這種可能接奈。
這時(shí)候估計(jì)會(huì)有疑惑:代碼這里有沒有涉及多線程調(diào)用,為什么非要考慮到多線程的影響呢船殉?
Java里對(duì)于空安全是交于代碼運(yùn)行時(shí)的鲫趁,Kotlin對(duì)于空安全是交于編譯時(shí)的(除非使用!!
)斯嚎,一個(gè)是出現(xiàn)問題再改利虫,一個(gè)是強(qiáng)迫在寫代碼的時(shí)候就去處理問題。既然Kotlin設(shè)計(jì)了空安全堡僻,那么還是得考慮多線程情況下的空安全糠惫。
注:Kotlin的空安全只是保證了多線程時(shí)不會(huì)觸發(fā)空指針異常,但是沒有保證數(shù)據(jù)在多線程條件下的一致性和同步性钉疫。
對(duì)于這種局部變量的判空方式硼讽,肯定是較為麻煩的,起名糾結(jié)癥患者甚至對(duì)此嗤之以鼻牲阁,所以又有下面這種語法糖寫法:
fun showDialog() {
dialog?.setTitle("abc")
dialog?.show()
}
看起來簡(jiǎn)潔又安全固阁,但卻不夠優(yōu)雅。
首先城菊,Kotlin中每個(gè)?
操作符判空的背后备燃,都是一次對(duì)于局部變量的賦值與判空。
也就是凌唬,這里有兩次判空的命令執(zhí)行并齐。
更好的方式,是利用標(biāo)準(zhǔn)函數(shù)來優(yōu)化重復(fù)的判空操作:
比如用let
:
fun showDialog() {
dialog?.let {
it.setTitle("abc")
it.show()
}
}
這里個(gè)人更傾向于用run
客税,因?yàn)閞un的單詞語義與上下文表意更貼切:
fun showDialog() {
dialog?.run {
setTitle("abc")
show()
}
}
兩者在此時(shí)本質(zhì)上都是在編譯時(shí)產(chǎn)生了一個(gè)局部變量進(jìn)行判空并調(diào)用况褪,原理上等同于上面的手寫局部變量條件判空的方式,只不過這時(shí)候更耻,局部變量名交予了編譯器產(chǎn)生测垛。
Kotlin的空安全語法糖,無論哪一種較之于Java復(fù)雜一些秧均,但是對(duì)于空指針確實(shí)更安全了食侮!
6. 關(guān)于對(duì)語法糖的思考
有人調(diào)侃脊奋,Kotlin一個(gè)最明顯的特點(diǎn)便是語法糖永遠(yuǎn)比Java多。
退一步說疙描,即使不用Kotlin的各種語法糖诚隙,按照J(rèn)ava的方式也基本可以實(shí)現(xiàn)功能,所以為什么要去過多糾結(jié)Kotlin設(shè)計(jì)的語法糖呢起胰?
這里借用魯迅先生的《孔乙己》中回字的四種寫法來作類比久又,一般慣以回字四種寫法嘲諷無用的知識(shí)和炫耀,然而把這件事剝開看效五,如果讀書人只會(huì)炫耀回字的四種寫法地消,那自然是無用知識(shí)。但是畏妖,如果能理解不同寫法的使用場(chǎng)景脉执、字體含義,在合適的前后文(字體戒劫、書函半夷、學(xué)科等)中正確地使用不同的回字,以達(dá)到更貼切迅细、更豐富的表達(dá)巫橄,這時(shí)候回字的不同寫法的知識(shí)一定程度上便是有意義的內(nèi)容。
只著眼于回字的四種寫法表面茵典,自然是無用知識(shí)湘换,魯迅先生借此諷刺的是封建制度對(duì)于讀書人的限制以及摧殘。
更多地统阿,只著眼于知識(shí)本身而忽略對(duì)于知識(shí)本身的理解彩倚,這種學(xué)習(xí)是否正確呢?
回到開發(fā)語言上扶平,只著眼于語法糖本身而去否定語法糖帆离,忽略對(duì)語法糖設(shè)計(jì)的理解,是否正確蜻直?
從Java轉(zhuǎn)戰(zhàn)Kotlin過程盯质,過于著眼于Kotlin的語法糖本身而忽略了語法糖設(shè)計(jì)帶來的變化和改進(jìn),忽略了對(duì)于Kotlin語言本身的設(shè)計(jì)的理解概而,是否正確呼巷?
這些問題,每個(gè)人有不同的思考赎瑰,這里也不是應(yīng)試教育不必追求標(biāo)準(zhǔn)答案王悍,本文也僅給出個(gè)人目前對(duì)于Kotlin語法糖的初步總結(jié):
- 使代碼更簡(jiǎn)潔專注
- 使代碼更貼近閱讀習(xí)慣
- 使代碼更具維護(hù)性
- 使代碼更加安全
這些內(nèi)容或許可以有更多的維度的解讀和理解,與語法糖本身相比餐曼,語法糖背后的思考與理解或許會(huì)更重要压储。