2017-08-30 | translate

[翻译]kotlin-hidden-cost-part-3

https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4

Delegated properties

delegated property 指的是一个 property 的 getter 或可选的 setter 是由一个额外的对象实现的,这个对象被称为 delegate。我们可以使用这个特性复用自定义的属性实现。

1
2
3
class Example {
var p: String by Delegate()
}

delegate 对象需要实现 operatorgetValue()函数和getValue()函数提供读写操作。函数执行时收到 所代理对象实例属性相关的 metadata (比如 name)。

delegate property 背后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Example {
@NotNull
private final Delegate p$delegate = new Delegate();
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))};

@NotNull
public final String getP() {
return this.p$delegate.getValue(this, $$delegatedProperties[0]);
}

public final void setP(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}

类中加入了一些静态属性元数据,delegate 在构造函数初始化,读写属性时调用 delegate 的方法。

Delegate instances

上面的例子中,我们实例化了一个新的 delegate 对象。当 delegate 的内部实现是有状态的时需要这步,比如下面这个例子,一个缓存 property 计算后的值:

1
2
3
4
5
6
7
8
9
10
11
12
class StringDelegate {
private var cache: String? = null

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
var result = cache
if (result == null) {
result = someOperation()
cache = result
}
return result
}
}

当 delegate 实例需要构造函数提供额外的参数时,也需要创建新的实例:

1
2
3
class Example {
private val nameView by BindViewDelegate<TextView>(R.id.name)
}

当然也有某些场景只需一个 delegate 实例代理所有 property:delegate 是状态无关的,工作过程只与所代理对象实例和 property name 相关。这时你可以把 delegate 写成单例的,用 object 替换 class.

比如这个例子:

1
2
3
4
5
object FragmentDelegate {
operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
return thisRef.fragmentManager.findFragmentByTag(property.name)
}
}

同样,一个对象也可以扩展为 delegategetValue() and setValue() 也可以声明为扩展函数的形式。kotlin 内部还提供了一组扩展函数,允许将 Map/MutableMap 实例作为代理,使用 property name 做 key。

如果你打算在一个类内复用某个代理实例,记得在构造函数里给他初始化。

Note: Kotlin 1.1 之后,函数的局部变量也可以被 delegate ,此时可以不必在构造函数立即初始化 delegate,在函数使用前实例化即可。

使用 delegate property 引入 delegate 对象的开销,还会在类中加入一些 metadata。尽可能复用 delegate。 大量使用 delegate 时要考虑这样做是必须的吗?有没有更好地解决方式。

Generic delegates

delegate 函数还可以以泛型形式声明,用于代理不同的类型。

1
private var maxDelay: Long by SharedPreferencesDelegate<Long>()

如果代理的是原始类型,每次读写都将会产生装箱拆箱开销,即使将原始类型声明为 non-null 也没用。

对于非空原始类型,不用泛型,使用指定类型的 delegate 就可以避免装箱拆箱操作。

Standard delegates: lazy()

kotlin 提供了几个标准 delegate,like Delegates.notNull(), Delegates.observable() and lazy()。

lazy() 函数返回一个只读属性 delegate,使用 lambda 负责在初次使用时初始化。

1
2
3
private val dateFormat: DateFormat by lazy {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}

在保持可读性的同时,提供了一个简洁的方式将耗时的初始化过程延迟执行,提升性能。

需要注意,lazy() 并不是内联函数,传入的 lambda 被编译为单独的 Function,而不是内联到返回的 delegate 对象内。

lazy() 有个不起眼的 optional 参数 mode ,可以以 3 种形式返回 delegate :

1
2
3
4
5
6
7
public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

默认 mode 是 LazyThreadSafetyMode.SYNCHRONIZED,内部会进行 double-checked lock,在多线程访问时会检查锁,相对来说开销较高。

如果你确定只有单线程的访问,设置 mode 为 LazyThreadSafetyMode.NONE 可以规避访问锁引发的额外开销。

1
2
3
val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}

使用 lazy() 将耗时初始化延迟执行,指定 mode 避免锁带来的开销。

Ranges

kotlin 使用 ranges 表示有限集的 value,value 可以是任何 Comparable 类型。这个表达式创建一个实现了 ClosedRange 接口的对象,操作符是 ..

Inclusion tests

range 主要是为了使用 in!in 操作符书写 inclusion or exclusion tests。

1
2
3
if (i in 1..10) {
println(i)
}

内部实现对 非空原始类型的 range 进行了优化 (bounded byInt, Long, Byte, Short, Float, Double or Char values),上例会编译为:

1
2
3
if(1 <= i && i <= 10) {
System.out.println(i);
}

没有额外开销,也不会有对象生成。也可以在 when 语句内使用 range:

1
2
3
4
5
val message = when (statusCode) {
in 200..299 -> "OK"
in 300..399 -> "Find it somewhere else"
else -> "Oops"
}

if{...} else if{...} 功能相同,但可读性更高。

不过,当 range 实例化和实际使用位置并不在同一层级时,会有一个小的开销

1
2
3
4
5
6
private val myRange get() = 1..10
fun rangeTest(i: Int) {
if (i in myRange) {
println(i)
}
}

这导致编译后额外生成了一个 IntRange 对象:

1
2
3
4
5
6
7
8
9
private final IntRange getMyRange() {
return new IntRange(1, 10);
}

public final void rangeTest(int i) {
if(this.getMyRange().contains(i)) {
System.out.println(i);
}
}

将 getter 声明为内联也不能避免。kotlin 1.1 编译器可以考虑优化一下这里。不过至少没有装箱什么的,感谢 balabala。

不要瞎折腾,直接用,或者声明成 constants 复用。

range 也可用于其他实现了 Comparable 接口的非原始类型。

1
2
3
if (name in "Alfred".."Alicia") {
println(name)
}

像这种就没啥特殊处理了,肯定会创建一个 ClosedRange 对象:

1
2
3
4
if(RangesKt.rangeTo((Comparable)"Alfred", (Comparable)"Alicia")
.contains((Comparable)name)) {
System.out.println(name);
}

需要反复使用时考虑设置为 constant。

Iterations: for loops

整型 range 是连续的:可被遍历。可以用更简短的代码替代 java 的 for 循环。

1
2
3
for (i in 1..10) {
println(i)
}

也不会产生额外的开销:

1
2
3
4
5
6
7
8
9
int i = 1;
byte var1 = 10;
while(true) {
System.out.println(i);
if(i == var1) {
return;
}
++i;
}

向后遍历可以用 downTo() infix 函数替换 ..

1
2
3
for (i in 10 downTo 1) {
println(i)
}

这也不会产生额外的开销:

1
2
3
4
5
6
7
8
9
int i = 10;
byte var1 = 1;
while(true) {
System.out.println(i);
if(i == var1) {
return;
}
--i;
}

还有 until()

1
2
3
for (i in 0 until size) {
println(i)
}

文章完成时的 kotlin 版本对这个函数生成的代码并不完美。但 Kotlin 1.1.4 有很大的提升,现在生成的代码更高效:

1
2
3
4
int i = 0;
for(int var2 = size; i < var2; ++i) {
System.out.println(i);
}

However, other iteration variants are not as well optimized.

除了 downto() 以外另一种方式:

1
2
3
for (i in (1..10).reversed()) {
println(i)
}

不要用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
if(i > var3) {
return;
}
} else if(i < var3) {
return;
}

while(true) {
System.out.println(i);
if(i == var3) {
return;
}

i += var4;
}

产生了一个临时 IntRange 对象对应 range,然后创建一个 IntProgression 对象将第一个对象的值逆序。

任何将两个以上函数结合来创建 progression 的代码都会产生相似的结果,至少两个轻量 progression object 被生成

step() infix function(修饰函数?) 也一样,就算 step = 1

1
2
3
for (i in 1..10 step 2) {
println(i)
}

提示,生成的代码读取 IntProgressionlast 属性时会进行一个计算,确定世界边界,比如上例中,这个值是 9。

遍历就用一个表达式,别整太多中间步骤

Iterations: forEach()

用 range 的 内联扩展函数 forEach() 替代 for

1
2
3
(1..10).forEach {
println(it)
}

他不会进行优化,只是使用 Iterable 遍历:

1
2
3
4
5
6
7
8
Iterable $receiver$iv = (Iterable)(new IntRange(1, 10));
Iterator var1 = $receiver$iv.iterator();

while(var1.hasNext()) {
int element$iv = ((IntIterator)var1).nextInt();
System.out.println(element$iv);
}

这段代码比上面的性能还低,它除了创建 IntRange 对象还有一个 IntIterator 的开销。At least, this one generates primitive values.

forEach() 有额外开销,不如用 for 循环。

Iterations: collection indices

kotlin 标准库为 Collection 提供了扩展函数获取 rande 引索

1
2
3
4
val list = listOf("A", "B", "C")
for (i in list.indices) {
println(list[i])
}

这部分代码也获得了优化

1
2
3
4
5
6
List list = CollectionsKt.listOf(new String[]{"A", "B", "C"});
int i = 0;
for(int var2 = ((Collection)list).size(); i < var2; ++i) {
Object var3 = list.get(i);
System.out.println(var3);
}

没有多余的东西生成,对数组和实现 Collection 的类兼容很好。也许你想自己实现个扩展函数实现相同功能:

1
2
3
4
5
6
7
8
inline val SparseArray<*>.indices: IntRange
get() = 0 until size()

fun printValues(map: SparseArray<String>) {
for (i in map.indices) {
println(map.valueAt(i))
}
}

但你的实现可不会被编译器优化,它还没那么叼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final void printValues(@NotNull SparseArray map) {
Intrinsics.checkParameterIsNotNull(map, "map");
IntRange var10000 = RangesKt.until(0, map.size());
int i = var10000.getFirst();
int var2 = var10000.getLast();
if(i <= var2) {
while(true) {
Object $receiver$iv = map.valueAt(i);
System.out.println($receiver$iv);
if(i == var2) {
break;
}
++i;
}
}
}

我建议你就在 for 循环里用until()

1
2
3
4
5
fun printValues(map: SparseArray<String>) {
for (i in 0 until map.size()) {
println(map.valueAt(i))
}
}

如果… 你要写一个不实现 Collection 接口的 collection …最好用 for 循环写引索 range ,避免生成多余对象。


balabala over