如何正確地使用Kotlin的屬性代理

簡述:
今天繼續(xù)Kotlin原創(chuàng)系列的第十一講,一起來揭開Kotlin屬性代理的漂亮外衣。屬性代理可以說是Kotlin獨有的強大的功能之一,特別是對于框架開發(fā)的小伙伴來說非常有用屈梁,因為會經(jīng)常涉及到更改存儲和修改屬性的方式操作,例如Kotlin中的SQL框架Exposed源碼就大量使用了屬性代理继控。相信你已經(jīng)在代碼也使用了諸如Delegates.observable()旬迹、Delegates.notNull()、Delegates.vetoable()或者自定義的屬性代理液斜。也許你還停留用的階段或者對它還有點陌生累贤,不用擔(dān)心這篇文章將會基本上解決你所有的疑惑。廢話不多說少漆,直接來看一波章節(jié)導(dǎo)圖:

image

一臼膏、屬性代理的基本定義

  • 1、基本定義

屬性代理是借助于代理設(shè)計模式示损,把這個模式應(yīng)用于一個屬性時渗磅,它可以將訪問器的邏輯代理給一個輔助對象。

可以簡單理解為屬性的setter检访、getter訪問器內(nèi)部實現(xiàn)是交給一個代理對象來實現(xiàn)始鱼,相當(dāng)于使用一個代理對象來替換了原來簡單屬性字段讀寫過程,而暴露外部屬性操作還是不變的脆贵,照樣是屬性賦值和讀取医清,只是setter、getter內(nèi)部具體實現(xiàn)變了卖氨。

  • 2会烙、基本語法格式
class Student{
    var name: String by Delegate()
}

class Delegate{
    operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
        ...
    }
    operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
        ...
    }
}

屬性name將它訪問器的邏輯委托給了Delegate對象,通過by關(guān)鍵字對表達(dá)式Delegate()求值獲取這個對象筒捺。任何符合屬性代理規(guī)則都可以使用by關(guān)鍵字柏腻。屬性代理類必須要遵循getValue(),setValue()方法約定,getValue系吭、setValue方法可以是普通方法也可以是擴展方法五嫂,并且是方法是支持運算符重載贴妻。如果是val修飾的屬性只需要具備getValue()方法即可帐姻。

屬性代理基本流程就是代理類中的getValue()方法包含屬性getter訪問器的邏輯實現(xiàn),setValue()方法包含了屬性setter訪問器的邏輯實現(xiàn)鞭达。當(dāng)屬性name執(zhí)行賦值操作時蟆盹,會觸發(fā)屬性setter訪問器孩灯,然后在setter訪問器內(nèi)部調(diào)用delegate對象的setValue()方法;執(zhí)行讀取屬性name操作時逾滥,會在getter訪問器中調(diào)用delegate對象的getValue方法.

  • 3峰档、by關(guān)鍵字

by關(guān)鍵字實際上就是一個屬性代理運算符重載的符號败匹,任何一個具備屬性代理規(guī)則的類,都可以使用by關(guān)鍵字對屬性進(jìn)行代理讥巡。

二掀亩、常見屬性代理基本使用

屬性代理是Kotlin獨有的特性,我們自己去自定義屬性代理欢顷,當(dāng)然Kotlin還提供了幾種常見的屬性代理實現(xiàn)槽棍。例如:Delegates.notNull(), Delegates.observable(), Delegates.vetoable()

  • 1、Delegates.notNull()的基本使用

Delegate.notNull()代理主要用于可以不在構(gòu)造器初始化時候初始化而是可以延遲到之后再初始化這個var修飾的屬性,它和lateinit功能類似抬驴,但是也有一些不同炼七,不過它們都需要注意的一點是屬性的生命周期,開發(fā)者要做到可控布持,也就是一定要確保屬性初始化是在屬性使用之前豌拙,否則會拋出一個IllegalStateException.

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Teacher {
    var name: String by Delegates.notNull()
}
fun main(args: Array<String>) {
    val teacher = Teacher().apply { name = "Mikyou" }
    println(teacher.name)
}

