Kotlin专题「十九」:类型别名(type alias)

前言:越努力,越幸运,若是不幸运,就一定是不够努力。

一、概述

  类型别名为现有类型提供了替代名称。如果类型名称太长,可以引入一个不同的较短名称并使用新的名称。

interface RestaurantPatron {
    
    
    fun makeReservation(restaurant: Organization<(String, Int?) -> String>)
    fun visit(restaurant: Organization<(String, Int?) -> String>)
    fun complainAbout(restaurant: Organization<(String, Int?) -> String>)
}

当你看到很多代码拥挤在一起的时候,代码就显得很混乱可读性很差,Kotlin 为我们提供了一种简单的方法将复杂类型简化成更可读性的别名。

针对上面的代码,我们通过创建类型别名来优化:

typealias Restaurant = Organization<(String, Int?) -> String>

现在不需要每次都声明 Organization<(String, Int?) -> String>,而是可以像下面那样表达出 Restaurant 术语:

interface RestaurantPatron {
    
    
    fun makeReservation(restaurant: Restaurant)
    fun visit(restaurant: Restaurant)
    fun complainAbout(restaurant: Restaurant)
}

这样避免在接口 RestaurantPatron 中大量重复的类型,仅仅写 restaurant: Restaurant 即可,而且修改这种复杂的类型是很方便的。例如,如果要将原来的 Organization<(String, Int?) -> String> 修改成 Organization<(Currency, Coupon?) -> User>,只需要修改一处即可,而不需要修改三个地方。

typealias Restaurant = Organization<(Currency, Coupon?) -> User>

二、类型别名

缩短长泛型类型是非常实用的,比如,经常缩小集合类型:

typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

你可以为函数类型提供不同的别名:

typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

你可以为内部和嵌套类有新的名称:

class A {
    
    
    inner class Inner
}
class B {
    
    
    inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。它们等同于相应的基础类型。当你在代码中添加 typealias Predicate<T> 并使用 Predicate<Int> 时,Kotlin 编译器总是将其拓展为 (Int) -> Boolean 。因此,你可以传递你的类型的变量,每当一个通用的函数类型是必须的,反之亦然:

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    
    
