翻譯說明:
原標題: All About Type Aliases in Kotlin
原文地址: typealias.com/guides/all-…
原文作者: Dave Leeds
你是否經歷過像下面的對話?
希望你在現實生活中沒有像這樣的對話,但是這樣情景可能會出現在你的程式碼中。
例如,看下這個程式碼:
interface RestaurantPatron {
fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}
複製程式碼
當你看到很多型別的程式碼被擠在一起的時候,你很容易迷失在程式碼的細節中。事實上,僅僅看這些函式的宣告就感覺挺嚇人的。
幸運的是,Kotlin為我們提供了一種簡單的方法來將複雜型別簡化成更具可讀性的別名。
在這篇文章中:
- 我們將學習關於型別別名的一切內容以及他們的工作原理。
- 然後,我們將看看你可能會使用到關於它們的一些方法。
- 然後,我們將會看下有關它們需要注意的點。
- 最後,我們來看看一個類似的概念, Import As, 並看看它們之間的比較。
介紹Type Aliases(型別別名)
一旦我們為某個概念創造了一個術語,其實我們就沒必要每次談論到它的時候都要去描述一下這個概念,我們只需要使用這個術語就可以了! 所以讓我們程式碼也去做類似事情吧。讓我們來看看這個複雜的型別並給它一個命名。
針對上面的程式碼,我們將通過建立一個型別的別名來優化它:
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>
複製程式碼
現在,在每個描述restaurant概念的地方,而不是每次都去寫出 Organization<(Currency, Coupon?) -> Sustenance> 宣告,而是可以像下面這樣表達出 Restaurant術語:
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>
複製程式碼
簡單!
你或許會思考...
可讀性
你可能會對自己說,“我不明白這是如何有助於程式碼的可讀性的...,由於上述的示例中引數的名稱已經明確表明了restaurant的概念,為什麼我還需要一個Restaurant型別呢?難道我們不能使用具體的引數名稱和抽象型別嗎?”
是的,引數的名稱確實它應該可以更具體地表示型別,但是我們上面的RestaurantPatron介面的別名版本仍然更具有可讀性,並且也不容易受到侵入
然而,有些情況下是沒有命名的,或者說他們沒有一個確切型別名稱,例如Lambda表示式的型別:
interface RestaurantService {
var locator: (String, ZipCode) -> List<Organization<(Currency, Coupon?) -> Sustenance>>
}
複製程式碼
在上面那段程式碼中,仍然在表示locator這個lambda表示式正在返回一個restaurant的列表,但是獲取這些表示含義的資訊唯一線索就是介面的名稱。然而僅僅從locator函式型別中沒有那麼明確得到,因為冗長的型別定義已經失去了含義本質。
而下面的這個版本,只需要看一眼就能很容易理解:
interface RestaurantService {
var locator: (String, ZipCode) -> List<Restaurant>
}
複製程式碼
間接性
你或許會想,“等等,我需要更多地考慮型別別名嗎?之前沒有型別別名的時候,把確切真實的型別直接暴露在外部宣告處,現在卻要將他們隱藏在別名的後面”
當然,我們已經引入了一層間接定址-有些被別名掩蓋了具體型別細節。但是作為程式設計師,我們一直在做著隱藏命名背後的細節事情。例如:
- 我們不會把具體常量數值9.8寫到我們程式碼中,而是我們建立一個靜態常量ACCELERATION_DUE_TO_GRAVITY,在程式碼中直接使用靜態常量。
- 我們不會把一個表示式 6.28 * radius 實現寫在程式碼任何地方,而是把這個表示式放入到一個 circumference() 函式中去,然後在程式碼中去呼叫circumference() 函式
記住-如果我們需要去檢視別名背後隱藏細節是什麼,僅僅只需要在IDE中使用Command+Click即可。
繼承性
或者你也許在想,"我為什麼需要一個型別別名呢?我可以使用繼承方式,來繼承這個複雜型別" 如下所示:
class Restaurant : Organization<(Currency, Coupon?) -> Sustenance>()
複製程式碼
沒錯,在這種情況下,你確實可以通過其詳細的型別引數對 Organization 類進行子類化。事實上,你可能在Java中看到了這一點。
但是型別別名適用性很廣,它也適用於你不能或通常不會去繼承的型別。例如:
- 非 open 一些的類 例如:String,或者Java中的Optional<T>
- Kotlin中的單例物件例項( object )。
- 函式型別,例如: (Currency, Coupon?) -> Sustenance
- 甚至函式接收者型別,例如: Currency.(Coupon?) -> Sustenance
在文章後面的部分,我們將更多地比較型別別名方法和繼承方法。
理解型別別名(Type Aliases)
我們已經瞭解過如何簡單地去宣告一個型別別名。現在讓我們放大一些,這樣我們就可以瞭解建立時發生的原理!
當處理型別別名的時候,我們有兩個型別需要去思考:
- 別名(alias)
- 底層型別(underlying type)
據說它本身是一個別名(如UserId),或者包含別名(如List<UserId>)的縮寫型別
當Kotlin編譯器編譯您的程式碼時,所有使用到的相應縮寫型別將會擴充套件成原來的全型別。讓我們看一個更為完整例子。
class UniqueIdentifier(val value: Int)
typealias UserId = UniqueIdentifier
val firstUserId: UserId = UserId(0)
複製程式碼
當編譯器處理上述程式碼時,所有對 UserId 的引用都會擴充套件到 UniqueIdentifier
換句話說,在擴充套件期間,編譯器大概做了類似於在程式碼中搜尋別名(UserId)所有用到的地方,然後將程式碼中用到的地方逐字地將其別名替換成全稱型別名(UniqueIdentifier)的工作。
你可能已經注意到我使用了“大部分”和“大概”等字樣。 這是因為,雖然這是我們理解型別別名的一個好的起點,但有一些情況下Kotlin不完全是通過逐字替換原理來實現。 我們將馬上闡述這些內容! 現在,我們只需記住這個逐字替換原理通常是有效的。
順便說一下,如果你使用IntelliJ IDEA,你會很高興發現IDE對型別別名有一些很好的支援。例如,您可以在程式碼中看到別名和底層型別:
並且可以快速檢視宣告文件:
型別別名和型別安全
現在我們已經瞭解了型別別名的基礎知識,下面我們來探討另一個例子。這一個使用多個別名例子:
typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier
interface Store {
fun purchase(user: UserId, product: ProductId): Receipt
}
複製程式碼
一旦我們拿到了我們 Store 的一個例項,我們可以進行購買:
val receipt = store.purchase(productId, userId)
複製程式碼
此時,你是否注意到什麼了?
我們意外地把我們的呼叫引數順序弄反了! userId應該是第一個引數,而productId應該是第二個引數!
為什麼編譯器沒有提示我們這個問題呢?
如果我們按照上面的逐字替換原理,我們可以模擬編譯器擴充套件出的程式碼:
哇!兩個引數型別都擴充套件為相同的底層型別!這意味著可以將它們混在一起使用,並且編譯器無法分辨出對應引數。
一個重大的發現: 型別別名不會建立新的型別。他們只是給現有型別取了另一個名稱而已
當然,這也就是為什麼我們可以給一個沒有子類繼承的非 open的類新增型別別名。
雖然你可能認為這總是一件壞事,但實際上有些情況下它是有幫助的!
我們來比較兩種不同的方式對型別命名:
- 1、使用 型別別名
- 2、使用 繼承 去建立一個子型別(如上面的繼承部分所述)。
兩種情況下的底層型別都是String提供者,它只是一個不帶引數並返回String的函式。
typealias AliasedSupplier = () -> String
interface InheritedSupplier : () -> String
複製程式碼
現在,我們去建立一對函式去接收這些提供者:
fun writeAliased(supplier: AliasedSupplier) =
println(supplier.invoke())
fun writeInherited(supplier: InheritedSupplier) =
println(supplier.invoke())
複製程式碼
最後,我們準備去呼叫這些函式:
writeAliased { "Hello" }
writeInherited { "Hello" } // Zounds! A compiler error!(編譯器錯誤)
複製程式碼
使用lambda表示式的型別別名方式可以正常執行,而繼承方式甚至不能編譯!相反,它給了我們這個錯誤資訊:
Required: InheritedSupplier / Found: () -> String
事實上,我發現實際呼叫writeInherited()的唯一方法,像下面這樣拼湊一個冗長的內容。
writeInherited(object : InheritedSupplier {
override fun invoke(): String = "Hello"
})
複製程式碼
所以在這種情況下,型別別名方式相比基於繼承的方式上更具有優勢。
當然,在某些情況下,型別安全將對您更為重要,在這種情況下,型別別名可能不適合您的需求。
型別別名的例子
現在我們已經很好地掌握了型別別名,讓我們來看看一些例子!這裡將為你提供一些關於型別別名的建議:
// Classes and Interfaces (類和介面)
typealias RegularExpression = String
typealias IntentData = Parcelable
// Nullable types (可空型別)
typealias MaybeString = String?
// Generics with Type Parameters (型別引數泛型)
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>
// Generics with Concrete Type Arguments (混合型別引數泛型)
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 (函式型別)
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
複製程式碼
你可以基於型別別名可以做很酷的操作
正如我們所看到的一樣,一旦建立了別名就可以在各種場景中使用它來代替底層型別,比如:
- 在宣告變數型別、引數型別和返回值型別的時候
- 在作為型別引數約束和型別引數的時候
- 在使用比較型別is或者強轉型別的as的時候
- 在獲得函式引用的時候
除了以上那些以外,它還有一些其他的用法細節。讓我們一起來看看:
構造器(Constructors)
如果底層型別有一個構造器,那麼它的型別別名也是如此。你甚至可以在一個可空型別的別名上呼叫建構函式!
class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?
// Constructing with the alias: 使用別名來構造物件
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")
複製程式碼
所以你可以看到編譯時的擴充套件並不總是逐字擴充套件的,在這個例子中就是很有效的說明。
如果底層型別本身就沒有構造器(例如介面或者型別投影),自然地你也不可能通過別名來呼叫構造器。
伴生物件
你可以通過含有伴生物件類的別名來呼叫該類的伴生物件中的屬性和方法。即使底層型別具有指定的具體型別引數,也是如此。一起來看下:
class Container<T>(var item: T) {
companion object {
const val classVersion = 5
}
}
// Note the concrete type argument of String(注意此處的String是具體的引數型別)
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並不總是逐字替換擴充套件的,特別是在其他情況下是有幫助的。
需要注意的點
在你使用型別別名的時候,這有一些注意的點你需要記住。
只能定義在頂層位置
型別別名只能定義在程式碼頂層位置,換句話說,他們不能被內嵌到一個類、物件、介面或者其他的程式碼塊中。如果你執意要這樣做,你將會得到一個來自編譯器的錯誤:
Nested and local type aliases are not supported.(不支援巢狀和本地型別別名)
然而,你可以限制型別別名的訪問許可權,比如像常見的訪問許可權修飾符internal和private。所以如果你想要讓一個型別別名只能在一個類中被訪問,你只需要將型別別名和這個類放在同一個檔案即可,並且這個別名標記為private來修飾,比如像這樣:
private typealias Message = String
object Messages {
val greeting: Message = "Hello"
}
複製程式碼
有趣的是,這個private型別別名可以出現在公共區域,例如以上的程式碼 greeting: Message。
與Java的互操作性
你能在Java程式碼中使用Kotlin的型別別名嗎?
你不能,它們在Java中是不可見的。
但是,如果在Kotlin程式碼你有引用型別別名,類似這樣的:
typealias Greeting = String
fun welcomeUser(greeting: Greeting) {
println("$greeting, user!")
}
複製程式碼
雖然你的Java程式碼不能使用別名,但是可以通過使用底層型別繼續與它互動,類似這樣:
// 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
型別投影
如果你建立了一個型別投影,請注意你期望的樣子。例如,我們有這樣的程式碼:
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. 它允許你給一個型別、函式或者屬性一個新的命名,然後你可以把它匯入到一個檔案中。例如:
import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder
複製程式碼
在這種情況下,我們從NotificationCompat匯入了Builder類,但是在當前檔案中,它將以名稱NotificationBuilder的形式出現。
你是否遇到過需要匯入兩個同名的類的情況?
如果有,那麼你可以想象一下 Import As將會帶來巨大的幫助,因為它意味著你不需要去限定這些類中某個類。
例如,檢視以下Java程式碼,我們可以將資料庫模型中的User轉換為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之間的區別?畢竟,您還可以用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}")
}
複製程式碼
沒錯,事實上,除了後設資料(metadata)之外,這兩個版本的UserService都可以編譯成相同的位元組碼!
所以,問題來了,你怎麼去選擇你需要那一個?它們之間有什麼不同? 這裡列舉了一系列有關 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 |
正如你所看到的,一些目標物件僅僅被支援一種或多種。
這兒有一些內容需要被牢記:
- 型別別名可以具有可見性修飾符,如internal和private,而它訪問的範圍是整個檔案。
- 如果您從已經自動匯入的包中匯入類,例如kotlin.*或kotlin.collections*,那麼您必須通過該名稱引用它。 例如,如果您要將import kotlin.String寫為RegularExpression,則String的用法將引用java.lang.String.
順便說一下,如果您是Android開發人員,並且在您的專案中使用到了 Kotlin Android Extensions,那麼使用import as將是一個美妙的方式去重新命名來自於Activity中對應佈局的id,將原來佈局中下劃線分割的id,可以重新命名成駝峰形式,使你的程式碼更具有可讀性。例如:
import kotlinx.android.synthetic.main.activity.upgrade_button as upgradeButton
複製程式碼
這可以使您從findViewById()(或Butter Knife)轉換到Kotlin Android Extensions變得非常簡單!
總結
使用型別別名是一種很好的方式,它可以為複雜,冗長和抽象的型別提供簡單,簡潔和特定於域的名稱。它們易於使用,並且IDE工具支援可讓您深入瞭解底層型別。在正確的地方使用,它們可以使您的程式碼更易於閱讀和理解。
譯者有話說
- 1、為什麼我要翻譯這篇部落格?
typealias型別別名,可能有的Kotlin開發人員接觸到過,有的還沒有碰到過。接觸過的,可能也用得不多,不知道如何更好地使用它。這篇部落格非常好,可以說得上是Kotlin中的typealias的深入淺出。它闡述了什麼是型別別名、型別別名的使用場景、型別別名的實質原理、型別別名和import as對比以及型別別名中需要注意的坑。看完這篇部落格,彷彿開啟kotlin中的又一個新世界,你將會很神奇發現一個小小typealias卻如此強大,深入實質原理你又會發現原來也挺簡單的,但是無不被kotlin這門語言設計思想所折服,使用它可以大大簡化程式碼以及提升程式碼的可讀性。所以對於Kotlin的初學者以及正在使用kotlin開發的你來說,它可能會對你有幫助。
- 2、這篇部落格中幾個關鍵點和注意點。
關於typealias我之前有篇部落格淺談Kotlin語法篇之Lambda表示式完全解析(六)也大概介紹了下,但是這篇部落格已經介紹的非常詳細,這裡再次強調其中比較重要幾點:
- 型別別名(typealias)不會建立新的型別。他們只是給現有型別取了另一個名稱而已.
- typealias實質原理,大部分情況下是在編譯時期採用了逐字替換的擴充套件方式,還原成真正的底層型別;但是不是完全是這樣的,正如本文例子提到的那樣。
- typealias只能定義在頂層位置,不能被內嵌在類、介面、函式等內部
- 使用import as對於已經使用Kotlin Android Extension 或者anko庫的Android開發人員來說非常棒。看下以下程式碼例子:
沒有使用import as
//使用anko庫直接引用佈局中下劃線id命名,看起來挺彆扭,不符合駝峰規範。
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宣告處修改即可,無需像之前那樣每個用到的地方都需要修改
注意的一點是如果給每個View元件都用import as感覺又回到重新回到findViewById的,又會產生冗長宣告,這裡建議你慎重使用。但是此處出發點不一樣,目的在於簡化冗長的id命名的使用。
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
}
}
}
複製程式碼
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~