可能有的人并沒有看到notNull()有什么大的用處,先說下大背景吧就會明白它的用處在哪了题暖?

大背景: 在Kotlin開發(fā)中與Java不同的是在定義和聲明屬性時必須要做好初始化工作按傅,否則編譯器會提示報錯的,不像Java只要定義就OK了胧卤,管你是否初始化呢唯绍。我解釋下這也是Kotlin優(yōu)于Java地方之一,沒錯就是空類型安全枝誊,就是Kotlin在寫代碼時就讓你明確一個屬性是否初始化况芒,不至于把這樣的不明確定義拋到后面運行時。如果在Java你忘記了初始化叶撒,那么恭喜你在運行時你就會拿到空指針異常牛柒。

問題來了: 大背景說完了那么問題也就來了,相比Java痊乾,Kotlin屬性定義時多出了額外的屬性初始化的工作。但是可能某個屬性的值在開始定義的時候你并不知道椭更,而是需要執(zhí)行到后面的邏輯才能拿到哪审。這時候解決方式大概有這么幾種:

方式A: 開始初始化的時給屬性賦值個默認(rèn)值

方式B: 使用Delegates.notNull()屬性代理

方式C: 使用lateinit修飾屬性

以上三種方式有局限性,方式A就是很暴力直接賦默認(rèn)值虑瀑,對于基本類型還可以湿滓,但是對于引用類型的屬性,賦值一個默認(rèn)引用類型對象就感覺不太合適了舌狗。方式B適用于基本數(shù)據(jù)類型和引用類型叽奥,但是存在屬性初始化必須在屬性使用之前為前提條件。方式C僅僅適用于引用類型痛侍,但是也存在屬性初始化必須在屬性使用之前為前提條件朝氓。

優(yōu)缺點分析:

屬性使用方式 優(yōu)點 缺點
方式A(初始化賦默認(rèn)值) 使用簡單,不存在屬性初始化必須在屬性使用之前的問題 僅僅適用于基本數(shù)據(jù)類型
方式B(Delegates.notNull()屬性代理) 適用于基本數(shù)據(jù)類型和引用類型 1、存在屬性初始化必須在屬性使用之前的問題赵哲;
2待德、不支持外部注入工具將它直接注入到Java字段中
方式C(lateinit修飾屬性) 僅適用于引用類型 1、存在屬性初始化必須在屬性使用之前的問題枫夺;
2将宪、不支持基本數(shù)據(jù)類型

使用建議: 如果能對屬性生命周期做很好把控的話,且不存在注入到外部字段需求橡庞,建議使用方式B较坛;此外還有一個不錯建議就是方式A+方式C組合,或者方式A+方式B組合扒最。具體看實際場景需求丑勤。

  • 2、Delegates.observable()的基本使用

Delegates.observable()主要用于監(jiān)控屬性值發(fā)生變更扼倘,類似于一個觀察者确封。當(dāng)屬性值被修改后會往外部拋出一個變更的回調(diào)。它需要傳入兩個參數(shù)再菊,一個是initValue初始化的值爪喘,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)。

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.observable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")
    })
}

fun main(args: Array<String>) {
    val person = Person().apply { address = "ShangHai" }
    person.address = "BeiJing"
    person.address = "ShenZhen"
    person.address = "GuangZhou"
}

運行結(jié)果:

property: address  oldValue: NanJing  newValue: ShangHai
property: address  oldValue: ShangHai  newValue: BeiJing
property: address  oldValue: BeiJing  newValue: ShenZhen
property: address  oldValue: ShenZhen  newValue: GuangZhou

Process finished with exit code 0

  • 3纠拔、Delegates.vetoable()的基本使用

Delegates.vetoable()代理主要用于監(jiān)控屬性值發(fā)生變更秉剑,類似于一個觀察者,當(dāng)屬性值被修改后會往外部拋出一個變更的回調(diào)稠诲。它需要傳入兩個參數(shù)侦鹏,一個是initValue初始化的值,另一個就是回調(diào)lamba, 回調(diào)出property, oldValue, newValue三個參數(shù)臀叙。與observable不同的是這個回調(diào)會返回一個Boolean值略水,來決定此次屬性值是否執(zhí)行修改。

