1. 程式人生 > >【騰訊Bugly乾貨分享】你為什麼需要 Kotlin

【騰訊Bugly乾貨分享】你為什麼需要 Kotlin

一、往事

曾經你有段時間研究 Intellij 的外掛開發,企圖編譯 Intellij Idea Community Edition (ICE)的原始碼,結果發現有個奇怪的東西讓你的程式碼無法編譯。。什麼鬼,kt 是什麼玩意兒?

怎麼又有新語言出來啊,簡直要瘋掉了。這時候,你的腦海裡面瞬間浮現出了這句話:

有困難要上,沒困難製造困難也要上。

『靠,這尼瑪究竟是誰說的,好有道理!』你調侃道。

為了不丟掉社會主義新青年勤奮刻苦的優良傳統,你決定學一下 Kotlin,不過說真的,這決定也是坑苦了自己,畢竟那段時間 Kotlin 的 API 還沒有趨於穩定,經常從網上找到個 demo,搞到本地就編不過去,哭死。直到 2016年2月,Kotlin 1.0 正式釋出,凌亂的 API 也隨著曾經躁動的心的平靜而穩定下來,你無需再忍受什麼,甚至還有了一種『終於看著娃長大了』的感覺。

二、消失的 Getter 和 Setter

你一直很喜歡 Java,就像你一直喜歡喝黑咖啡一樣。原因也很樸實,因為你別的也不怎麼會啊。可有一陣子做一個語音聊天的 app,裡面各種使用者、通話記錄等等的資料結構,簡直了,寫起來長長的一串,光 Getter 和 Setter 就一眼望不到邊,每寫一個數據結構類,彷彿眼前就是那金黃色的稻田,你吹一口咖啡,它們居然還簌簌作響。最諷刺的是,你發現大家在寫有關 Java 的文章的時候,遇到資料結構實體類,經常會這樣寫:

public class Person{
    private int id;
    private String name;
    //瞅啥瞅,省略掉的是 getter 和 setter!
... }

額,這麼重複的程式碼居然佔用了 80% 的篇幅,也難怪是個 IDE 就有幫忙生成 Getter 和 Setter 模板的功能。

你說你曾經試圖不寫 Getter 和 Setter,可作為一個寫 Java 這麼多年的人,沒了 Getter 和 Setter 讓你感覺就像是。。。

那時候你看到 C# 裡面的屬性也真是眼饞吶,『怎麼 Java 就不能搞這麼個特性呢?』

後來,你發現 Kotlin 居然是有屬性的:

data class Person(val id: Int, val name: String)

你眼前一亮,放下手中的咖啡,幻想著啥時候去 Kotlin 小島玩一趟。

三、又見空指標

你的專案都接入了 Bugly,那趕腳就好像在雞蛋上跳舞哇,每天去開啟崩潰統計都心驚肉跳的。這些 crash 裡面絕大多數都是空指標異常,這倒不是說空指標本身有什麼問題,空指標只能說明程式有考慮不周的情形出現,出現空指標呼叫通常都是程式碼的編寫問題,那麼為什麼 Java 會允許潛在的空指標存在呢?你望著窗外,思索著一定是什麼矇蔽了你的雙眼,讓你看不透,望不穿,尋不見。

person.setName("橘右京");

回過神來,突然看到這行程式碼,你嘟囔著,這哥們名字叫 橘右京,可這哥們是誰?萬一他是個 null 呢?

Person person = findPersonFromCacheOrCreate();
person.setName("橘右京");

你的目光往上移,看到了上面那行賦值,你開始懷疑,開始問自己:『
害怕不。。其實我們一直都沒有意識到,我們的 Java 程式碼 處處不讓我們放心。』

說來也巧,你最近在梳理專案程式碼的時候,見到的最多的就是:

if(x != null) x.y();

你對你的程式碼充滿了不信任,你的程式碼會回報你什麼呢?

『虛擬機器空指標全家桶來一份!。。。那一定很好吃吧』你一臉無奈的自嘲著。

你突然發現這程式碼居然看上去跟窗外的世界很像,只是,給程式碼用的淨化器應該是什麼牌子的呢?

『小米的怕是不行了吧。』你哈哈一笑,似乎對此感到很開心。

那開心轉瞬即逝,你不得不面對這令人苦惱的現狀。你在 Java 當中除了對自己說『我保證 findPersonFromCacheOrCreate() 不會返回空』,還有什麼更讓人踏實的辦法麼?當然沒有。

『看看 Kotlin 有沒有好辦法吧!』於是你嘗試著用 Kotlin 寫下了類似的程式碼:

fun findPersonFromCacheOrCreate(): String{
    ...
}

當你企圖在這個方法中返回 null 時,聰明的 IntelliJ 立即在你的程式碼上畫出紅線,告訴你不要這樣。

