Kotlin的扩展属性与扩展方法及其实战应用
前言
扩展函数真的是现代开发语言的神器,不管是之前用的 Kotlin 还是 Dart 都可以使用扩展函数,在不改变原本类的前提下添加一些方法和变量,特别的方便。
前段时间在写 Flutter 项目,定义了很多扩展函数,有各种定义:
当时还不觉得,最近又开始写 Kotlin 项目突然觉得,好像自己一直都是用 Kotlin 的扩展方法,不带括号的写法自己还真没怎么用,一时间反应不过来赶紧理清一下头绪,属实是被 Dart 给教育了一波。
关于 KT 的扩展大家可能都知道可以扩展对象的方法,也能扩展对象的属性,如果从一个简单的外观来区分,我们简单的区分为带括号的和不带括号的(扩展方法和扩展属性)。
下面一起复习一下 KT 的扩展函数的扩展方法和扩展属性吧。
一、跟着Dart学Kotlin扩展函数
在 Dart 中我们可以定义扩展方法 dp() 并且可以传递参数。同时我们也可以定义 ap 扩展属性,给 int 加入一个属性,并且我们指定了这个属性是只读的,代码如下:
csharp
extension IntExtension on int {
double get ap {
return ScreenUtil.getInstance().getAdapterSize(toDouble());
}
double dp() {
return ScreenUtil.getInstance().getAdapterSize(this);
}
}
他们的功能是一样的,但是使用起来就不同了,走的逻辑分支也不同,一个是调用方法,一个是调用属性。
其实我们扩展属性也是可以分别设置读写设置的:
extension IdNameEntityExtension on IdNameEntity { String get propertyId { return id ?? ""; }
set propertyId(String id) { this.id = id; }
String get propertyName { return name!; }
set propertyName(String value) { name = value; } }
比如我们对一个 IDName 的实体做一个扩展属性,那么可以设置它的可读可写,虽然是一个简单的例子看起来很傻,但是这个简单的示例确实最基本的属性扩展可读写的权限控制演示。
同样的代码我们也可以用 Kotlin 实现:
kotlin
val Int.ap: Int
get() = CommUtils.dip2px(this)
fun Int.sp(): Int {
return CommUtils.dip2px(this)
}
同样的设置 sp() 为扩展方法,ap 为可读的扩展属性。只是与 Dart 的扩展不同的是标识不同,扩展函数用 fun 标识函数,扩展属性用 val 或 var。
对应自定义对象的扩展, KT 也是类似的做法:
kotlin
var IDNameEntity.propertyId: String
get() = this.id
set(value:String) {
this.id = value
}
val IDNameEntity.propertyName :String
get() = this.name
如果只读的,我们用下面的方法 val 标识函数,使用 get 返回对应的值,如果是可读可写的就需要用 var 标识并且设置 get 和 set 标识并处理对应的逻辑。
测试代码:
ini
val idNameEntity = IDNameEntity("25", "关羽")
val propertyName = idNameEntity.propertyName
idNameEntity.propertyId = "30"
val propertyId = idNameEntity.propertyId
YYLogUtils.w("propertyName:$propertyName propertyId:$propertyId")
打印:
二、实践一:空间大小格式化
熟悉了扩展方法和扩展属性,我们就可以先来一个简单的空间大小格式化的处理。
一般我们获取到当前 App 占用的空间之后,我们就会把内置卡占用空间和外置卡占用空间加起来就是 App 占用空间。
如果单位的格式不同我们就需要转换为最基本的 B 单位相加,然后再处理格式化到指定的空间展示,我们可以把这一过程用扩展方法和扩展属性来集中管理。
我们参考大佬的文章【传送门】实现。
我们先定义一个普通的类,用于传入基本的 B 单位:
kotlin
class DataUnitSize constructor(val rawBytes: Long) {
fun toDouble(unit: DataUnit): Double = convertDataUnit(rawBytes.toDouble(), DataUnit.BYTES, unit)
fun toLong(unit: DataUnit): Long = convertDataUnit(rawBytes, DataUnit.BYTES, unit)
//默认的 toString 输出最基本的 B 单位
override fun toString(): String = String.format("%dB", rawBytes)
fun toString(unit: DataUnit, decimals: Int = 2): String {
require(decimals >= 0) { "小数点必须大于0, 现在的小数点是$decimals" }
val number = toDouble(unit)
if (number.isInfinite()) {
return number.toString()
}
val newDecimals = decimals.coerceAtMost(12)
return DecimalFormat("0").run {
if (newDecimals > 0) minimumFractionDigits = newDecimals
roundingMode = RoundingMode.HALF_UP
format(number) + unit.shortName
}
}
}
我们这里简单的定义一个构造方法,用于传递最基本的 B 单位,重写和提供 toString 方法,分别打印最基本的 B 单位大小,和指定格式的大小。
内部涉及到 DataUnit 单位枚举 和 convertDataUnit 单位转换的方法:
kotlin
enum class DataUnit(val shortName: String) {
BYTES("B"),
KILOBYTES("KB"),
MEGABYTES("MB"),
GIGABYTES("GB"),
TERABYTES("TB"),
}
private const val BYTES_PER_KB: Long = 1024 //KB
private const val BYTES_PER_MB = BYTES_PER_KB * 1024 //MB
private const val BYTES_PER_GB = BYTES_PER_MB * 1024 //GB
private const val BYTES_PER_TB = BYTES_PER_GB * 1024 //TB
//主要的单位转换方法之一
fun convertDataUnit(value: Long, sourceUnit: DataUnit, targetUnit: DataUnit): Long {
val valueInBytes = when (sourceUnit) {
DataUnit.BYTES -> value
DataUnit.KILOBYTES -> Math.multiplyExact(value, BYTES_PER_KB)
DataUnit.MEGABYTES -> Math.multiplyExact(value, BYTES_PER_MB)
DataUnit.GIGABYTES -> Math.multiplyExact(value, BYTES_PER_GB)
DataUnit.TERABYTES -> Math.multiplyExact(value, BYTES_PER_TB)
}
return when (targetUnit) {
DataUnit.BYTES -> valueInBytes
DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
}
}
//主要的单位转换方法之一
fun convertDataUnit(value: Double, sourceUnit: DataUnit, targetUnit: DataUnit): Double {
val valueInBytes = when (sourceUnit) {
DataUnit.BYTES -> value
DataUnit.KILOBYTES -> value * BYTES_PER_KB
DataUnit.MEGABYTES -> value * BYTES_PER_MB
DataUnit.GIGABYTES -> value * BYTES_PER_GB
DataUnit.TERABYTES -> value * BYTES_PER_TB
}
require(!valueInBytes.isNaN()) { "DataUnit value cannot be NaN." }
return when (targetUnit) {
DataUnit.BYTES -> valueInBytes
DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
}
}
有了基本的类处理,和基本的单位转换方法,我们就能定义相对于的扩展方法和扩展属性了:
kotlin
//定义常用的一系列扩展属性(只读)
val Long.b get() = this.toDataSize(DataUnit.BYTES)
val Long.kb get() = this.toDataSize(DataUnit.KILOBYTES)
val Long.mb get() = this.toDataSize(DataUnit.MEGABYTES)
val Long.gb get() = this.toDataSize(DataUnit.GIGABYTES)
val Long.tb get() = this.toDataSize(DataUnit.TERABYTES)
val Int.b get() = this.toLong().toDataSize(DataUnit.BYTES)
val Int.kb get() = this.toLong().toDataSize(DataUnit.KILOBYTES)
val Int.mb get() = this.toLong().toDataSize(DataUnit.MEGABYTES)
val Int.gb get() = this.toLong().toDataSize(DataUnit.GIGABYTES)
val Int.tb get() = this.toLong().toDataSize(DataUnit.TERABYTES)
val Double.b get() = this.toDataSize(DataUnit.BYTES)
val Double.kb get() = this.toDataSize(DataUnit.KILOBYTES)
val Double.mb get() = this.toDataSize(DataUnit.MEGABYTES)
val Double.gb get() = this.toDataSize(DataUnit.GIGABYTES)
val Double.tb get() = this.toDataSize(DataUnit.TERABYTES)
//定义的扩展方法之一,转换为 new 出来的 DataSize 对象
fun Long.toDataSize(unit: DataUnit): DataUnitSize {
return DataUnitSize(convertDataUnit(this, unit, DataUnit.BYTES))
}
//定义的扩展方法之一,转换为 new 出来的 DataSize 对象
fun Double.toDataSize(unit: DataUnit): DataUnitSize {
return DataUnitSize(convertDataUnit(this, unit, DataUnit.BYTES).roundToLong())
}
// 给自定义的 DataSize 定义扩展方法,可以自动格式化到指定的单位
fun DataUnitSize.autoFormatDataSize(): String {
val dataSize = this
return when {
dataSize.inWholeTerabytes >= 1 -> dataSize.toString(DataUnit.TERABYTES)
dataSize.inWholeGigabytes >= 1 -> dataSize.toString(DataUnit.GIGABYTES)
dataSize.inWholeMegabytes >= 1 -> dataSize.toString(DataUnit.MEGABYTES)
else -> dataSize.toString(DataUnit.KILOBYTES)
}
}
// 给自定义的 DataSize 定义扩展方法,是否满足当前单位
val DataUnitSize.inWholeBytes: Long get() = toLong(DataUnit.BYTES)
val DataUnitSize.inWholeKilobytes: Long get() = toLong(DataUnit.KILOBYTES)
val DataUnitSize.inWholeMegabytes: Long get() = toLong(DataUnit.MEGABYTES)
val DataUnitSize.inWholeGigabytes: Long get() = toLong(DataUnit.GIGABYTES)
val DataUnitSize.inWholeTerabytes: Long get() = toLong(DataUnit.TERABYTES)
使用的时候,可以手动指定格式化单位:
kotlin
val dataSize1 = 888.mb
YYLogUtils.w("格式化 B:${dataSize1.toString()}")
YYLogUtils.w("格式化 KB:${dataSize1.toString(DataUnit.KILOBYTES)}")
YYLogUtils.w("格式化 GB:${dataSize1.toString(DataUnit.GIGABYTES)}")
也可以自动转换格式化单位:
ini
val sizeInBytes = 1500000.b
val formattedSize = sizeInBytes.autoFormatDataSize()
YYLogUtils.w(formattedSize) // 输出:1.43MB
如果想要想加也可以自己实现想加,当然也可以自己实现 Comparable 接口加上 operator 操作符,有兴趣可以往下看。
ini
val dataSize1 = 888.mb
val dataSize2 = 1.gb
val plusSize = DataUnitSize(dataSize1.rawBytes + dataSize2.rawBytes).autoFormatDataSize()
YYLogUtils.w(plusSize)
打印 Log 如下:
二、实践二:汇率格式化
如果我们的需求是,做完这个任务获得100美元,做完另一个任务获得80港币,最终我需要计算一共获得多少人民币。
我们可以通过类似的方法来实现,由于上面给出了具体的实现步骤这里就直接上代码了:
kotlin
enum class CurrencyUnit(val prefix: String, val fullName: String, val exchangeRate: Double) {
CNY("¥", "CNY", 1.0),
USD("$", "USD", 8.0),
SGD("S$", "SGD", 5.0),
HKD("HK$", "HKD", 0.8),
}
//简单的转换方法
fun convertCurrency(value: Double, sourceUnit: CurrencyUnit, targetUnit: CurrencyUnit): Double {
val valueInRMB = value * sourceUnit.exchangeRate
return valueInRMB / targetUnit.exchangeRate
}
/**
使用 @JvmInline value class 优化旨在提高内存和性能效率
使用 @JvmInline value class 必须有且仅有一个属性,主构造函数的参数只能是 val,
并且类不能被继承,生成的字节码会在使用处直接嵌入,而不会引入额外的对象
如果不想使用 @JvmInline value class 用普通的class也能行
*/
@JvmInline
value class CurrencyAmount constructor(private val rawAmount: Double) : Comparable<CurrencyAmount> {
private fun toDouble(unit: CurrencyUnit): Double = convertCurrency(rawAmount, CurrencyUnit.CNY, unit)
operator fun unaryMinus(): CurrencyAmount {
return CurrencyAmount(-this.rawAmount)
}
operator fun plus(other: CurrencyAmount): CurrencyAmount {
return CurrencyAmount(this.rawAmount + other.rawAmount)
}
operator fun minus(other: CurrencyAmount): CurrencyAmount {
return this + (-other) // a - b = a + (-b)
}
operator fun times(scale: Int): CurrencyAmount {
return CurrencyAmount(this.rawAmount * scale)
}
operator fun div(scale: Int): CurrencyAmount {
return CurrencyAmount(this.rawAmount / scale)
}
operator fun times(scale: Double): CurrencyAmount {
return CurrencyAmount(this.rawAmount * scale)
}
operator fun div(scale: Double): CurrencyAmount {
return CurrencyAmount(this.rawAmount / scale)
}
override fun compareTo(other: CurrencyAmount): Int {
return this.rawAmount.compareTo(other.rawAmount)
}
override fun toString(): String = String.format("¥ %.2f", rawAmount)
fun toString(unit: CurrencyUnit, decimals: Int = 2): String {
require(decimals >= 0) { "小数点必须大于0, 现在的小数点是$decimals" }
val number = toDouble(unit)
if (number.isInfinite()) {
return number.toString()
}
val newDecimals = decimals.coerceAtMost(12)
return DecimalFormat("0").run {
if (newDecimals > 0) minimumFractionDigits = newDecimals
roundingMode = RoundingMode.HALF_UP
unit.prefix + " " + format(number)
}
}
}
//定义常用的一系列扩展属性(只读)
val Double.cny get() = CurrencyAmount(this)
val Long.cny get() = CurrencyAmount(this.toDouble())
val Int.cny get() = CurrencyAmount(this.toDouble())
val Double.usd get() = CurrencyAmount(this * CurrencyUnit.USD.exchangeRate)
val Long.usd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.USD.exchangeRate)
val Int.usd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.USD.exchangeRate)
val Double.sgd get() = CurrencyAmount(this * CurrencyUnit.SGD.exchangeRate)
val Long.sgd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.SGD.exchangeRate)
val Int.sgd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.SGD.exchangeRate)
val Double.hkd get() = CurrencyAmount(this * CurrencyUnit.HKD.exchangeRate)
val Long.hkd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.HKD.exchangeRate)
val Int.hkd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.HKD.exchangeRate)
相对而言更简单了,我们不需要想空间转换那样叠加使用,也不需要判断是否超过某一个单位,也无需自动格式化单位。
我们只需要简单的定义单位转换,并且实现简单语义化的加减乘除等操作,方便金额的计算。
如何使用?
kotlin
val money1 = 100.usd
YYLogUtils.w("格式化金额人民币:${money1.toString()}")
YYLogUtils.w("格式化金额新加坡:${money1.toString(CurrencyUnit.SGD)}")
YYLogUtils.w("格式化金额港币:${money1.toString(CurrencyUnit.HKD)}")
val money2 = 100.50.hkd
YYLogUtils.w("100美元加100.5港币等于:${(money1+money2).toString()}")
打印如下:
当然这是试验性质,实际的汇率是根据后端返回实时变动的,切不可直接生搬硬套。
后记
除此之外常规的扩展方法更是应用广泛,比如RV的扩展,FindView的扩展,SP的扩展,DataStore的扩展,Intent的扩展,Dialog扩展等等层出不穷。
扩展属性对于一些简单的逻辑也更方便,更简洁。一般我们需要一个值可以直接用扩展属性,如果要进行逻辑运算那么可以用扩展方法,两者结合使用可以实现很多骚功能。
本文的几个例子只是使用了基本的扩展属性和扩展方法,大家有兴趣可以自行练习一下。本文的代码仅用于学习交流,如果要用于实战我推荐你最好自己参照着实现对应的逻辑。
除此之外,我们还能实现扩展的高阶函数,或实现DSL的方式,都是一些相对"高级"的用法,有兴趣的可以看我这篇文章【扩展高阶函数与DSL】。
关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目(虽然是两年前的老项目了)我有时间都会持续更新。
如果本文有错漏的地方或者有其他更好的方案,希望同学们可以在评论区指出或交流。
好了,如果感觉本文对你有一点点的启发与帮助,还望你能点赞
支持一下,你的支持是我最大的动力啦。
Ok,这一期就此完结。