package com.mikyou.kotlin.delegate

import kotlin.properties.Delegates

class Person{
    var address: String by Delegates.vetoable(initialValue = "NanJing", onChange = {property, oldValue, newValue ->
        println("property: ${property.name}  oldValue: $oldValue  newValue: $newValue")
        return@vetoable newValue == "BeiJing"
    })
}

fun main(args: Array<String>) {
    val person = Person().apply { address = "NanJing" }
    person.address = "BeiJing"
    person.address = "ShangHai"
    person.address = "GuangZhou"
    println("address is ${person.address}")
}

三劝萤、常見屬性代理的源碼分析

以上我們介紹了常見的屬性代理基本使用渊涝,如果僅僅停留在使用的階段,確實有點low了, 那么讓我們一起先來揭開它們的第一層外衣床嫌。先來看波Kotlin標(biāo)準(zhǔn)庫源碼中常見的屬性代理包結(jié)構(gòu)跨释。

  • 1、源碼包結(jié)構(gòu)


    image
  • 2厌处、關(guān)系類圖

image

Delegates: 是一個代理單例對象鳖谈,里面有notNull、observable阔涉、vetoable靜態(tài)方法缆娃,每個方法返回不同的類型代理對象

NotNullVar: notNull方法返回代理對象的類

ObserableProperty: observable捷绒、vetoable方法返回代理對象的類

ReadOnlyProperty: 只讀屬性代理對象的通用接口

ReadWriteProperty: 讀寫屬性代理對象的通用接口

  • 3、Delegates.notNull()源碼分析

notNull()首先是一個方法龄恋,返回的是一個NotNullVar屬性代理實例疙驾;那么它處理核心邏輯就是NotNullVar內(nèi)部的setValue和getValue方法,一起來瞅一眼郭毕。

  public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.")
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }

通過源碼可以看到一旦getValue中的value是為null,那么就會拋出一個IllegalStateException它碎,也就是在使用該屬性之前沒有做初始化。實際上可以理解在訪問器getter加了一層判空的代理實現(xiàn)显押。

  • 4扳肛、Delegates.observable()源碼分析

observable()是一個方法,返回的是一個ObservableProperty屬性代理實例乘碑;那它是怎么做到在屬性值發(fā)生變化通知到外部的呢挖息,其實很簡單,首先在內(nèi)部保留一個oldValue用于存儲上一次的值兽肤,然后就在ObservableProperty類setValue方法執(zhí)行真正賦值之后再向外部拋出了一個afterChange的回調(diào)套腹,并且把oldValue,newValue,property回調(diào)到外部,最終利用onChange方法回調(diào)到最外層资铡。

 public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        val oldValue = this.value
        if (!beforeChange(property, oldValue, value)) {
            return
        }
        this.value = value
        afterChange(property, oldValue, value)
    }
 public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
 }    
  • 5电禀、Delegates.vetoable()源碼分析
    vetoable()是一個方法,返回的是一個ObservableProperty屬性代理實例笤休;通過上面源碼就可以發(fā)現(xiàn)尖飞,在setValue方法中執(zhí)行真正賦值之前,會有一個判斷邏輯店雅,根據(jù)beforeChange回調(diào)方法返回的Boolean決定是否繼續(xù)執(zhí)行下面的真正賦值操作政基。如果beforChange()返回false就終止此次賦值,那么observable也不能得到回調(diào)闹啦,如果返回true則會繼續(xù)此次賦值操作沮明,并執(zhí)行observable的回調(diào)。

四窍奋、屬性代理背后的原理和源碼反編譯分析

如果說第三節(jié)是揭開屬性代理第一層外衣珊擂,那么第四節(jié)將是揭開最后一層外衣了,你會看到屬性代理真正背后的原理费变,看完你會發(fā)現(xiàn)其實挺簡單的。不多說先上一個簡單例子

class Teacher {
    var name: String by Delegates.notNull()
    var age: Int by Delegates.notNull()
}

實際上圣贸,以上那行代碼是經(jīng)歷了兩個步驟:

