kotlin 抽象
重点 (Top highlight)
*Terms and Conditions apply
*条款和条件适用
Warning: this blog post covers a Kotlin experimental feature, subject to change. This article was written using Kotlin 1.3.50.
警告:此博客文章涵盖了Kotlin实验功能,可能会随时更改。 本文是使用Kotlin 1.3.50编写的。
Type safety prevents us from making errors or having to debug them later. For Android resource types, like String
, Font
or Animation
resources, we can use androidx.annotations
like @StringRes
, @FontRes
and Lint enforces that we pass a parameter of the correct type:
类型安全性防止我们犯错误或以后不得不调试它们。 对于Android资源类型(例如String
, Font
或Animation
资源),我们可以使用androidx.annotations
例如@StringRes
, @FontRes
和Lint强制我们传递正确类型的参数:
fun myStringResUsage(@StringRes string: Int){ }// Error: expected resource of type String
myStringResUsage(1)
If our id is not an Android resource, but rather an id for a domain object like a Doggo
or a Cat
, then differentiating between these two Int
ids can’t be easily done. To achieve type-safety which encodes that the id of a dog is not the same as a cat’s, you’d have to wrap your id in a class. The downside of this is that you pay a performance cost, as a new object needs to be instantiated when actually, all you need is a primitive.
如果我们的ID不是Android资源,而是域对象(如Doggo
或Cat
ID,那么Doggo
区分这两个Int
ID。 为了实现将狗的ID与猫的ID编码不同的类型安全,您必须将ID包装在一个类中。 这样做的缺点是您要付出性能成本,因为实际上需要实例化一个新对象时,您所需要的只是一个原语。
Kotlin inline classes allow you to create these wrapper types without the performance cost. This is an experimental feature added in Kotlin 1.3. Inline classes must have exactly one property. At compile time, inline classes instances are replaced with their underlying property (are unboxed) where possible, reducing the performance cost of a regular wrapper class. This matters even more for cases where the wrapped object is a primitive type as the compiler already optimizes them. So wrapping a primitive type in an inline class leads, where possible, to the value represented as primitive values at runtime.
Kotlin 内联类使您可以创建这些包装器类型, 而不会降低性能。 这是Kotlin 1.3中添加的实验功能。 内联类必须仅具有一个属性。 在编译时,如果可能,将内联类实例替换为其内在属性(取消装箱),从而降低常规包装器类的性能成本。 对于包装对象是原始类型的情况,这尤其重要,因为编译器已经对其进行了优化。 因此,在可能的情况下,将原始类型包装在内联类中会导致在运行时表示为原始值的值。
inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )// usage
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }
内联 (Get inline)
The sole role of an inline class is to be a wrapper around a type so Kotlin enforces a number of restrictions:
内联类的唯一作用是成为类型的包装,因此Kotlin实施了许多限制:
- No more than one parameter (no limitation on the type) 参数不超过一个(类型不受限制)
No backing fields
没有背景
- No init blocks 没有初始化块
- No extending classes 没有扩展课程
But, inline classes can:
但是,内联类可以:
- Inherit from interfaces 从接口继承
- Have properties and functions 具有属性和功能
interface Id
inline class DoggoId(val id: Long) : Id {
val stringId
get() = id.toString() fun isValid()= id > 0L
}
⚠️ Warning: Typealias might seem similar to inline classes but, type aliases just provide an alternate name for existing types, while inline classes create a new type.
Warning️警告: Typealias看起来与内联类相似,但是类型别名只是为现有类型提供了备用名称,而内联类会创建新类型。
代表性-是否包装? (Representation — wrapped or not?)
Since the biggest advantage of inline classes over manual wrapper classes is the memory allocation impact, it’s important to remember that this heavily depends on where and how you’re using inline classes. The general rule is that the parameter will be wrapped (boxed) if the inline class is used as another type.
由于内联类相对于手动包装类的最大优点是对内存分配的影响,因此请务必记住,这在很大程度上取决于您在何处以及如何使用内联类。 一般规则是,如果将内联类用作其他类型,则将包装(装箱)参数。
A parameter is boxed when used as another type
用作其他类型的参数时将其装箱
For example, if you’re using it when an Object or Any is expected such as in collections, arrays or as nullable objects. Depending on how you’re checking two inline classes for structural equality, one or none of them will be boxed:
例如,如果您在需要对象或任何对象(例如在集合,数组或可为空的对象中)时使用它。 根据您如何检查两个内联类的结构相等性,将其中一个或一个都不装箱:
val doggo1 = DoggoId(1L)val doggo2 = DoggoId(2L)
doggo1 == doggo2
— neither doggo1 and doggo2 are boxed
doggo1 == doggo2
装箱都没有doggo1和doggo2
doggo1.equals(doggo2)
— doggo1 is used as a primitive but doggo2 is boxed
doggo1.equals(doggo2)
— doggo1用作基本类型,但将boxgo2装箱
引擎盖下 (Under the hood)
Let’s take a simple inline class:
让我们来看一个简单的内联类:
interface Id
inline class DoggoId(val id: Long) : Id
Let’s see what the decompiled Java programming language code looks like step by step and what implications they have for using inline classes. You can find the full decompiled code here.
让我们逐步看一下反编译后的Java编程语言代码,以及它们对使用内联类的影响。 您可以在此处找到完整的反编译代码。
内幕-构造函数 (Under the hood — constructors)
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
// $FF: synthetic method
private DoggoId(long id) {
this.id = id;
}
public static long constructor_impl/* $FF was: constructor-impl*/(long id) {
return id;
}
}
The DoggoId has two constructors:
DoggoId具有两个构造函数:
A private synthetic constructor
DoggoId(long id)
私有合成构造函数
DoggoId(long id)
A public
constructor-impl
公共
constructor-impl
When creating a new instance of the object the public constructor is used:
创建对象的新实例时,将使用公共构造函数:
val myDoggoId = DoggoId(1L)// decompiled
static final long myDoggoId = DoggoId.constructor-impl(1L);
If we try to create the doggo id in Java, we’ll get an error:
如果尝试在Java中创建Doggo ID,则会收到错误消息:
DoggoId u = new DoggoId(1L);
// Error: DoggoId() in DoggoId cannot be applied to (long)
You can’t instantiate an inline class from Java
您无法从Java实例化内联类
The parameterized constructor is a private and the second constructor contains a -
in the name — an invalid character in Java. This means that inline classes can’t be instantiated from Java.
参数化的构造函数是私有的,第二个构造函数在名称中包含-
名称(在Java中为无效字符)。 这意味着不能从Java实例化内联类。
内幕-参数用法 (Under the hood — parameter usage)
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
private final long id;
public final long getId() {
return this.id;
}
// $FF: synthetic method
@NotNull
public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
return new DoggoId(v);
}
}
The id is exposed in two ways:
标识以两种方式公开:
As a primitive, via a
getId
作为原始,通过
getId
As an object via a
box_impl
method that creates a new instance of the DoggoId通过
box_impl
方法创建一个DoggoId新实例的对象
When the inline class is used where a primitive could have been used, then the Kotlin compiler will know this and will directly use the primitive:
当在可以使用原语的地方使用内联类时,Kotlin编译器将知道这一点,并将直接使用原语:
fun walkDog(doggoId: DoggoId) {}// decompiled Java code
public final void walkDog_Mu_n4VY(long doggoId) { }
When an object is expected, then the Kotlin compiler will use the boxed version of our primitive, leading to a new object creation every time.
当需要一个对象时,Kotlin编译器将使用原语的盒装版本,每次都将创建一个新的对象。
When an object is expected, then the Kotlin compiler will use the boxed version of our primitive, leading to a new object creation every time. For example:
如果需要一个对象,那么Kotlin编译器将使用原始的盒装版本, 每次都会创建一个新的对象。 例如:
可空对象 (Nullable objects)
fun pet(doggoId: DoggoId?) {}// decompiled Java code
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}
Because only objects can be nullable, then the boxed implementation is used.
因为只有对象可以为空,所以使用盒装实现。
馆藏 (Collections)
val doggos = listOf(myDoggoId)// decompiled Java code
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));
The signature of CollectionsKt.listOf
is:
CollectionsKt.listOf
的签名为:
fun listOf(element: T): List
Because this method expects an object, then the Kotlin compiler boxes our primitive, making sure that an object is used.
因为此方法需要一个对象,所以Kotlin编译器会将我们的原语装箱,以确保使用了对象。
基类 (Base classes)
fun handleId(id: Id) {}fun myInterfaceUsage() {
handleId(myDoggoId)
}// decompiled Java code
public static final void myInterfaceUsage() {
handleId(DoggoId.box-impl(myDoggoId));
}
Because here we’re expecting a super type, then the boxed implementation is used.
因为在这里我们期望一个超级类型,所以使用了盒装实现。
内幕-平等检查 (Under the hood — Equality checks)
The Kotlin compiler tries to use the unboxed parameter wherever possible. To do this, inline classes have 3 different implementations for equality: an override of equals and 2 generated methods:
Kotlin编译器尝试尽可能使用unboxed参数。 为此,内联类具有3种不同的实现方式:覆盖equals和2个生成的方法:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
public static boolean equals_impl/* $FF was: equals-impl*/(long var0, @Nullable Object var2) {
if (var2 instanceof DoggoId) {
long var3 = ((DoggoId)var2).unbox-impl();
if (var0 == var3) {
return true;
}
}
return false;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(long p1, long p2) {
return p1 == p2;
}
public boolean equals(Object var1) {
return equals-impl(this.id, var1);
}
}
doggo1.equals(doggo2)
doggo1.equals(doggo2)
The equals method calls a generated method: equals_impl(long, Object)
. Since equals expects an object, the doggo2 value will be boxed, but doggo1 will be used as a primitive:
equals方法调用一个生成的方法: equals_impl(long, Object)
。 由于equals期望有一个对象,所以将对doggo2的值进行装箱,但是将doggo1用作原语:
DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
doggo1 == doggo2
doggo1 == doggo2
Using ==
generates:
使用==
生成:
DoggoId.equals-impl0(doggo1, doggo2)
So in the ==
case, the primitive will be used for both doggo1 and doggo2.
因此,在==
情况下,该原语将用于doggo1和doggo2。
doggo1 == 1L
doggo1 == 1L
If Kotlin is able to determine that doggo1 is actually a long, then you’d expect that this equality check to work. But, because we are using inline classes for their type safety, then, the first thing the compiler will do is to check whether the type of the two objects we’re trying to compare is the same. And since it’s not, we’ll get a compiler error: Operator ==
can’t be applied to long and DoggoId. In the end, for the compiler, this is just as if we’d said cat1 == doggo1
, which is definitely not true.
如果Kotlin能够确定doggo1实际上很长,那么您可以期望这种相等性检查有效。 但是,由于我们将内联类用于类型安全,因此,编译器将要做的第一件事是检查我们尝试比较的两个对象的类型是否相同。 既然不是,我们将得到一个编译器错误: Operator ==
不能应用于long和DoggoId 。 最后,对于编译器,这就像我们说cat1 == doggo1
,这绝对不是事实。
doggo1.equals(1L)
doggo1.equals(1L)
This equality check does compile because the Kotlin compiler uses the equals implementation that expects a long and an Object. But, because the first thing this method does is to check the type of the Object, this equality check will be false, as the Object is not a DoggoId.
这种相等性检查的确可以编译,因为Kotlin编译器使用的equals实现需要一个long和一个Object。 但是,由于此方法的第一件事是检查Object的类型,因此该相等性检查将为false,因为Object不是DoggoId。
使用原始和内联类参数覆盖函数 (Overriding functions with primitive and inline class parameters)
The Kotlin compiler allows defining functions with both primitive and non-nullable inline class as parameters:
Kotlin编译器允许使用原始和不可空的内联类作为参数来定义函数:
fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}// decompiled Java code
public static final void pet(long id) { }
public final void pet_Mu_n4VY(long doggoId) { }
In the decompiled code, we can see that for both functions, the primitive is used.
在反编译的代码中,我们可以看到两个函数都使用了原语。
To allow this functionality, the Kotlin compiler will mangle the name of the function taking the inline class as a parameter.
为了允许此功能,Kotlin编译器将内联类作为参数来处理函数的名称。
在Java中使用内联类 (Using inline classes in Java)
We already saw that we can’t instantiate an inline class in Java. How about using them?
我们已经看到我们无法在Java中实例化内联类。 如何使用它们?
✅将内联类传递给Java函数 (✅ Passing inline classes to Java functions)
We can pass them as parameters to be used as objects and we can get the property they wrap.
我们可以将它们作为参数传递给对象,然后获得它们包装的属性。
void myJavaMethod(DoggoId doggoId){
long id = doggoId.getId();
}
✅在Java函数中使用内联类实例 (✅ Using inline classes instances in Java functions)
If we have inline classes instances defined as top level objects, we can get a reference to them in Java as primitives, as
如果我们将内联类实例定义为顶级对象,则可以在Java中获取它们作为原始的引用,如下所示:
// Kotlin declaration
val doggo1 = DoggoId(1L)// Java usage
long myDoggoId = GoodDoggosKt.getU1();
✅&❌调用以内联类作为参数的Kotlin函数 (✅ & ❌Calling Kotlin functions that have inline classes as parameters)
If we have a Java function that receives an inline class parameter and we want to call a Kotlin function that accepts an inline class, we get a compile error:
如果我们有一个接收内联类参数的Java函数,并且想调用一个接受内联类的Kotlin函数,则会收到编译错误:
fun pet(doggoId: DoggoId) {}// Java
void petInJava(doggoId: DoggoId){pet(doggoId)
// compile error: pet(long) cannot be applied to pet(DoggoId)
}
For Java, DoggoId
is a new type, but the compiler generates pet(long)
and pet(DoggoId)
doesn’t exist.
对于Java, DoggoId
是一种新类型,但是编译器会生成pet(long)
而pet(DoggoId)
不存在。
But, we are able to pass the underlying type:
但是,我们能够传递基础类型:
fun pet(doggoId: DoggoId) {}// Java
void petInJava(doggoId: DoggoId){
pet(doggoId.getId)
}
If in the same class we overrode a function with the inline class and the underlying type, when we call the function from Java we get an error, as the compiler can’t tell which function we actually wanted to call:
如果在同一个类中,我们用内联类和基础类型覆盖了一个函数,那么当我们从Java调用该函数时,会得到一个错误,因为编译器无法告知我们实际上要调用哪个函数:
fun pet(doggoId: Long) {}fun pet(doggoId: DoggoId) {}// Java
TestInlineKt.pet(1L);Error: Ambiguous method call. Both pet(long) and pet(long) match
内联或不内联 (To inline or not to inline)
Type safety helps us to write more robust code but historically could have performance implications. Inline classes offer the best of both worlds, type safety without the costs–should you always use them?
类型安全性可以帮助我们编写更健壮的代码,但是从历史上看可能会对性能产生影响。 内联类提供了两全其美的方式, 无需花钱就可以保证输入的安全性-您应该始终使用它们吗?
Inline classes bring a series of restrictions that ensure that the object you create play a single role: to be a wrapper. This means that in the future, developers unfamiliar with the code won’t be able to wrongly increase the complexity of the class, by adding other parameters to the constructor, as you could with a data class.
内联类带来了一系列限制,以确保您创建的对象发挥单一作用:成为包装器。 这意味着将来,不熟悉代码的开发人员将无法像在数据类中那样通过向构造函数添加其他参数来错误地增加类的复杂性。
Performance wise we’ve seen that the Kotlin compiler tries its best to use the underlying type whenever possible, but still ends up creating new objects in many cases.
在性能方面,我们已经看到Kotlin编译器尽其所能尽力使用基础类型,但在许多情况下仍会最终创建新对象。
Usage from Java comes with several caveats so, if you haven’t migrated fully to Kotlin yet, you might end up in cases where you can’t use inline classes.
Java的用法带有一些警告,因此,如果您还没有完全迁移到Kotlin,则可能会遇到无法使用内联类的情况。
Finally, this is an experimental feature still, bringing uncertainty on whether its implementation will stay the same once it’s stable and whether it will actually graduate to stable.
最后,这仍然是一项实验性功能,带来了不确定性,一旦实现,它的实现是否会保持不变,以及它是否会逐渐稳定。
So now that you understand inline classes’ benefits and restrictions, you can make an informed decision about if and when to use them.
因此,既然您了解了内联类的好处和限制,则可以就是否以及何时使用它们做出明智的决定。
翻译自: https://medium.com/androiddevelopers/zero-cost-abstractions-in-kotlin-19b953f3a936
kotlin 抽象