翻譯說明:
原標題: All About Type Aliases in Kotlin
原文地址: https://typealias.com/guides/all-about-type-aliases/
原文作者: Dave Leeds
你是否經(jīng)歷過像下面的對話?
希望你在現(xiàn)實生活中沒有像這樣的對話,但是這樣情景可能會出現(xiàn)在你的代碼中。
例如,看下這個代碼:
interface RestaurantPatron {
fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}
當你看到很多類型的代碼被擠在一起的時候,你很容易迷失在代碼的細節(jié)中。事實上,僅僅看這些函數(shù)的聲明就感覺挺嚇人的。
幸運的是,Kotlin為我們提供了一種簡單的方法來將復雜類型簡化成更具可讀性的別名。
在這篇文章中:
- 我們將學習關(guān)于類型別名的一切內(nèi)容以及他們的工作原理。
- 然后,我們將看看你可能會使用到關(guān)于它們的一些方法。
- 然后,我們將會看下有關(guān)它們需要注意的點。
- 最后,我們來看看一個類似的概念, Import As, 并看看它們之間的比較。
介紹Type Aliases(類型別名)
一旦我們?yōu)槟硞€概念創(chuàng)造了一個術(shù)語,其實我們就沒必要每次談論到它的時候都要去描述一下這個概念,我們只需要使用這個術(shù)語就可以了! 所以讓我們代碼也去做類似事情吧。讓我們來看看這個復雜的類型并給它一個命名。
針對上面的代碼,我們將通過創(chuàng)建一個類型的別名來優(yōu)化它:
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>
現(xiàn)在,在每個描述restaurant概念的地方,而不是每次都去寫出 Organization<(Currency, Coupon?) -> Sustenance> 聲明,而是可以像下面這樣表達出 Restaurant術(shù)語:
interface RestaurantPatron {
fun makeReservation(restaurant: Restaurant)
fun visit(restaurant: Restaurant)
fun complainAbout(restaurant: Restaurant)
}
哇! 這樣看上去容易多了,而且當你看到它時,你在代碼中的疑惑也會少很多。
我們還避免了很多在整個RestaurantPatron接口中大量重復的類型,而不是每次都需要去寫Organization<(Currency, Coupon?) -> Sustenance>,我們僅僅只有一種類型Restaurant即可。
這樣也就意味著如果我們需要修改這種復雜類型也是很方便的。例如,如果我們需要將原來的 Organization<(Currency, Coupon?) -> Sustenance> 化簡成 Organization<(Currency, Coupon?) -> Meal>,我們僅僅只需要改變一處即可,而不是像原來那樣定義需要修改三個地方。
typealias Restaurant = Organization<(Currency, Coupon?) -> Meal>
簡單!
你或許會思考...
可讀性
你可能會對自己說,“我不明白這是如何有助于代碼的可讀性的...,由于上述的示例中參數(shù)的名稱已經(jīng)明確表明了restaurant的概念,為什么我還需要一個Restaurant類型呢?難道我們不能使用具體的參數(shù)名稱和抽象類型嗎?”
是的,參數(shù)的名稱確實它應該可以更具體地表示類型,但是我們上面的RestaurantPatron接口的別名版本仍然更具有可讀性,并且也不容易受到侵入
然而,有些情況下是沒有命名的,或者說他們沒有一個確切類型名稱,例如Lambda表達式的類型:
interface RestaurantService {
var locator: (String, ZipCode) -> List<Organization<(Currency, Coupon?) -> Sustenance>>
}
在上面那段代碼中,仍然在表示locator這個lambda表示式正在返回一個restaurant的列表,但是獲取這些表示含義的信息唯一線索就是接口的名稱。然而僅僅從locator函數(shù)類型中沒有那么明確得到,因為冗長的類型定義已經(jīng)失去了含義本質(zhì)。
而下面的這個版本,只需要看一眼就能很容易理解:
interface RestaurantService {
var locator: (String, ZipCode) -> List<Restaurant>
}
間接性
你或許會想,“等等,我需要更多地考慮類型別名嗎?之前沒有類型別名的時候,把確切真實的類型直接暴露在外部聲明處,現(xiàn)在卻要將他們隱藏在別名的后面”
當然,我們已經(jīng)引入了一層間接尋址-有些被別名掩蓋了具體類型細節(jié)。但是作為程序員,我們一直在做著隱藏命名背后的細節(jié)事情。例如:
- 我們不會把具體常量數(shù)值9.8寫到我們代碼中,而是我們創(chuàng)建一個靜態(tài)常量ACCELERATION_DUE_TO_GRAVITY,在代碼中直接使用靜態(tài)常量。
- 我們不會把一個表達式 6.28 * radius 實現(xiàn)寫在代碼任何地方,而是把這個表達式放入到一個 circumference() 函數(shù)中去,然后在代碼中去調(diào)用circumference() 函數(shù)
記住-如果我們需要去查看別名背后隱藏細節(jié)是什么,僅僅只需要在IDE中使用Command+Click即可。
繼承性
或者你也許在想,"我為什么需要一個類型別名呢?我可以使用繼承方式,來繼承這個復雜類型" 如下所示:
class Restaurant : Organization<(Currency, Coupon?) -> Sustenance>()
沒錯,在這種情況下,你確實可以通過其詳細的類型參數(shù)對 Organization 類進行子類化。事實上,你可能在Java中看到了這一點。
但是類型別名適用性很廣,它也適用于你不能或通常不會去繼承的類型。例如:
- 非 open 一些的類 例如:String,或者Java中的Optional<T>
- Kotlin中的單例對象實例( object )。
- 函數(shù)類型,例如: (Currency, Coupon?) -> Sustenance
- 甚至函數(shù)接收者類型,例如: Currency.(Coupon?) -> Sustenance
在文章后面的部分,我們將更多地比較類型別名方法和繼承方法。
理解類型別名(Type Aliases)
我們已經(jīng)了解過如何簡單地去聲明一個類型別名。現(xiàn)在讓我們放大一些,這樣我們就可以了解創(chuàng)建時發(fā)生的原理!
當處理類型別名的時候,我們有兩個類型需要去思考:
- 別名(alias)
- 底層類型(underlying type)
據(jù)說它本身是一個別名(如UserId),或者包含別名(如List<UserId>)的縮寫類型
當Kotlin編譯器編譯您的代碼時,所有使用到的相應縮寫類型將會擴展成原來的全類型。讓我們看一個更為完整例子。
class UniqueIdentifier(val value: Int)
typealias UserId = UniqueIdentifier
val firstUserId: UserId = UserId(0)
當編譯器處理上述代碼時,所有對 UserId 的引用都會擴展到 UniqueIdentifier
換句話說,在擴展期間,編譯器大概做了類似于在代碼中搜索別名(UserId)所有用到的地方,然后將代碼中用到的地方逐字地將其別名替換成全稱類型名(UniqueIdentifier)的工作。
你可能已經(jīng)注意到我使用了“大部分”和“大概”等字樣。 這是因為,雖然這是我們理解類型別名的一個好的起點,但有一些情況下Kotlin不完全是通過逐字替換原理來實現(xiàn)。 我們將馬上闡述這些內(nèi)容! 現(xiàn)在,我們只需記住這個逐字替換原理通常是有效的。
順便說一下,如果你使用IntelliJ IDEA,你會很高興發(fā)現(xiàn)IDE對類型別名有一些很好的支持。例如,您可以在代碼中看到別名和底層類型:
并且可以快速查看聲明文檔:
類型別名和類型安全
現(xiàn)在我們已經(jīng)了解了類型別名的基礎(chǔ)知識,下面我們來探討另一個例子。這一個使用多個別名例子:
typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier
interface Store {
fun purchase(user: UserId, product: ProductId): Receipt
}
一旦我們拿到了我們 Store 的一個實例,我們可以進行購買:
val receipt = store.purchase(productId, userId)
此時,你是否注意到什么了?
我們意外地把我們的調(diào)用參數(shù)順序弄反了! userId應該是第一個參數(shù),而productId應該是第二個參數(shù)!
為什么編譯器沒有提示我們這個問題呢?
如果我們按照上面的逐字替換原理,我們可以模擬編譯器擴展出的代碼:
哇!兩個參數(shù)類型都擴展為相同的底層類型!這意味著可以將它們混在一起使用,并且編譯器無法分辨出對應參數(shù)。
一個重大的發(fā)現(xiàn): 類型別名不會創(chuàng)建新的類型。他們只是給現(xiàn)有類型取了另一個名稱而已
當然,這也就是為什么我們可以給一個沒有子類繼承的非 open的類添加類型別名。
雖然你可能認為這總是一件壞事,但實際上有些情況下它是有幫助的!
我們來比較兩種不同的方式對類型命名:
- 1、使用 類型別名
- 2、使用 繼承 去創(chuàng)建一個子類型(如上面的繼承部分所述)。
兩種情況下的底層類型都是String提供者,它只是一個不帶參數(shù)并返回String的函數(shù)。
typealias AliasedSupplier = () -> String
interface InheritedSupplier : () -> String
現(xiàn)在,我們?nèi)?chuàng)建一對函數(shù)去接收這些提供者:
fun writeAliased(supplier: AliasedSupplier) =
println(supplier.invoke())
fun writeInherited(supplier: InheritedSupplier) =
println(supplier.invoke())
最后,我們準備去調(diào)用這些函數(shù):
writeAliased { "Hello" }
writeInherited { "Hello" } // Zounds! A compiler error!(編譯器錯誤)
使用lambda表達式的類型別名方式可以正常運行,而繼承方式甚至不能編譯!相反,它給了我們這個錯誤信息:
Required: InheritedSupplier / Found: () -> String
事實上,我發(fā)現(xiàn)實際調(diào)用writeInherited()的唯一方法,像下面這樣拼湊一個冗長的內(nèi)容。
writeInherited(object : InheritedSupplier {
override fun invoke(): String = "Hello"
})
所以在這種情況下,類型別名方式相比基于繼承的方式上更具有優(yōu)勢。
當然,在某些情況下,類型安全將對您更為重要,在這種情況下,類型別名可能不適合您的需求。
類型別名的例子
現(xiàn)在我們已經(jīng)很好地掌握了類型別名,讓我們來看看一些例子!這里將為你提供一些關(guān)于類型別名的建議:
// Classes and Interfaces (類和接口)
typealias RegularExpression = String
typealias IntentData = Parcelable
// Nullable types (可空類型)
typealias MaybeString = String?
// Generics with Type Parameters (類型參數(shù)泛型)
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>
// Generics with Concrete Type Arguments (混合類型參數(shù)泛型)
typealias Users = ArrayList<User>
// Type Projections (類型投影)
typealias Strings = Array<out String>
typealias OutArray<T> = Array<out T>
typealias AnyIterable = Iterable<*>
// Objects (including Companion Objects) (對象,包括伴生對象)
typealias RegexUtil = Regex.Companion
// Function Types (函數(shù)類型)
typealias ClickHandler = (View) -> Unit
// Lambda with Receiver (帶接收者的Lambda)
typealias IntentInitializer = Intent.() -> Unit
// Nested Classes and Interfaces (嵌套類和接口)
typealias NotificationBuilder = NotificationCompat.Builder
typealias OnPermissionResult = ActivityCompat.OnRequestPermissionsResultCallback
// Enums (枚舉類)
typealias Direction = kotlin.io.FileWalkDirection
// (but you cannot alias a single enum *entry*)
// Annotation (注解)
typealias Multifile = JvmMultifileClass
你可以基于類型別名可以做很酷的操作
正如我們所看到的一樣,一旦創(chuàng)建了別名就可以在各種場景中使用它來代替底層類型,比如:
- 在聲明變量類型、參數(shù)類型和返回值類型的時候
- 在作為類型參數(shù)約束和類型參數(shù)的時候
- 在使用比較類型is或者強轉(zhuǎn)類型的as的時候
- 在獲得函數(shù)引用的時候
除了以上那些以外,它還有一些其他的用法細節(jié)。讓我們一起來看看:
構(gòu)造器(Constructors)
如果底層類型有一個構(gòu)造器,那么它的類型別名也是如此。你甚至可以在一個可空類型的別名上調(diào)用構(gòu)造函數(shù)!
class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?
// Constructing with the alias: 使用別名來構(gòu)造對象
val member = MaybeTeamMember("Miguel")
// The above code does *not* expand verbatim to this (which wouldn't compile):(以上代碼不會是逐字擴展成如下無法編譯的代碼)
val member = TeamMember?("Miguel")
// Instead, it expands to this:(而是擴展如下代碼)
val member = TeamMember("Miguel")
所以你可以看到編譯時的擴展并不總是逐字擴展的,在這個例子中就是很有效的說明。
如果底層類型本身就沒有構(gòu)造器(例如接口或者類型投影),自然地你也不可能通過別名來調(diào)用構(gòu)造器。
伴生對象
你可以通過含有伴生對象類的別名來調(diào)用該類的伴生對象中的屬性和方法。即使底層類型具有指定的具體類型參數(shù),也是如此。一起來看下:
class Container<T>(var item: T) {
companion object {
const val classVersion = 5
}
}
// Note the concrete type argument of String(注意此處的String是具體的參數(shù)類型)
typealias BoxedString = Container<String>
// Getting a property of a companion object via an alias:(通過別名獲取伴侶對象的屬性:)
val version = BoxedString.classVersion
// The line above does *not* expand to this (which wouldn't compile):(這行代碼不會是擴展成如下無法編譯的代碼)
val version = Container<String>.classVersion
// Instead, it expands to this:(它是會在即將進入編譯期會擴展成如下代碼)
val version = Container.classVersio
我們再次看到Kotlin并不總是逐字替換擴展的,特別是在其他情況下是有幫助的。
需要注意的點
在你使用類型別名的時候,這有一些注意的點你需要記住。
只能定義在頂層位置
類型別名只能定義在代碼頂層位置,換句話說,他們不能被內(nèi)嵌到一個類、對象、接口或者其他的代碼塊中。如果你執(zhí)意要這樣做,你將會得到一個來自編譯器的錯誤:
Nested and local type aliases are not supported.(不支持嵌套和本地類型別名)
然而,你可以限制類型別名的訪問權(quán)限,比如像常見的訪問權(quán)限修飾符internal和private。所以如果你想要讓一個類型別名只能在一個類中被訪問,你只需要將類型別名和這個類放在同一個文件即可,并且這個別名標記為private來修飾,比如像這樣:
private typealias Message = String
object Messages {
val greeting: Message = "Hello"
}
有趣的是,這個private類型別名可以出現(xiàn)在公共區(qū)域,例如以上的代碼 greeting: Message。
與Java的互操作性
你能在Java代碼中使用Kotlin的類型別名嗎?
你不能,它們在Java中是不可見的。
但是,如果在Kotlin代碼你有引用類型別名,類似這樣的:
typealias Greeting = String
fun welcomeUser(greeting: Greeting) {
println("$greeting, user!")
}
雖然你的Java代碼不能使用別名,但是可以通過使用底層類型繼續(xù)與它交互,類似這樣:
// Using type String here instead of the alias Greeting(使用String類型,而不是使用別名Greeting)
String hello = "Hello";
welcomeUser(hello);
遞歸別名
總的來說可以為別名取別名:
typealias Greeting = String
typealias Salutation = Greeting
然而,你明確不能有一個遞歸類型別名定義:
typealias Greeting = Comparable<Greeting>
編譯器會拋出如下異常信息:
Recursive type alias in expansion: Greeting
類型投影
如果你創(chuàng)建了一個類型投影,請注意你期望的樣子。例如,我們有這樣的代碼:
class Box<T>(var item: T)
typealias Boxes<T> = ArrayList<Box<T>>
fun read(boxes: Boxes<out String>) = boxes.forEach(::println)
然后我們就期望它這樣定義:
val boxes: Boxes<String> = arrayListOf(Box("Hello"), Box("World"))
read(boxes) // Oops! Compiler error here.(這里有編譯錯誤)
這個報錯誤的原因是 Boxes<out String> 會擴展成 ArrayList<Box<out T>> 而不是 ArrayList<out Box<out T>>
Import As: 類型別名(Type Alias)的親兄弟
這里有個非常類似于類型別名(type alias)的概念,叫做 Import As. 它允許你給一個類型、函數(shù)或者屬性一個新的命名,然后你可以把它導入到一個文件中。例如:
import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder
在這種情況下,我們從NotificationCompat導入了Builder類,但是在當前文件中,它將以名稱NotificationBuilder的形式出現(xiàn)。
你是否遇到過需要導入兩個同名的類的情況?
如果有,那么你可以想象一下 Import As將會帶來巨大的幫助,因為它意味著你不需要去限定這些類中某個類。
例如,查看以下Java代碼,我們可以將數(shù)據(jù)庫模型中的User轉(zhuǎn)換為service模型的User。
package com.example.app.service;
import com.example.app.model.User;
public class UserService {
public User translateUser(com.example.app.database.User user) {
return new User(user.getFirst() + " " + user.getLast());
}
}
由于此代碼處理兩個不同的類,但是這兩個類都叫User,因此我們無法將它們兩者都同時導入。相反,我們只能將其中某個以類名+包名全稱使用User。
利用Kotlin中的 Import As, 我們就不需要以全稱類名的形式使用,我僅僅只需要給它另一個命名,然后去導入它即可。
package com.example.app.service
import com.example.app.model.User
import com.example.app.database.User as DatabaseUser
class UserService {
fun translateUser(user: DatabaseUser): User =
User("${user.first} ${user.last}")
}
此時的你,或許想知道,類型別名(type alias)和 Import As之間的區(qū)別?畢竟,您還可以用typealias消除User引用的沖突,如下所示:
package com.example.app.service
import com.example.app.model.User
typealias DatabaseUser = com.example.app.database.User
class UserService {
fun translateUser(user: DatabaseUser): User =
User("${user.first} ${user.last}")
}
沒錯,事實上,除了元數(shù)據(jù)(metadata)之外,這兩個版本的UserService都可以編譯成相同的字節(jié)碼!
所以,問題來了,你怎么去選擇你需要那一個?它們之間有什么不同? 這里列舉了一系列有關(guān) typealias 和 import as 各自支持特性情況如下:
目標對象(Target) | 類型別名(Type Alias) | Import As |
---|---|---|
Interfaces and Classes | yes | yes |
Nullable Types | yes | no |
Generics with Type Params | yes | no |
Generics with Type Arguments | yes | no |
Function Types | yes | no |
Enum | yes | yes |
Enum Members | no | yes |
object | yes | yes |
object Functions | no | yes |
object Properties | no | yes |
正如你所看到的,一些目標對象僅僅被支持一種或多種。
這兒有一些內(nèi)容需要被牢記:
- 類型別名可以具有可見性修飾符,如internal和private,而它訪問的范圍是整個文件。
- 如果您從已經(jīng)自動導入的包中導入類,例如kotlin.*或kotlin.collections*,那么您必須通過該名稱引用它。 例如,如果您要將import kotlin.String寫為RegularExpression,則String的用法將引用java.lang.String.
順便說一下,如果您是Android開發(fā)人員,并且在您的項目中使用到了 Kotlin Android Extensions,那么使用import as將是一個美妙的方式去重命名來自于Activity中對應布局的id,將原來布局中下劃線分割的id,可以重命名成駝峰形式,使你的代碼更具有可讀性。例如:
import kotlinx.android.synthetic.main.activity.upgrade_button as upgradeButton
這可以使您從findViewById()(或Butter Knife)轉(zhuǎn)換到Kotlin Android Extensions變得非常簡單!
總結(jié)
使用類型別名是一種很好的方式,它可以為復雜,冗長和抽象的類型提供簡單,簡潔和特定于域的名稱。它們易于使用,并且IDE工具支持可讓您深入了解底層類型。在正確的地方使用,它們可以使您的代碼更易于閱讀和理解。
譯者有話說
- 1、為什么我要翻譯這篇博客?
typealias類型別名,可能有的Kotlin開發(fā)人員接觸到過,有的還沒有碰到過。接觸過的,可能也用得不多,不知道如何更好地使用它。這篇博客非常好,可以說得上是Kotlin中的typealias的深入淺出。它闡述了什么是類型別名、類型別名的使用場景、類型別名的實質(zhì)原理、類型別名和import as對比以及類型別名中需要注意的坑。看完這篇博客,仿佛打開kotlin中的又一個新世界,你將會很神奇發(fā)現(xiàn)一個小小typealias卻如此強大,深入實質(zhì)原理你又會發(fā)現(xiàn)原來也挺簡單的,但是無不被kotlin這門語言設(shè)計思想所折服,使用它可以大大簡化代碼以及提升代碼的可讀性。所以對于Kotlin的初學者以及正在使用kotlin開發(fā)的你來說,它可能會對你有幫助。
- 2、這篇博客中幾個關(guān)鍵點和注意點。
關(guān)于typealias我之前有篇博客淺談Kotlin語法篇之Lambda表達式完全解析(六)也大概介紹了下,但是這篇博客已經(jīng)介紹的非常詳細,這里再次強調(diào)其中比較重要幾點:
- 類型別名(typealias)不會創(chuàng)建新的類型。他們只是給現(xiàn)有類型取了另一個名稱而已.
- typealias實質(zhì)原理,大部分情況下是在編譯時期采用了逐字替換的擴展方式,還原成真正的底層類型;但是不是完全是這樣的,正如本文例子提到的那樣。
- typealias只能定義在頂層位置,不能被內(nèi)嵌在類、接口、函數(shù)等內(nèi)部
- 使用import as對于已經(jīng)使用Kotlin Android Extension 或者anko庫的Android開發(fā)人員來說非常棒。看下以下代碼例子:
沒有使用import as
//使用anko庫直接引用布局中下劃線id命名,看起來挺別扭,不符合駝峰規(guī)范。
import kotlinx.android.synthetic.main.review_detail.view.*
class WidgetReviewDetail(context: Context, parent: ViewGroup){
override fun onViewCreated() {
mViewRoot.run {
review_detail_tv_checkin_days.isBold()
review_detail_tv_course_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
review_detail_tv_elevator_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
review_detail_tv_word_num.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
}
}
override fun renderWidget(viewModel: VModelReviewDetail) = with(viewModel) {
mViewRoot.review_detail_iv_avatar.loadUrl(url = avatarUrl)
mViewRoot.review_detail_tv_checkin_days.text = labelCheckInDays
mViewRoot.review_detail_tv_word_num.text = labelWordNum
mViewRoot.review_detail_tv_elevator_num.text = labelElevatorNum
mViewRoot.review_detail_tv_course_num.text = labelCourseNum
}
}
使用import as 整體代碼更加簡單和更具有可讀性,此外還有一個好處就是布局文件ID變了,只需要import as聲明處修改即可,無需像之前那樣每個用到的地方都需要修改
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_checkin_days as tvCheckInDays
import kotlinx.android.synthetic.main.review_detail.view.review_detail_iv_avatar as ivAvatar
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_word_num as tvWordNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_elevator_num as tvElevatorNum
import kotlinx.android.synthetic.main.review_detail.view.review_detail_tv_course_num as tvCourseNum
class WidgetReviewDetail(context: Context, parent: ViewGroup){
override fun onViewCreated() {
mViewRoot.run {
tvCheckInDays.isBold()
tvCourseNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
tvElevatorNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
tvWordNum.typeface = FontUtil.getFont(mContext, FontUtil.FONT_OSWALD_MEDIUM)
}
}
override fun renderWidget(viewModel: VModelReviewDetail) {
with(viewModel) {
mViewRoot.ivAvatar.loadUrl(url = avatarUrl)
mViewRoot.tvCheckInDays.text = labelCheckInDays
mViewRoot.tvWordNum.text = labelWordNum
mViewRoot.tvElevatorNum.text = labelElevatorNum
mViewRoot.tvCourseNum.text = labelCourseNum
}
}
}
歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟,這里有最新Kotlin技術(shù)文章,每周會不定期翻譯一篇Kotlin國外技術(shù)文章。如果你也喜歡Kotlin,歡迎加入我們~~~