class Teacher {
    private val delegateString: ReadWriteProperty<Teacher, String> = Delegates.notNull()
    private val delegateInt: ReadWriteProperty<Teacher, Int> = Delegates.notNull()
    var name: String by delegateString
    var age: Int by delegateInt
}

Kotlin反編譯后Java源碼

public final class Teacher {
   // $FF: synthetic field
   //關(guān)鍵點一
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "name", "getName()Ljava/lang/String;")), (KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Teacher.class), "age", "getAge()I"))};
   //關(guān)鍵點二
   @NotNull
   private final ReadWriteProperty name$delegate;
   @NotNull
   private final ReadWriteProperty age$delegate;

   //關(guān)鍵點三
   @NotNull
   public final String getName() {
      return (String)this.name$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name$delegate.setValue(this, $$delegatedProperties[0], var1);
   }

   public final int getAge() {
      return ((Number)this.age$delegate.getValue(this, $$delegatedProperties[1])).intValue();
   }

   public final void setAge(int var1) {
      this.age$delegate.setValue(this, $$delegatedProperties[1], var1);
   }

   public Teacher() {
      this.name$delegate = Delegates.INSTANCE.notNull();
      this.age$delegate = Delegates.INSTANCE.notNull();
   }
}

分析過程:

  • 1挚歧、首先, Teacher類的name和age屬性會自動生成對應(yīng)的setter,getter方法,并且會自動生成對應(yīng)的name$delegate吁峻、age$delegate委托對象滑负,如代碼中標(biāo)識的關(guān)鍵點二在张。
  • 2、然后矮慕,$$delegatedProperties的KProperty數(shù)組中會保存通過Kotlin反射出當(dāng)前Teacher類中的中name帮匾,age屬性,反射出來每個屬性單獨對應(yīng)保存在KProperty數(shù)組中痴鳄。
  • 2瘟斜、然后,在對應(yīng)屬性setter,getter方法中是把具體的實現(xiàn)委托給對應(yīng)的name$delegate痪寻、age$delegate對象的setValue螺句、getValue方法來實現(xiàn)的,如代碼中標(biāo)識的關(guān)鍵點三橡类。
  • 3蛇尚、最后在delegate對象中的setValue和getValue方法中的傳入對應(yīng)反射出來的屬性以及相應(yīng)的值。

五顾画、自己動手實現(xiàn)屬性代理

有以上的介紹取劫,自己寫個自定義的屬性代理應(yīng)該很簡單了吧。實現(xiàn)一個簡單的屬性代理最基本架子就是setValue,getValue方法且無需實現(xiàn)任何的接口研侣。

在Android中SharedPreferences實際上就是個很好場景谱邪,因為它涉及到了屬性存儲和讀取。自定義屬性代理實現(xiàn)Android中SharedPreferences可以直接實現(xiàn)自帶的ReadWriteProperty接口义辕,當(dāng)然也可以自己去寫一個類然后去定義相應(yīng)的setValue方法和getValue方法虾标。

class PreferenceDelegate<T>(private val context: Context, private val name: String, private val default: T, private val prefName: String = "default")
    : ReadWriteProperty<Any?, T> {
    private val prefs: SharedPreferences by lazy {
        context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
    }

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("setValue from delegate")
        return getPreference(key = name)
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        println("setValue from delegate")
        putPreference(key = name, value = value)
    }

    private fun getPreference(key: String): T {
        return when (default) {
            is String -> prefs.getString(key, default)
            is Long -> prefs.getLong(key, default)
            is Boolean -> prefs.getBoolean(key, default)
            is Float -> prefs.getFloat(key, default)
            is Int -> prefs.getInt(key, default)
            else -> throw IllegalArgumentException("Unknown Type.")
        } as T
    }

    private fun putPreference(key: String, value: T) = with(prefs.edit()) {
        when (value) {
            is String -> putString(key, value)
            is Long -> putLong(key, value)
            is Boolean -> putBoolean(key, value)
            is Float -> putFloat(key, value)
            is Int -> putInt(key, value)
            else -> throw IllegalArgumentException("Unknown Type.")
        }
    }.apply()

}