你查了下資料,發現原來在 Kotlin 當中, String 表示一個不可為 null 的字串型別。這一刻,你的內心感到無比踏實:

val person = findPersonFromCacheOrCreate()
person.name = "橘右京"

寫 findPersonFromCacheOrCreate 這個方法的人必須給你保證返回的 person 不為 null,他在編寫這個方法的時候就要百般小心,不然編譯器就要削他了。

『找削襖!』你突然想起微信裡面的那套 桃子一家東北話 的表情,想想就好歡樂。

『不過』,你又想,『萬一它還是給我返回了 null 怎麼辦呢?』說著你按照 Kotlin 的要求改了下程式碼:

fun findPersonFromCacheOrCreate(): String?{
    ...
}

結果發現下面的第二行報錯。

『什麼情況?』你不明白了。

val person = findPersonFromCacheOrCreate()
person.name = "橘右京"

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

『就是說,如果有人膽敢給我返回個可空型別,我必須做判空處理才可以行唄?』你眼前一亮,旋即驚呼道,『太厲害了!』

緊接著你發現,雖然返回的是可空型別,但這絲毫不會影響你與你的程式碼談笑風生,因為 Kotlin 可以給你一千種選擇讓你的程式碼看起來猶如行雲流水一般,比如你希望拿到 null 直接返回,你就這麼寫:

val person = findPersonFromCacheOrCreate()?:return

或者這裡沒辦法用 return,你一樣可以確保 person 不為空的時候執行你的邏輯:

val person = findPersonFromCacheOrCreate()
person?.let{
    //在這裡直接用 it 指代 person,絕對不為空
}
person?.setName("安琪拉")//只有person不為空的時候執行

四、Smart Cast

『聽說風已經到張家口了。』亞瑟說。
『哦是麼,那我們可真得拭目以待呀。』你淡定地說。
『待什麼,眼前一亮嗎,哈哈。』也樂了。
『是啊,不過,眼下我倒更希望 Java 能聰明一點兒。。』
『它怎麼了?』
『你看,Java 有一種特別缺心眼的寫法:

if(view instanceof ViewGroup){
    ((ViewGroup) view).addView(child);
}

強轉加方法呼叫,兩對括號,寫到手抽筋啊。可我已經告訴 Java view 是 ViewGroup 了啊,結果還是要強轉,這種感覺就像我坐地鐵的時候本來刷卡進站,結果到了車上,還有人查票!』說完,你忽然覺得口渴,隨手拿起熱乎的咖啡喝了一口,你眉頭一皺,那味道似乎不太好。

『雖然我們寫程式碼應該儘量避免強轉,可你明明知道這東西我們無法避免,於是本來想多型的用父類或者介面引用例項,結果強轉程式碼寫得多到變態。要是我在判斷了 view 的型別之後,在這個型別判斷有效的作用域內不用做強轉就好了。』你接著說道,一臉的煩惱。

『Kotlin 可以呀,Kotlin 有個特性叫 Smart-Cast,你寫的程式碼就可以像這樣:

if(view is ViewGroup){
    view.addView(child) // 現在 Kotlin 已經知道 view 是 ViewGroup 型別了!
}

咋樣,厲害吧?』亞瑟一臉神氣的表情。

『真的呀!』你興奮的說。

『這你都不知道?』在一旁打遊戲的太2真人一如既往地調侃道,他看上去似乎比亞瑟更得意。

五、打日誌

『你們連日誌都打,真不要臉。。對了,日誌是誰,打的時候叫上我啊!』你露出一臉的壞笑。

原來,你有個函式傳入了三個引數,

void check(ArrayList<String> list, String tag, int id);

你想把他們的值列印一下,於是你不假思索地敲出了一行程式碼:

System.out.println("list: "+list.size()+"; tag="+tag+";id="+id);

這樣的語句並沒有引起你的任何不適——畢竟,你早已習慣了它的醜陋。你稍稍停頓,活動了一下手指,突然想到那個經久不衰的段子:

女神:你能讓這個論壇的人都吵起來,我今晚就跟你走。
程式猿:PHP語言是最好的語言!
論壇炸鍋了,各種吵架。
女神:服了你了,我們走吧,你想幹啥都行。
程式猿:今天不行,我一定要說服他們,PHP語言是最好的語言。

『難怪 PHP 是最好的語言,沒有之一呢!』你自嘲著,『至少人家支援字串模板呀。』

$size = count($list);
echo "list: $size; tag=$tag; id=$id";

說來也是好奇,這麼好的特性 Kotlin 有沒有呢?這傢伙最近給你帶來的驚喜是在太多了,於是你決定試一試。

println("list: ${list.size}; tag=$tag; id=$id")

『嗯————』你滿意的點了點頭。

