Kotlin 初窺門徑[1]-基礎概念
Kotlin 是由 JetBrains 開發的基於JVM的語言。JetBrains 因為創造了一個強大的Java開發 IDE(Intellij) 而被大家所熟知。Android Studio 就是基於Intellij IDEA 的開源社群版,今年的I/O大會上谷歌宣佈 Kotlin 正式成為 Android 的官方語言。
Kotlin 是使用 Java 開發者的思維被建立的,相比於 JAVA 具有如下優勢:
- 簡潔,通過支援 variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation 等實現,你可以編寫少得多的程式碼。
- 安全,在我們編譯時期就處理了各種null的情況,避免了執行時異常。
- 函式式,Kotlin是基於面向物件的語言,但它使用了很多函數語言程式設計的概念,如,使用lambda表示式來更方便地解決問題。
- 完美相容JAVA,可以繼續使用所有的你用Java寫的程式碼和庫,甚至可以在一個專案中使用Kotlin和Java兩種語言混合程式設計。
包
為了方便管理大型軟體系統中數目眾多的類,解決類的命名衝突問題,類似於Java,Kotlin同樣引入包(package)機制,提供類的多重類名空間。
宣告包
package語句必須是檔案中的第一行非註釋的程式程式碼。
package foo.bar
fun doSomething (){} // 定義的函式
class User(){} //定義的類
原始檔的所有內容(比如類和函式)都被包宣告包括。 因此在上面的例子中,doSomething() 的全名應該是 foo.bar.doSomething , User 的全名是 foo.bar.User 。
如果沒有任何包宣告的話,則當中的程式碼都屬於預設包,匯入時包名即為函式名!
匯入包
匯入一個單獨的名字
import foo.Bar
匯入範圍內的所有可用的內容 (包, 類, 物件, 等等):
import foo.*
如果將兩個含有相同名稱的的類庫以 "*" 同時匯入,將會存在潛在的衝突。例如:
import net.mindview .simple.*
import java.until.*
由於java.util.和net.mindview.simple.都包含Vector類。在建立一個Vector類時,編譯器並不知道呼叫哪個類庫的類,從而會報錯誤資訊。
而此時就是 Kotlin 中強大的 as
關鍵字大顯神威的時候了,此關鍵字用於區域性重新命名,以解決衝突。
import net.mindview.simple.Vecotr
import java.until.Vecotr as aVector
val v : Vecotr = Vector()
val vA : aVector = aVector()
import 關鍵字不侷限於匯入類,您也可以使用它來匯入其他宣告:
- 頂級函式與屬性
- 在物件宣告中宣告的函式和屬性
- 列舉常量
類
Kotlin中的類遵循一個簡單的結構。儘管與Java有一點細微的差別。你可以使用 try.kotlinlang.org 在不需要一個真正的專案和不需要部署到機器的前提下來測試一些簡單的程式碼範例。
定義一個類
如果你想定義一個類,你只需要使用 class
關鍵字。
class Person {
}
它具有一個預設唯一的無參建構函式,如果我們想使用有參的參建構函式,只需要在類名後面寫上它的引數:
class Person (name: String, var age: Int) {
init {
// init
}
}
而 init
方法是建構函式的函式體,可以用來執行一些初始化的工作。
在上面的程式碼時,我們並沒有使用
;
結尾,當然你也可以使用分號結尾,但這不是必須的,官方也建議不使用分號,這是一個很好的實踐,可以節約你很多時間。
訪問性修飾符
Kotlin中這些修飾符是與C#中的使用是有些不同的。在 kotlin 中預設的修飾符是 public ,這節約了很多的時間和字元。而 kotlin 中具有以下幾種訪問性修飾符:
-
private 表示它只能被自己所的在檔案可見,我們就不能在定義這個類之外的檔案中使用它(但在同一個檔案中的其它類可以訪問,與C#中不同)。
-
protected 這個修飾符只能被用在類或者介面中的成員上,一個包成員不能被定義為 protected 。它可以被成員自己和繼承它的成員可見(比如,類和它的子類)。
-
internal 如果是一個定義為 internal 的包成員的話,對所在的整個 module 可見。如果它是一個其它領域的成員,它就需要依賴那個領域的可見性了。比如,如果我們寫了一個 private 類,那麼它的 internal 修飾的函式的可見性就會限制與它所在的這個類的可見性。
-
public 這是預設的修飾符,成員在任何地方被修飾為 public ,很明顯它只限制於它的領域。一個定義為 public 的成員被包含在一個 private 修飾的類中,這個成員在這個類以外也是不可見的。
什麼是 module ?
根據Jetbrains的定義,一個 module 應該是一個單獨的功能性的單位,它應該是可以被單獨編譯、執行、測試、debug的。根據我們專案不同的模組,可以在Android Studio中建立不同的 module 。在Eclipse中,這些 module 可以認為是在一個 workspace 中的不同的 project 。
建構函式
在 Kotlin 中類可以有一個主建構函式以及多個二級建構函式。主建構函式是類頭的一部分:跟在類名後面(可以有可選的型別引數)。
class Person constructor(name: String) {
}
如果主建構函式沒有註解或可見性說明,則 constructor 關鍵字可以省略:
class Person(name: String){
}
所有建構函式預設都是 public 的,它們的類是可見的,可以被其它地方使用,我們也可以把建構函式修改為 private :
class Person private constructor(name: String) {
}
同樣,也可以在建構函式中宣告屬性的訪問級別:
class A private constructor(private var a: Int) {
}
在建構函式中宣告的引數,如果沒有使用 var 修飾時,則表示為區域性變數,而使用 var 時則是類的屬性。
類也可以有二級建構函式,需要加字首 constructor:
class Person(var name: String){
constructor(name: String, age: Int) : this(name) {
}
}
需要注意的時候的是,如果主建構函式帶有引數,則二級建構函式必須顯式的呼叫主建構函式。
建立一個例項
我們可以像使用普通函式那樣使用建構函式建立類例項:
var persion1 = Persion("aaa");
var persion2 = Persion("aaa",10);
在 Kotlin 中並沒有
new
關鍵字。
繼承
預設任何類都是繼承自 Any
,與 C# 中的 Object 類似。而在 Kotlin 中,所有的類預設是不可繼承的,只有那些宣告為
open
或者 abstract
的類才可以被繼承:
open class Person(name: String, age: Int) {
}
class Student(name: String, age: Int, score: Int) : Persion(name, age) {
}
列舉
Kotlin也提供了列舉( enums )的實現:
enum class Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY }
列舉可以引用引數:
enum class Icon(val res: Int) {
UP(R.drawable.ic_up),
SEARCH(R.drawable.ic_search),
CAST(R.drawable.ic_cast)
}
val searchIconRes = Icon.SEARCH.res
列舉可以通過 String 匹配名字來獲取,我們也可以獲取包含所有列舉的 Array ,所以我們可以遍歷它。
val search: Icon = Icon.valueOf("SEARCH")
val iconList: Array<Icon> = Icon.values()
而且每一個列舉都有一些函式來獲取它的名字、宣告的位置:
val searchName: String = Icon.SEARCH.name()
val searchPosition: Int = Icon.SEARCH.ordinal()
列舉還根據它的順序實現了 Comparable 介面,所以可以很方便地把它們進行排序。
變數和屬性
在 Kotlin 中,一切都是物件。沒有像C#中那樣的原始基本型別。這個是非常有用的,因為我們可以使用一致的方式來處理所有的可用的型別。
基本型別
當然,像integer,float或者boolean等型別仍然存在,但是它們全部都會作為物件存在的。基本型別的名字和它們工作方式都是與C#非常相似的,但也有一些不同:
- 數字型別中不會自動轉型。比如,你不能給 Double 變數分配一個 Int 。必須要做一個明確的型別轉換
toDouble()
。 - 字元(Char)不能直接作為一個數字來處理。我們需要把他們轉換為一個數字
'a'.toInt()
。 - 位運算也有一點不同,在C#中我們經常使用
|
和&
,但在 Kotlin 中則使用or
和and
關鍵字。
變數
變數分為 可變(var
) 和不可變(val
) 兩種,val
類似於C#中的readonly。
一個不可變物件意味著它在例項化之後就不能再去改變它的狀態了,如果大部分的物件都是可變的,那就意味著任何可以訪問它這個物件的程式碼都可以去修改它,從而影響整個程式的其它地方。
不可變物件也可以說是執行緒安全的,因為它們無法去改變,也不需要去定義訪問控制,因為所有執行緒訪問到的物件都是同一個。
而變數的宣告和Typescript
非常相似:
var a = "aaaaaaaaaa"
var b: String = "bbbbbbbbbb"
val c: = 10
當我們在宣告中就為變數賦值時可以不用指定型別,因為 kotlin 會自動推斷出它的型別,這樣可以讓程式碼更加清晰和快速修改。
屬性
屬性做的事情就是為欄位加上 getter 和 setter,在C#中我們可以使用 public string a { get; set; }
的方式來宣告一個屬性,而在 kotlin 中,一個變數就是一個擁有預設getter和setter的屬性,我們也可以自定義getter和setter:
public class Person {
var name: String = "zs"
get() = field.toUpperCase()
set(value) {
field = value
}
}
函式
定義
函式使用 fun
關鍵字來定義:
fun test(x: Int) {
}
如果沒有指定返回值,則會返回一個 kotlin.Unit
,與C#中的 void
類似,但
kotlin.Unit
是一個真正的物件。而指定返回值也很簡單:
fun test(x: Int) : Boolean {
return x > 0
}
而上面的程式碼還有一種更簡單的方式,類似C#中的表示式方法體:
fun test(x: Int) : Boolean = x > 0
引數預設值
引數預設值在C#中比較常見,而在 Kotlin 中也支援了引數預設值:
fun test(x: Int, y: String = "default value") {
}
我們為第二個引數設定了一個預設值,這樣,在呼叫的時候便可以不傳第二個引數:
test(1)
test(1,"yyyyyyyy")
擴充套件函式
擴充套件函式數是指在一個類上增加一種新的行為,甚至我們沒有這個類程式碼的訪問許可權。這是一個在缺少有用函式的類上擴充套件的方法。
在C#中,我們可以使用一個靜態類中的靜態方法,把要擴充套件的型別使用this修飾,做為第一個引數傳進來,而 kotlin 中,更加簡潔:
class Persion {
fun test1() {
}
}
class MyClass {
// 擴充套件Persion類
fun Persion.test2() {
}
fun test() {
var persion = Persion()
persion.test1()
// 呼叫擴充套件方法
persion.test2()
}
}
就像定義一個普通函式一樣,只需要把函式名定義為要擴充套件的型別加上要擴充套件方法即可。
而擴充套件函式也可以是一個屬性,可以通過相似的方法來擴充套件屬性:
public var Persion.name: String
get() = getName()
set(v) = setName(v)
資料類
資料類是一種非常強大的類,它可以讓我們避免建立C#中用於儲存狀態但又操作非常簡單的POCO類模版程式碼。它們通常只提供了用於訪問它們屬性的簡單的getter和setter。定義一個新的資料類非常簡單:
data class UserDto(val name: String, val age: Int, val email: String, var date: Date)
額外的函式
通過資料類,我們可以方便地得到一些非常有用的函式:
- equals(): 它可以比較兩個物件的屬性來確保他們是相同的。
- hashCode(): 我們可以得到一個hash值,也是從屬性中計算出來的。
- copy(): 你可以拷貝一個物件,可以根據你的需要去修改裡面的屬性。
- 一系列可以對映物件到變數中的函式。
對映物件到變數中
對映物件的每一個屬性到一個變數中,這個過程是非常枯燥的。而在 kotlin 中則非常簡潔:
var user = UserDto('zs', 18, Date())
val (name, age, email) = user
上面這個多宣告會被編譯成下面的程式碼:
val name = user.component1()
val age = user.component2()
val email = user.component3()
在主建構函式中有多少個引數,就會依次生成對應的component1,component2,component3……這些函式返回的就是對應欄位的值。
這個特性背後的邏輯是非常強大的,它可以在很多情況下幫助我們簡化程式碼。舉個例子, Map 類含有一些擴充套件函式的實現,允許它在迭代時使用key和value:
for ((key, value) in map) {
Log.d("map", "key:$key, value:$value")
}