六、結(jié)語

到這里屬性代理的內(nèi)容就結(jié)束了灌砖,有沒有覺得Kotlin語言糖設(shè)計還是很巧妙的璧函。雖然很多人抵觸語法糖,但不可否認(rèn)的是它給我們開發(fā)在效率上帶來了很大的提升基显。有時候我們更多地是需要透過語法糖外衣蘸吓,看到其背后的原理,弄清整個語法糖設(shè)計思路和技巧撩幽,以一個全局眼光去看待它库继,就會覺得它也就那么回事。最后窜醉,感謝一波bennyHuo大佬宪萄,我是先看到他sharedPreferences的屬性擴展例子,感覺很不錯榨惰,然后決定去深入探究一下屬性擴展拜英,這下應(yīng)該對Kotlin屬性擴展有了比較深的認(rèn)識了。

<div align="center"><img src="https://user-gold-cdn.xitu.io/2018/5/14/1635c3fb0ba21ec1?w=430&h=430&f=jpeg&s=39536" width="200" height="200"></div>

歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟琅催,這里有最新Kotlin技術(shù)文章居凶,每周會不定期翻譯一篇Kotlin國外技術(shù)文章虫给。如果你也喜歡Kotlin,歡迎加入我們~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侠碧,一起剝皮案震驚了整個濱河市抹估,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弄兜,老刑警劉巖药蜻,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異挨队,居然都是意外死亡谷暮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門盛垦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來湿弦,“玉大人,你說我怎么就攤上這事腾夯〖瞻#” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵蝶俱,是天一觀的道長班利。 經(jīng)常有香客問我,道長榨呆,這世上最難降的妖魔是什么罗标? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮积蜻,結(jié)果婚禮上闯割,老公的妹妹穿的比我還像新娘。我一直安慰自己竿拆,他們只是感情好宙拉,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丙笋,像睡著了一般谢澈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上御板,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天锥忿,我揣著相機與錄音,去河邊找鬼怠肋。 笑死缎谷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播列林,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼酪惭!你這毒婦竟也來了希痴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤春感,失蹤者是張志新(化名)和其女友劉穎砌创,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鲫懒,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡嫩实,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了窥岩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甲献。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颂翼,靈堂內(nèi)的尸體忽然破棺而出晃洒,到底是詐尸還是另有隱情,我是刑警寧澤朦乏,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布球及,位于F島的核電站,受9級特大地震影響呻疹,放射性物質(zhì)發(fā)生泄漏吃引。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一刽锤、第九天 我趴在偏房一處隱蔽的房頂上張望镊尺。 院中可真熱鬧,春花似錦姑蓝、人聲如沸鹅心。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旭愧。三九已至,卻和暖如春宙暇,著一層夾襖步出監(jiān)牢的瞬間输枯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工占贫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桃熄,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓型奥,卻偏偏與公主長得像瞳收,于是被迫代替她去往敵國和親碉京。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 本文是在學(xué)習(xí)和使用kotlin時的一些總結(jié)與體會螟深,一些代碼示例來自于網(wǎng)絡(luò)或Kotlin官方文檔谐宙,持續(xù)更新... 對...
    竹塵居士閱讀 3,286評論 0 8
  • 前言 人生苦多,快來 Kotlin 界弧,快速學(xué)習(xí)Kotlin凡蜻! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,209評論 9 118
  • 1用戶屬性(Custom delegate ) 語法結(jié)構(gòu)是: val/var : <Type> by <expr...
    泛光燈閱讀 1,452評論 0 1
  • 面向?qū)ο缶幊蹋∣OP) 在前面的章節(jié)中,我們學(xué)習(xí)了Kotlin的語言基礎(chǔ)知識条获、類型系統(tǒng)忠荞、集合類以及泛型相關(guān)的知識。...
    Tenderness4閱讀 4,441評論 1 6
  • 從前月匣,有一個小姑娘叫茉莉钻洒,她特別喜歡裙子〕可是素标,她家里很窮,買不起那么貴重的的裙子萍悴。所以她就拿來報紙头遭,在報紙...
    TIANER閱讀 258評論 1 2