六、再見,Utils

你肯定用過 String,不僅如此,你還知道 String 居然連個 empty 方法都沒捨得提供,每次都要寫 str.equals(“”) ,真是無比醜陋。這還算好的,如果恰好要判斷不為空呢?這個應該更常用吧,於是你就會寫 !str.equals(“”),搞不好幾個字元敲完了發現,尼瑪,還得加個小括號 (!str.equals(“”))。。。

『怎麼可以這麼反人類!!!』你嘟囔著,一氣之下寫了個工具類:

StringUtils.java

public class StringUtils{
    public static boolean notEmpty(String str){
        return !"".equals(str);
    }
    public static boolean isEmpty(String str){
        return "".equals(str);
    }
}

你滿意的點點頭,覺得這樣就再也不用忍受那些醜陋了。

隨後的某一天,陽光依舊試圖穿過層層霧霾照到你那北向的屋子的樓的正面,無果,於是垂頭喪氣的點了一支菸。而你呢,正要寫下一句叫做 if(StringUtils.notEmpty(str)){ … } 的程式碼,你很快地寫完 if(str. 疲憊的雙眼企盼著 IDE 意會你的小眼神,可它蒙了,String 並不曾有一個叫做 notEmpty 的方法啊。這時候你遲疑了一下,緩過神來,靈活的把右手小拇指從鍵盤上的 ;移到 backspace,刪掉 str. 重新寫出 if(StringUtils.notEmpty(str))。多麼令人苦惱的經歷啊,於是你惆悵的點了一支菸,屋裡的空氣淨化器也開始飛速的轉了起來。

『我要是能重寫一下 Java 的 String 類好了,我一定先給它加上這倆方法!!』

這時,只見一道亮光閃過,你的窗戶上映出了幾行字:

你驚喜的差點兒喊出聲來。『這真的是 Kotlin 嗎?』你有點兒不敢相信自己的眼睛。是的,有了擴充套件方法,你再也不需要什麼 XXXUtils 了。

七、晚安,ButterKnife

『晚安。』你輕輕地對 ButterKnife 說到。你知道這也許是最後一次這樣說了,畢竟在 Kotlin 的世界裡,ButterKnife 開始變得有些不知所措。

『你不需要我了。』ButterKnife 有些疲憊。
『不,你是最棒的。』突如其來的這麼一句話,讓你顯得有些慌亂。
『kotlin-android-extensions 還不夠麼?』ButterKnife 不耐煩了。
『可。。可是。。。』你不知道該怎麼回答了,畢竟在 Kotlin 出現之後,你很少提及 ButterKnife 了。

是啊,過去你的 View 都是用 ButterKnife 注入的:

@BindView(R.id.nameView) TextView nameView;
...
nameView.setText("橘右京");
...

而現在呢?你搖搖頭,顯得有些無奈。『再也不需要注入 View 了是麼?』你在問自己,儘管對過去百般不捨,可你還是很欣賞你的程式碼現在的樣子:

nameView.text = "橘右京"

再也不需要 ButterKnife 了,更不需要什麼 findViewById 了——這就是命。

八、請和我的代理談吧

聽說有人把 SharedPreference 先生告上法庭了,說他總是在接受新值之後不去做持久化,SP 先生卻覺得很委屈:

『先生們,大家都是紳士,我實在想不出什麼能比我的信譽更重要。如果你們在給我們更新值的時候呼叫 commit,我們一定會按照約定完成持久化的。可你們為什麼就不願意 commit 呢?』SP 先生大惑不解。

『請問 SP 先生,我是 《Dalvik 日報》記者,我想問一下,為什麼必須要 commit 呢?』
『您好,這是規定。』SP 先生慢條斯理的回答道。
『可這有點兒反人類呀!』記者追問道。
『沒關係,基於此次訴訟事件的教訓,我們特意引進了高科技人才 Preference< T > 先生,P先生在這方面可是行家了。』

記者打量著這位 P 先生,試圖從他身上發現點兒什麼。

『哈哈,P 先生是一位 Delegate 吧,SP 先生就是 Delegated Properties 咯~』你從人群中走出來,淡定的說道。

『不錯,』P 先生扶了扶眼鏡,『這位先生,SP 先生的操作方案是有些繁瑣,我們來看個例子:

context.getSharedPreferences("name", Context.MODE_PRIVATE)
    .edit().putString("key", "value").putInt("intKey", intValue).commit();

一位紳士需要操作它的時候,總是首先要獲取 SharedPreferences 例項,接著 edit 拿到 Editor 例項才能存入值,一方面存入的值存在隨意性,key 的值必須約束好才行,否則讀取方就無法獲取到值,另一方面只有 commit 之後值才可以被存入。這樣操作起來確實不是很友好。』

『那 P 先生高見?』你很好奇。

『從今天起,大家如果有需要 SP 先生持久化資料的需求,只需要在我這裡登記一次,剩下的,大家只需要像讀寫變數一樣操作即可生效。』P 先生停頓了一下,打量了一下四周,大家都在注視著他,而一旁的 SP 先生也給了他一個堅定的眼神。

『那麼以後,如果有位紳士需要我們,比如他需要持久化的資料名叫 “name”,值叫 “橘右京”,當然這個值也是可以修改的,那麼他只需要這樣操作:

var name by Preference(context, "name", "橘右京", "sp_name")
...
Log.d(TAG, name) // 第一次讀取,只能讀取到預設值,那就是 橘右京
name = "不知火舞"
Log.d(TAG, name) // 這裡輸出的就是 不知火舞 啦

我們真正做到了讀寫持久化資料就如同讀寫記憶體變數一樣簡單直接。』

『真的好贊。』你不禁鼓起掌來。『那 P 先生,我能讀一下你的原始碼麼?』

……

突然,你的手機振動了一下,打斷了你的思緒。你從沉思中回來,發現你眼前不過仍然是你的 IDE,而螢幕上的這段程式碼,正是 P 先生的原始碼。真的是太巧妙了:

class Preference<T>(val context: Context, val name: String, val default: T, val prefName: String = "default") : ReadWriteProperty<Any?, T> {
    val prefs by lazy { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) }
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(name, default)
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }
    private fun <U> findPreference(name: String, default: U): U = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }
        res as U
    }
    private fun <U> putPreference(name: String, value: U) = with(prefs.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can be saved into Preferences")
        }.apply()
    }
}

