Kotlin基礎用法總結
Kotlin 團隊為 Android 開發提供了一套超越標準語言功能的工具:
- Kotlin Android 擴充套件是一個編譯器擴充套件, 可以讓你擺脫程式碼中的
findViewById()
呼叫,並將其替換為合成的編譯器生成的屬性。 - Anko 是一個提供圍繞
Android API
的Kotlin
友好的包裝器的庫 ,以及一個可以用Kotlin
程式碼替換佈局.xml
檔案的DSL
。
定義變數
val
變數值不可變(只讀)val a: Int = 1 // 立即賦值 val b = 2 // 自動推斷出 `Int` 型別 val c: Int // 如果沒有初始值型別不能省略
var
可變變數var x = 5 // 自動推斷出 `Int` 型別 x += 1
is
運算子檢測一個表示式是否某型別的一個例項。
fun getStringLength(obj: Any): Int? {
// obj 在 && 右邊自動轉換成 String 型別
if (obj is String && obj.length > 0) {
return obj.length
}
return null
}
使用
in
運算子來檢測某個數字是否在指定區間內
編碼規範
命名風格
如果拿不準的時候,預設使用Java
的編碼規範,比如:
- 使用駝峰法命名(並避免命名含有下劃線)
- 型別名以大寫字母開頭
- 方法和屬性以小寫字母開頭
- 使用 4 個空格縮排
- 公有函式應撰寫函式文件,這樣這些文件才會出現在
Kotlin Doc
中
Unit
如果函式返回 Unit
型別,該返回型別應該省略:
fun foo() { // 省略了 ": Unit"
}
使用可空值及 null 檢測
當某個變數的值可以為 null
的時候,必須在宣告處的型別後新增 ?
來標識該引用可為空。
如果 str
的內容不是數字返回 null
:
fun parseInt (str: String): Int? {
// ……
}
對於位運算,沒有特殊字元來表示,而只可用中綴方式呼叫命名函式
例如:
val x = (1 shl 2) and 0x000FF000
完整的位運算列表(只用於 Int
和 Long
):
shl(bits)
– 有符號左移 (Java 的<<
)shr(bits)
– 有符號右移 (Java 的>>
)ushr(bits)
– 無符號右移 (Java 的>>>
)
-and(bits)
– 位與or(bits)
– 位或xor(bits)
– 位異或inv()
– 位非
字元
字元用 Char
型別表示。它們不能直接當作數字
字串字面值
Kotlin 有兩種型別的字串字面值: 轉義字串 可以有轉義字元,以及 原生字串 可以包含換行和任意文字。轉義字串很像 Java
字串:
val s = "Hello, world!\n"
轉義採用傳統的反斜槓方式。
原生字串 使用三個引號("""
)分界符括起來,內部沒有轉義並且可以包含換行和任何其他字元:
val text = """
for (c in "foo")
print(c)
"""
字串模板
字串可以包含 模板表示式 ,即一些小段程式碼,會求值並把結果合併到字串中。 模板表示式以美元符($)開頭,由一個簡單的名字構成:
val i = 10
val s = "i = $i" // 求值結果為 "i = 10"
原生字串和轉義字串內部都支援模板。 如果你需要在原生字串中表示字面值 $
字元(它不支援反斜槓轉義),你可以用下列語法:
val price = """
${'$'}9.99
"""
控制流
if表示式
在Kotlin
中,if
是一個表示式,即它會返回一個值。 因此就不需要三元運算子(條件 ? 然後 : 否則),因為普通的 if
就能勝任這個角色。
// 傳統用法
var max = a
if (a < b) max = b
// With else
var max: Int
if (a > b) {
max = a
} else {
max = b
}
// 作為表示式
val max = if (a > b) a else b
if的分支可以是程式碼塊,最後的表示式作為該塊的值:
val a=2
val b=5
val max=if(a>b){
print("choose a")
a
}else{
print("Choose b")
b
}
如果你使用 if
作為表示式而不是語句(例如:返回它的值或者把它賦給變數),該表示式需要有 else
分支。
When 表示式
when
取代了類 C 語言的 switch
操作符。其最簡單的形式如下:
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意這個塊
print("x is neither 1 nor 2")
}
}
when
將它的引數和所有的分支條件順序比較,直到某個分支滿足條件。 when
既可以被當做表示式使用也可以被當做語句使用。如果它被當做表示式, 符合條件的分支的值就是整個表示式的值,如果當做語句使用, 則忽略個別分支的值。(像 if
一樣,每一個分支可以是一個程式碼塊,它的值是塊中最後的表示式的值。)
如果其他分支都不滿足條件將會求值 else
分支。 如果when
作為一個表示式使用,則必須有 else
分支, 除非編譯器能夠檢測出所有的可能情況都已經覆蓋了。
如果很多分支需要用相同的方式處理,則可以把多個分支條件放在一起,用逗號分隔:
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
我們可以用任意表達式(而不只是常量)作為分支條件
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}
我們也可以檢測一個值在(in
)或者不在(!in
)一個區間或者集合中:
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
另一種可能性是檢測一個值是(is
)或者不是(!is
)一個特定型別的值。注意: 由於智慧轉換,你可以訪問該型別的方法和屬性而無需任何額外的檢測。
fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
when
也可以用來取代 if-else if
鏈。 如果不提供引數,所有的分支條件都是簡單的布林表示式,而當一個分支的條件為真時則執行該分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
For 迴圈
for
迴圈可以對任何提供迭代器(iterator
)的物件進行遍歷,這相當於像 C#
這樣的語言中的 foreach
迴圈。語法如下:
for (item in collection) print(item)
迴圈體可以是一個程式碼塊。
for (item: Int in ints) {
// ……
}
While 迴圈
while
和 do..while
照常使用
迴圈中的Break和continue
在迴圈中 Kotlin
支援傳統的 break
和 continue
操作符。
Break 與 Continue 標籤
表示式,是指由常量、變數或是運算元與運算子所組合而成的語句。
在 Kotlin
中任何表示式都可以用標籤(label)來標記。 標籤的格式為識別符號後跟 @
符號,例如:[email protected]
、[email protected]
都是有效的標籤(參見語法)。 要為一個表示式加標籤,我們只要在其前加標籤即可。
[email protected] for (i in 1..100) {
// ……
}
現在,我們可以用標籤限制 break
或者continue
:
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}
標籤限制的 break
跳轉到剛好位於該標籤指定的迴圈後面的執行點。 continue
繼續標籤指定的迴圈的下一次迭代。
類和物件
建構函式
在 Kotlin 中的一個類可以有一個主建構函式和一個或多個次建構函式。主建構函式是類頭的一部分:它跟在類名(和可選的型別引數)後。
class Person constructor(firstName: String) {
}
如果主建構函式沒有任何註解或者可見性修飾符,可以省略這個 constructor
關鍵字。
class Person(firstName: String) {
}
主建構函式不能包含任何的程式碼。初始化的程式碼可以放到以 init 關鍵字作為字首的初始化塊(initializer blocks
)中:
class Customer(name: String) {
init {
logger.info("Customer initialized with value ${name}")
}
}
!注意,主構造的引數可以在初始化塊中使用。它們也可以在類體內宣告的屬性初始化器中使用:
class Customer(name: String) {
val customerKey = name.toUpperCase()
}
事實上,宣告屬性以及從主建構函式初始化屬性,Kotlin
有簡潔的語法:
class Person(val firstName: String, val lastName: String, var age: Int) {
// ……
}
與普通屬性一樣,主建構函式中宣告的屬性可以是可變的(var
)或只讀的(`val)。
如果建構函式有註解或可見性修飾符,這個constructor
關鍵字是必需的,並且這些修飾符在它前面:
class Customer public @Inject constructor(name: String) { …… }
次建構函式
類也可以宣告字首有 constructor
的次建構函式:
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
如果類有一個主建構函式,每個次建構函式需要委託給主建構函式, 可以直接委託或者通過別的次建構函式間接委託。委託到同一個類的另一個建構函式用 this
關鍵字即可:
class Person(val name: String) {
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
如果一個非抽象類沒有宣告任何(主或次)建構函式,它會有一個生成的不帶引數的主建構函式。建構函式的可見性是 public
。如果你不希望你的類有一個公有建構函式,你需要宣告一個帶有非預設可見性的空的主建構函式:
class DontCreateMe private constructor () {
}
注意:在
JVM
上,如果主建構函式的所有的引數都有預設值,編譯器會生成 一個額外的無參建構函式,它將使用預設值。這使得Kotlin
更易於使用像 Jackson 或者 JPA 這樣的通過無參建構函式建立類的例項的庫。
class Customer(val customerName: String = "")
繼承
在 Kotlin
中所有類都有一個共同的超類 Any
,這對於沒有超型別宣告的類是預設超類:
class Example // 從 Any 隱式繼承
Any
不是 java.lang.Object
;尤其是,它除了 equals()
、hashCode()
和toString()
外沒有任何成員。
要宣告一個顯式的超型別,我們把型別放到類頭的冒號之後:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
如果該類有一個主建構函式,其基型別可以(並且必須) 用(基型別的)主建構函式引數就地初始化。
如果類沒有主建構函式,那麼每個次建構函式必須使用super
關鍵字初始化其基型別,或委託給另一個建構函式做到這一點。 注意,在這種情況下,不同的次建構函式可以呼叫基型別的不同的建構函式:
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
類上的 open
標註與 Java
中 final
相反,它允許其他類從這個類繼承。預設情況下,在 Kotlin
中所有的類都是 final
, 對應於 Effective Java書中的第 17 條:要麼為繼承而設計,並提供文件說明,要麼就禁止繼承。
覆蓋屬性
屬性覆蓋與方法覆蓋類似;在超類中宣告然後在派生類中重新宣告的屬性必須以 override
開頭,並且它們必須具有相容的型別。每個宣告的屬性可以由具有初始化器的屬性或者具有 getter
方法的屬性覆蓋。
open class Foo {
open val x: Int get() { …… }
}
class Bar1 : Foo() {
override val x: Int = ……
}
呼叫超類實現
派生類中的程式碼可以使用 super
關鍵字呼叫其超類的函式與屬性訪問器的實現:
open class Foo {
open fun f() { println("Foo.f()") }
open val x: Int get() = 1
}
class Bar : Foo() {
override fun f() {
super.f()
println("Bar.f()")
}
override val x: Int get() = super.x + 1
}
在一個內部類中訪問外部類的超類,可以通過由外部類名限定的 super
關鍵字來實現:[email protected]
:
class Bar : Foo() {
override fun f() { /* …… */ }
override val x: Int get() = 0
inner class Baz {
fun g() {
super@Bar.f() // 呼叫 Foo 實現的 f()
println(super@Bar.x) // 使用 Foo 實現的 x 的 getter
}
}
}
覆蓋規則
在 Kotlin
中,實現繼承由下述規則規定:如果一個類從它的直接超類繼承相同成員的多個實現, 它必須覆蓋這個成員並提供其自己的實現(也許用繼承來的其中之一)。 為了表示採用從哪個超型別繼承的實現,我們使用由尖括號中超型別名限定的 super
,如 super<Base>
:
open class A {
open fun f() { print("A") }
fun a() { print("a") }
}
interface B {
fun f() { print("B") } // 介面成員預設就是“open”的
fun b() { print("b") }
}
class C() : A(), B {
// 編譯器要求覆蓋 f():
override fun f() {
super<A>.f() // 呼叫 A.f()
super<B>.f() // 呼叫 B.f()
}
}
同時繼承 A 和 B 沒問題,並且 a() 和 b() 也沒問題因為 C 只繼承了每個函式的一個實現。 但是 f() 由 C 繼承了兩個實現,所以我們必須在 C 中覆蓋 f() 並且提供我們自己的實現來消除歧義。
介面
Kotlin
的介面與 Java8
類似,既包含抽象方法的宣告,也包含實現。與抽象類不同的是,介面無法儲存狀態。它可以有屬性但必須宣告為抽象或提供訪問器實現。
使用關鍵字 interface
來定義介面
interface MyInterface {
fun bar()
fun foo() {
// 可選的方法體
}
}
委託
類委託:類的委託即一個類中定義的方法實際是呼叫另一個類的物件的方法來實現的。
以下例項中派生類 Derived
繼承了介面 Base
所有方法,並且委託一個傳入的 Base
類的物件來執行這些方法。
// 建立介面
interface Base {
fun print()
}
// 實現此介面的被委託的類
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
// 通過關鍵字 by 建立委託類
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 輸出 10
}
在 Derived
宣告中,by
子句表示,將 b
儲存在 Derived
的物件例項內部,而且編譯器將會生成繼承自 Base
介面的所有方法, 並將呼叫轉發給 b
。
注意,覆蓋會以你所期望的方式工作:編譯器會使用你的
override
實現取代委託物件中的實現。如果我們為Derived
新增override fun print() { print("abc") }
,該程式會輸出“abc”而不是“10”。
高階函式
高階函式是將函式用作引數或返回值的函式。
val doubled = ints.map { value -> value * 2 }
注意,如果 lambda 是該呼叫的唯一引數,則呼叫中的圓括號可以完全省略。
it:單個引數的隱式名稱
另一個有用的約定是,如果函式字面值只有一個引數, 那麼它的宣告可以省略(連同 ->
),其名稱是 it
。
ints.map { it * 2 }
Lambda 表示式與匿名函式
一個 lambda
表示式或匿名函式是一個“函式字面值”,即一個未宣告的函式, 但立即做為表示式傳遞。
例:
max(strings, { a, b -> a.length < b.length })
函式max
是一個高階函式,換句話說它接受一個函式作為第二個引數。 其第二個引數是一個表示式,它本身是一個函式,即函式字面值。寫成函式的話,它相當於:
fun compare(a: String, b: String): Boolean = a.length < b.length
lambda 表示式總是被大括號括著;
- 其引數(如果有的話)在 -> 之前宣告(引數型別可以省略);
- 函式體(如果存在的話)在 -> 後面。
如果 lambda
表示式的引數未使用,那麼可以用下劃線取代其名稱:
map.forEach { _, value -> println("$value!") }
集合
Kotlin 區分可變集合和不可變集合
不可變:
listOf()
setOf()
可變
mutableSetOf()
mutableListOf()
區間
區間表示式由具有操作符形式 ..
的 rangeTo
函式輔以in
和 !in
形成。
想要倒序迭代數字可以使用標準庫中定義的 downTo()
函式:
for (i in 4 downTo 1) print(i) // 輸出“4321”
型別的檢查與轉換“is”與“as”
is
與 !is
操作符
我們可以在執行時通過使用 is
操作符或其否定形式 !is
來檢查物件是否符合給定型別:
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // 與 !(obj is String) 相同
print("Not a String")
}
else {
print(obj.length)
}
空安全
在條件中檢查 null
顯式檢查 b 是否為 null
安全的呼叫
安全呼叫操作符,寫作 ?.
:
b?.length
如果 b 非空,就返回 b.length
,否則返回 null
,這個表示式的型別是 Int?
。
安全呼叫在鏈式呼叫中很有用。如:
bob?.department?.head?.name
如果任意一個屬性(環節)為空,這個鏈式呼叫就會返回 null
。
如果要只對非空值執行某個操作,安全呼叫操作符可以與 let
一起使用:
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // 輸出 A 並忽略 null
}
Elvis 操作符
val l = b?.length ?: -1
如果 ?:
左側表示式非空,elvis
操作符就返回其左側表示式,否則返回右側表示式。 請注意,當且僅當左側為空時,才會對右側表示式求值。
異常
異常類
Kotlin
中所有異常類都是 Throwable
類的子孫類。 每個異常都有訊息、堆疊回溯資訊和可選的原因
throw
表示式的型別是特殊型別Nothing
.該型別沒有值,而是用於標記永遠不能達到的程式碼位置。 在你自己的程式碼中,你可以使用 Nothing
來標記一個永遠不會返回的函式:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
泛型應用
Kotlin標準庫中的三個函式
let
let
可以被任何物件呼叫。它接收一個函式作為引數,作為引數的函式返回的結果作為整個函式的返回值。它在處理可null
物件的時候是非常有用的,下面是它的定義:inline fun <T, R> T.let(f: (T) -> R): R = f(this)
它使用了兩個泛型型別:
T
和R
。第一個是被呼叫者定義的,它的型別被函式
接收到。第二個是函式的返回值型別。對非空值執行某個操作,安全呼叫操作符可以與
let
一起使用,如://使用前 if (forecast != null) dataMapper.convertDayToDomain(forecast) else null //使用後 forecast?.let { dataMapper.convertDayToDomain(it) }
with
with
接收一個物件和一個函式,這個函式會作為這個物件的擴充套件函式執行。這表示我們根據推斷可以在函式內使用 this 。定義如下:inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
T
代表接收型別,R
代表結果。如你所見,函式通過f: T.() -> R
宣告被定義成了擴充套件函式。這就是為什麼我們可以呼叫receiver.f()
。例如:
fun convertFromDomain(forecast: ForecastList) = with(forecast) { val daily = dailyForecast map { convertDayFromDomain(id, it)} CityForecast(id, city, country, daily) }
apply
它看起來與
with
很相似,但是是有點不同之處。apply
可以避免建立builder的
方式來使用,因為物件呼叫的函式可以根據自己的需要來初始化自己,然後apply
函式會返回它同一個物件:inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
這裡我們只需要一個泛型型別,因為呼叫這個函式的物件也就是這個函式返回的物件。例如:
val textView = TextView(context).apply { text = "Hello" hint = "Hint" textColor = android.R.color.white }
獲取class 的例項:
class Test
val clazz = Test::class.java
val test = Test()
val clazz2 = test.javaClass
lateinit 和 by lazy
lateinit
用於var
,by lazy
用於val
lazy()
是一個函式,接受一個Lambda
表示式作為引數,返回一個Lazy
型別的例項,這個例項可以作為一個委託, 實現延遲載入屬: 第一次呼叫get()
時,儲存Lambda
表示式執行的結果, 以後對get()
的呼叫都只會簡單地返回以前記住的結果,在lazy
中並沒有setValue(…)
方法.lateinit
用於生命週期流程中進行獲取或者初始化的變數。