💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
Kotlin作为一门工程化的语言,拥有一些令开发者心旷神怡的特性。本章将带领大家一步步了解Kotlin中一个比较重要的特性——扩展(Extensions)。Kotlin的扩展其实是多态的一种表现形式,在深入了解扩展之前,让我们先探讨一下多态的不同技术手段。 ## 多态的不同方式 熟悉Java的读者对多态应该不会陌生,它是面向对象程序设计(OOP)的一个重要特征。当我们用一个子类继承一个父类的时候,这就是子类型多态(Subtype polymorphism)。另一种熟悉的多态是参数多态(Parametric polymorphism),我们在后面所讨论的泛型就是其最常见的形式。此外,也许你还会想到C++中的运算符重载,我们可以用特设多态(Ad-hoc polymorphism)来描述它。相比子类型多态和参数多态,可能你对特设多态会感到有些许陌生。其实这是一种更加灵活的多态技术,在Kotlin中,一些有趣的语言特性,如运算符重载、扩展都很好地支持这种多态。在本节接下来的内容中,我们将通过具体的例子来进一步展现各种多态的特点,并介绍更加深入的Kotlin语言特性。 ### 子类型多态 无论在前端、移动端还是后台开发中,数据持久化操作都是必不可少的。在Android中,原生就支持Sqlite的操作,一般我们会继承Sqlite操作的相关类: class CustomerDatabaseHelper(context:Context): SQLiteOpenHelper(context, "kotlinDemo.db", cursorFactory , db.version){ override fun onUpgrade(p0: SQLiteDatabase? , p1: Int, p2: Int) {} override fun onCreate(db: SQLiteDatabase) { val sql = "CREATE TABLE if not exists $tableName ( id integer PRIMARY KEY autoincrement, uniqueKey VARCHAR(32))" // 此处省略其他参数 db.execSQL(sql) } } 然后我们就可以使用父类DatabaseHelper的所有方法。这种用子类型替换超类型实例的行为,就是我们通常说的子类型多态。 ### 参数多态 在完成数据库的创建之后,现在我们要把客户(Customer)存入客户端数据库中。可能会写这样一个方法: ``` fun persist(customer: Customer) { db.save(customer.uniqueKey, customer) } ``` 如果代码成功执行,我们就成功地将customer以键值对的方式存入数据库(上述例子中以uniqueKey对应customer,便于查询等操作)。 但是,随着需求的变动,我们可能还会持久化多种类型的数据。如果每种类型都写一个presist方法,多少有些烦琐,通常我们会抽象一个方法来处理不同类型的持久化。因为我们采用键值对的方式存储,所以需要获取不同类型对应的uniqueKey: ``` interface KeyI { val uniqueKey : String } class ClassA(override val uniqueKey: String) : KeyI { … } class ClassB(override val uniqueKey: String) : KeyI { … } ``` 这样,class A、B都已经具备uniqueKey。我们可以将persist进行如下改写: ``` fun <T: KeyI> persist(t: T) { db.save(t.uniqueKey, t) } ``` 以上的多态形式我们可以称之为参数多态,其实最常见的参数多态的形式就是泛型。 参数多态在程序设计语言与类型论中是指声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用,所以它建立在运行时的参数基础上,并且所有这些都是在不影响类型安全的前提下进行的。 ### 对第三方类进行扩展 进一步思考,假使当对应的业务类ClassA、ClassB是第三方引入的,且不可被修改时,如果我们要想给它们扩展一些方法,比如将对象转化为Json,利用之前介绍的多态技术就会显得比较麻烦。 幸运的是,Kotlin支持扩展的语法,利用扩展我们就能给ClassA、ClassB添加方法或属性,从而换一种思路来解决上面的问题。 ``` fun ClassA.toJson(): String = { …… } ``` 如上我们给ClassA类扩展了一个将对象转换为Json的toJson方法。需要注意的是,扩展属性和方法的实现运行在ClassA实例,它们的定义操作并不会修改ClassA类本身。这样就为我们带来了一个很大的好处,即被扩展的第三方类免于被污染,从而避免了一些因父类修改而可能导致子类出错的问题发生。 当然,在Java中我们可以依靠其他的办法比如设计模式来解决,但相较而言依靠扩展的方案显得更加方便且合理,这其实也是另一种被称为特设多态的技术。下节我们就来了解下这种多态,然而再介绍Kotlin中另外一种同样可服务于它的语言特性——运算符重载。 ### 特设多态与运算符重载 除了子类型多态、参数多态以外,还存在一种更灵活的多态形式——特设多态(Ad-hoc polymorphism)。可能你对特设多态这个概念并不是很了解,我们来举一个具体的例子。 当你想定义一个通用的sum方法时,也许会在Kotlin中这么写: ``` fun <T> sum(x: T, y: T) : T = x + y ``` 但编译器会报错,因为某些类型T的实例不一定支持加法操作,而且如果针对一些自定义类,我们更希望能够实现各自定制化的“加法语义上的操作”。如果把参数多态做的事情打个比方:它提供了一个工具,只要一个东西能“切”,就用这个工具来切割它。然而,现实中不是所有的东西都能被切,而且材料也不一定相同。更加合理的方案是,你可以根据不同的原材料来选择不同的工具来切它。 再换种思路,我们可以定义一个通用的Summable接口,然后让需要支持加法操作的类来实现它的plusThat方法。就像这样子: interface Sumable<T> { fun plusThat(that: T): T } data class Len(val v: Int) : Sumable<Len> { override fun plusThat(that: Len) = Len(this.v + that.v) } 可以发现,当我们在自定义一个支持plusThat方法的数据结构如Len时,这种做法并没有什么问题。然而,如果我们要针对不可修改的第三方类扩展加法操作时,这种通过子类型多态的技术手段也会遇到问题。 于是,你又想到了Kotlin的扩展,我们要引出另一种叫作“特设多态”的技术了。相比更通用的参数多态,特设多态提供了“量身定制”的能力。参考它的定义,特设多态可以理解为:一个多态函数是有多个不同的实现,依赖于其实参而调用相应版本的函数。 针对以上的例子,我们完全可以采用扩展的语法来解决问题。此外,Kotlin原生支持了一种语言特性来很好地解决问题,这就是运算符重载。借助这种语法,我们可以完美地实现需求。代码如下: data class Area(val value: Double) operator fun Area.plus(that: Area): Area { return Area(this.value + that.value) } fun main(args: Array<String>) { println(Area(1.0) + Area(2.0)) // 运行结果:Area(value=3.0) } 下面我们来具体介绍下Kotlin中运算符重载的语法。相信你已经注意到了operator关键字,以及Kotlin中内置可重载的运算符plus。先来看看operator,它的作用是:将一个函数标记为重载一个操作符或者实现一个约定。 注意,这里的plus是Kotlin规定的函数名。除了重载加法,我们还可以通过重载减法(minus)、乘法(times)、除法(div)、取余(mod)(Kotlin1.1版本开始被rem替代)等函数来实现重载运算符。此外,你可以再回忆一下第2章中遇到的一些基础语法,它们也是利用这种神奇的语言特性来实现的,如: ``` a in b // 转换为b.contains(a) f(a) // 转换为f.invoke(a) ``` 我们将会展示如何利用Kotlin运算符重载的语法,来简化经典的设计模式。