『Impressive.』你一副戀戀不捨的樣子。『 SP 先生的全名應該不是 SharedPreferences 吧,我覺得他們一定是搞錯了,應該是 SophisticatedPreferences 還差不多。P 先生自然就是簡化的它咯。』

九、壯士一去兮為啥不復還?

這個問題你想了很久。是荊軻的匕首不夠快?還是不夠長?

『總之是不好用唄。』你嫌棄地說。

Java 裡面也有一副利刃,叫做 Dagger,這把利刃可以幫你生成一些程式碼。

『如果程式碼可以聰明到自己寫程式碼,那我們不就要失業了嗎?』不知道你說這話是在調侃,還是感到有些恐慌。

Kotlin 之前是無法使用這把利刃的,這可能真的打擊了不少人的積極性。不過,這已經不是問題了,因為你在前不久讀到 Kotlin 1.0.4 的更新說明的時候,就已經發現 kapt 的存在。只要你新增 apply plugin: “kotlin-kapt” 這句配置,你就可以像在 Java 當中一樣使用 Dagger 了——你甚至還做了個 demo 試了一下,程式設計師嘛,總是無法擺脫成功寫出一個 Hello World 程式時獲得的內心的愉悅感。

『似乎除了 FindBugs 之類與 Java 語法緊密結合的框架不能直接應用到 Kotlin 上,別的都沒有什麼問題哎。』你似乎發現了什麼。

Java 和 Kotlin 的對話

『Java 叔叔,我。。我怕。。。』Kotlin 怯懦的說。
『有叔叔在呢。』Java 拍著胸脯,安慰道。『世界是你們的,也是我們的,但是歸根結底是你們的。你們青年人朝氣蓬勃,正在興旺時期,好像早晨八九點鐘的太陽。希望寄託在你們身上。孩子,放手去幹吧,搞不定的,找叔叔,叔叔雖然老了,但叔叔還是有自信能幫你搞定。』

你突然回過神來,『我腦洞怎麼這麼大,哈哈哈~』

Java 和 Kotlin 的對話 2

『Java 叔叔,我。。我怕。。。』Kotlin 怯懦的說。
『有叔叔在呢。』Java 拍著胸脯,安慰道。『世界是你們的,也是我們的,但是歸根結底是你們的。你們青年人朝氣蓬勃,正在興旺時期,好像早晨八九點鐘的太陽。希望寄託在你們身上。孩子,放手去幹吧,搞不定的,找叔叔,叔叔雖然老了,但叔叔還是有自信能幫你搞定。』
『好的,叔叔,那我就不客氣了!』Kotlin 嬉皮笑臉的樣子真得很像個小孩子。
『我去,居然裝可憐騙你叔叔,看我不打你!』Java 氣急敗壞道。

『完了,腦洞合不上了。。』你已經陶醉了其實。

在你看來,Kotlin 似乎並不是一門新的程式語言,它看上去更像 Java 的語法糖,只不過,這糖放的徹底了些。

『嗯。。挺甜~』你喝了口咖啡,不過這次不同以往,你放了糖。

更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:

騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智慧合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的程式碼行,實時上報可以在釋出後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!