    val f: (Int) -> Boolean = {
    
     it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = {
    
     it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

2.1 可读性

有些人会说不明白这是如何有助于可读性,上面的示例中的参数名称已经明确表明了 restaurant,为什么还需要 Restaurant 类型,难道我们不能使用具体的类型名称和抽象类型吗?

扫描二维码关注公众号,回复: 16425166 查看本文章

没错,参数的名称更应该具体地表示类型,但是上面的 RestaurantPatron 接口的别名版本更具有可读性,也不容易受到侵入。

然而有些情况下是没有命名的,或者说他们没有一个确切的类型名称,比如 Lambda 表达式类型:

interface RestaurantService {
    
    
    var locator: (String, Person) -> List<Organization<(String, Int?) -> String>>
}

上面表示 locator 这个 Lambda 表达式正在返回一个 restaurant 的列表,但是获取这些含义的唯一线索就是接口名称。然而仅仅从 locator 函数类型中没有明确得到,因为冗长的类型定义已经失去了含义本质。然而下面的版本,只需要一眼就可以看出:

interface RestaurantService {
    
    
    var locator: (String, Person) -> List<Restaurant>
}

2.2 间接性

我们已经引入间接性寻址-有些被别名掩盖了具体类型细节,一直在做隐藏命名背后的细节事情,比如:

  • 我们不是把具体常量数值 9.9 写到代码中,而是创建一个静态常量:ACCELERATION_DUE_TO_GRAVITY,在代码中使用静态常量;
  • 我们不会把一个表达式 6.28 * radius 实现写在代码任何地方,而是把这个表达式放入到 circumference() 函数中去,然后在代码中调用 circumference() 函数。

如果需要查看别名背后隐藏细节是什么,Ctrl+click即可。

2.3 继承性

有人会说,为什么需要类型别名呢,我可以使用继承的方式,继承这个复杂的类型:

class Restaurant : Organization<(String, Int?) -> String>()

但是类型别名适用性很广,它也适用于你通常不能或者不会去继承的类型。例如:

  • 非 open 的一些类,例如 String 或者 Java 的 Optional<T>;
  • Kotlin 中的单例对象实例( object );
  • 函数类型,比如: (String, Int?) -> String;
  • 函数接收者类型,比如: (String, Int?) -> String

三、类型别名的理解

处理类型别名的时候,我们有两个类型需要思考:

  • 别名(alias)
  • 底层类型(underlying type)

据说它本身是一个别名(如UserId),或者包含别名(如List)的缩写类型。

当 Kotlin 编译器编译你的代码时,所有对 UserId 的引用都会拓展到 UniqueIdentifier。也就是说,在拓展间,编译器大概做了类似于在代码中搜索别名(UserId)所用到的地方,然后将代码中用到的地方逐字地将别名替换成全称类型名(UniqueIdentifier)的工作。

虽然这是我们理解别名的一个好的起点,但是有一些情况 Kotlin 不完全是通过逐字替换原理来实现的,下面会证实,你只需要记住逐字替换原理是有效的。

3.1 类型别名和类型安全

下面是一个使用多个别名的例子:

typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier
 
interface Store {
    
    
    fun purchase(user: UserId, product: ProductId): Person
}

一旦我们拿到 Store 实例,可以进行使用:

val receipt = store.purchase(productId, userId)

很明显,调用的顺序弄反了,userId 应该是第一个参数,productId 应该是第二个参数。

为什么编译器没有提示这个错误?

如果我们按照上面的逐字替换原理,可以模拟编译器拓展出的代码。两个参数类型都拓展相同的底层类型,这意味着它们可以混在一起使用,并且编译器无法分辨出对应的参数。

真正的原因是:类型别名不会创建新的类型,它们只是给现有的类型取一个别名而已。

我们来比较两种不同方式对类型命名:

  • 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" } // compiler error!(编译器错误)

使用 lambda 表达式的类型别名可以正常运行,而继承方式甚至不能编译,提示错误如下:

Required: InheritedSupplier / Found: () -> String

实际上,调用 writeInherited() 的唯一方法:

writeInherited(object : InheritedSupplier {
    
    
    override fun invoke(): String = "Hello"
})

所以类型别名的方式比基于继承的方式上更具有优势。当然,某些情况下类型安全更重要,类型别名可以根据实际情况来选用。

3.2 类型别名案例

这里提供一些关于类型别名的建议:

// 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

3.3 基于类型别名特别操作

一旦创建了类型别名,就可以在各种场景中使用它来替代底层类型。

  • 在声明变量类型,参数类型和返回值类型的时候;
  • 在作为类型参数约束和类型参数的时候;
  • 在使用比较类型 is 或者强转类型 as 的时候;
  • 在获得函数引用的时候。

除了上面的几种用法,还有其他的用法细节,我们一起来看看:

(1)构造器

如果底层类型有构造器,那么它的类型别名也是如此,甚至可以在一个可空类型的别名上调用构造函数:

class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?
 
//使用别名来构造对象
val member =  MaybeTeamMember("Miguel")
 
//以上代码不会是逐字扩展成如下无法编译的代码
val member = TeamMember?("Miguel")
 
//而是扩展如下代码
val member = TeamMember("Miguel")

你可以看到编译时的拓展并不总是逐字拓展的,如果底层类型本身就没有构造器(例如接口或者类型投影),那么不能通过别名来调用构造器。

(2)伴生对象

你可以通过包含伴生对象类的别名来调用该类的伴生对象中的属性和方法。即使底层类型具有指定的具体类型参数,例如:

class Container<T>(var item: T) {
    
    
    companion object {
    
    
        const val classVersion = 5
    }
}
 
//注意此处的String是具体的参数类型
typealias BoxedString = Container<String>
 
//通过别名获取伴侣对象的属性:
val version = BoxedString.classVersion
 
//这行代码不会是扩展成如下无法编译的代码
val version = Container<String>.classVersion
 
//它是会在即将进入编译期会扩展成如下代码
val version = Container.classVersio

3.4 其他问题

(1)只能定义在顶层位置

类型别名只能定义在顶层位置,也就是说,它不能被内嵌到一个类,对象,接口或者其他代码块中。否则会报错:

Nested and local type aliases are not supported.(不支持嵌套和本地类型别名)

但是,你可以限制类型别名的访问权限,比如常见的访问修饰符 internalprivate。所以如果你想让一个类型别名只能在一个类中被访问,你只需要将类型别名和这个类放在同一个文件即可,并且这个别名标记为 private 修饰。例如:

private typealias Message = String
 
object Messages {
    
    
    val greeting: Message = "Hello"
}

这个 private 类型别名可以出现在公共区域,比如上面的 greeting: Message

(2)与Java的互操作性

你能在 Java 代码中使用 Kotlin 的类型别名吗?答案是不能。它们在 Java 中是不可见的。

但是如果在 Kotlin 代码中你有引用类型别名,如下:

typealias Greeting = String
 
fun welcomeUser(greeting: Greeting) {
    
    
    println("$greeting, user!")
}

虽然 Java 代码不能使用类型别名,但是可以通过使用底层类型继续与它进行交互,例如:

// 使用String类型,而不是使用别名Greeting
String hello = "Hello";
welcomeUser(hello);
(3)递归别名

另外,可以为别名取别名:

typealias Greeting = String
typealias Salutation = Greeting 

然而,你需要明确不能有一个递归类型别名定义:

typealias Greeting = Comparable<Greeting>

编译器会报错:

Recursive type alias in expansion: Greeting
(4)类型投影

如果你创建了一个类型投影,请注意你想要的,例如:

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) //Compiler error

是因为 Boxes<out String> 会拓展成 ArrayList<Box<out T>> 而不是 ArrayList<out Box<out T>>

3.5 Import As: 类型别名的亲兄弟

Import As 是非常类似类型别名的概念,它允许你给一个类型,函数或者属性一个新的命名,然后你就可以把它导入到文件中。例如:

import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder

这种情况下,我们从 NotificationCompat 导入了 Builder 类,但是在当前文件中,它的名称将以 NotificationBuilder 的形式出现。

相信很多人都遇到过需要导入两个同名的类的情况。你可以想象以下 Import As 将会带来巨大的帮助,因为它意味着你不需要去限定这些类中的某个类。

package com.suming.kotlindemo.blog
 
import com.suming.kotlindemo.User
 
class TypeAliasesActivity {
    
    
    fun translateUser(user: com.suming.kotlindemo.blog.User): User? {
    
    
        return User(user.name, 1, "女")
    }
}

由于上面处理了两个不同的同名 User 类,因此我们无法都将他们两者都同时导入。所以我们只能将其中某个以类名+包名的形式声明 User。

我们可以将 com.suming.kotlindemo.blog.User 转换为 kotlindemo 模型中的 User。利用 Kotlin 中的 Import As ,我们就不需要以全称类名的形式使用了,仅仅需要给它另起一个命名,然后导入它即可。

package com.suming.kotlindemo.blog
 
import com.suming.kotlindemo.User
import com.example.app.database.User as DatabaseUser
 
class TypeAliasesActivity {
    
    
    fun translateUser(user: DatabaseUser): User? = User(user.name, 1, "女")
}

你可以用类型别名消除 User 的引用冲突:

package com.suming.kotlindemo.blog
 
import com.suming.kotlindemo.User
 
typealias DatabaseUser = com.example.app.database.User
 
class TypeAliasesActivity {
    
    
    fun translateUser(user: DatabaseUser): User? = User(user.name, 1, "女")
}

事实上,除了元数据(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.btn_upgrade_button as btnUpgrade

//使用
btnUpgrade.text = "按钮"

四、总结

使用类型别名可以将复杂,冗长和抽象的类型提供简洁和特定于域的名称。它们易于使用,并且IDE工具支持深入了解底层类型。在正确的地方使用,让你的代码更易于阅读和理解。

再次强调重要的几点:

  • 1.类型别名 type alias 不会创建新的类型,它只是给现有的类型提供一个新的名称而已;
  • 2.类型别名的实际原理,有一大部分情况下是在编译时期采取逐字替换的拓展方式,还原成真正的底层类型;
  • 3.类型别名只能定义在顶层位置,不能内嵌在类,接口,函数,代码块等内部。

源码地址:https://github.com/FollowExcellence/KotlinDemo-master

点关注,不迷路


好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才

我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !

要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!

猜你喜欢

转载自blog.csdn.net/m0_37796683/article/details/109100517