An in depth exploration of Kotlin extension function and its implementation mechanism

  • 2021-09-11 21:32:25
  • OfStack

Preface

In 2017, Google IO Conference announced the use of Kotlin as the official development language of Android. Compared with the typical JAVA language of face objects, Kotlin is a new functional programming language, and some people call it Swift language of Android platform.

Let's first look at the comparison between Java and Kotiln to achieve the same function:


// JAVA , 20 Multiple lines of code, full of findViewById Nonsense code such as, type conversion, and anonymous inner classes 

public class MainJavaActivity extends Activity {
 @Override
 public void onCreate(@Nullable Bundle savedInstanceState) { 
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);

 TextView label = (TextView) findViewById(R.id.label);
 Button btn = (Button) findViewById(R.id.btn);

 label.setText("hello");
 label.setOnClickListener(new View.OnClickListener() {  
  @Override
  public void onClick(View v) {
  Log.d("Glen","onClick TextView");
  }
 });
 btn.setOnClickListener(new View.OnClickListener(){  
  @Override
  public void onClick(View v) {
  Log.d("Glen","onClick Button");
  }
 });
 }
}

Let's look at Kotlin again


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}

Realize these need to use Kotlin extension function and high-order function, this paper mainly introduces 1 lower extension function.

What is an extension function?

Extending the number of functions means adding a new behavior to a class, even if we don't have access to the class code. This is a method that extends on classes that lack useful functions. Kotlin can do things that are interesting for us, but these Java can't.

In Java, many tool classes with static methods are usually implemented. One advantage of extension functions in Kotlin is that we don't need to pass in the whole object as parameters when calling methods, it behaves as if it belongs to this class, and we can use this keywords and call all public methods.

1. Kotlin Extension Functions and Extension Properties (Kotlin Extensions)

Kotlin can extend the new functionality of a class without inheriting the class, or use a design pattern like "Decorator (Decorator)" for any class. These are achieved through a special declaration called "Extension (extensions)". Kotlin extension declarations support both extension functions and extension attributes. This article mainly discusses extension functions, but the implementation mechanism of extension attributes is similar.

The extension function declaration is very simple, its keyword is., and we need a "recipient type (recievier type)" as its prefix. As class MutableList < Int > For example, now extend an swap method for it, as follows:


fun MutableList<Int>.swap(index1:Int,index2:Int) {
 val tmp = this[index1]
 this[index1] = this[index2]
 this[index2] = tmp
}

MutableList < T > It is the List container class in the basic library collection provided by kotlin, which is regarded as "recipient type" in the declaration. As the declaration keyword, swap is the extension function name, and the rest is no different from Kotlin declaring a common function.

In addition, the this syntax of Kotlin is more flexible than JAVA, where this in the extension function body represents the recipient type object.

If we want to call this extension function, we can do this:


fun use(){
 val list = mutableListOf(1,2,3)
 list.swap(1,2)
}

2. How is the Kotlin extension function implemented

Extension function calls look as natural and easy to use as native method 1, but will such a method bring performance constraints? It is necessary to explore 1 under Kotlin is how to achieve the spread function, direct analysis of Kotlin source code is quite difficult, but also Android Studio provides a number of tools, we can through Kotlin ByteCode instruction, view Kotlin language conversion bytecode file, still with MutableList < Int > For example, swap, the file after conversion to bytecode is as follows:


// ================com/example/glensun/demo/extension/MutableListDemoKt.class =================
// class version 50.0 (50)
// access flags 0x31

public final class com/example/glensun/demo/extension/MutableListDemoKt { 

 // access flags 0x19
 // signature (Ljava/util/List<Ljava/lang/Integer;>;II)V
 // declaration: void swap(java.util.List<java.lang.Integer>, int, int)
 public final static swap(Ljava/util/List;II)V
 @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
 L0
 ALOAD 0
 LDC "$receiver"
 INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
 L1
 LINENUMBER 8 L1
 ALOAD 0
 ILOAD 1
 INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
 CHECKCAST java/lang/Number
 INVOKEVIRTUAL java/lang/Number.intValue ()I
 ISTORE 3
 L2
 LINENUMBER 9 L2
 ALOAD 0
 ILOAD 1
 ALOAD 0
 ILOAD 2
 INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object;
 INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
 POP
 L3
 LINENUMBER 10 L3
 ALOAD 0
 ILOAD 2
 ILOAD 3
 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
 INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object;
 POP
 L4
 LINENUMBER 11 L4
 RETURN
 L5
 LOCALVARIABLE tmp I L2 L5 3
 LOCALVARIABLE $receiver Ljava/util/List; L0 L5 0
 LOCALVARIABLE index1 I L0 L5 1
 LOCALVARIABLE index2 I L0 L5 2
 MAXSTACK = 4
 MAXLOCALS = 4

 @Lkotlin/Metadata;(mv={1, 1, 7}, bv={1, 0, 2}, k=2, d1={"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\u0008\n\u0002\u0008\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\u0008\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003\u00a8\u0006\u0006"}, d2={"swap", "", "", "", "index1", "index2", "production sources for module app"}) 
// compiled from: MutableListDemo.kt

}
// ================META-INF/production sources for module app.kotlin_module =================

The bytecode here is quite intuitive. What is even more surprising is that Android Studio also has the ability to convert bytecode into JAVA files. Click the Decompile button above to get the following JAVA code:


import java.util.List;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
 mv = {1, 1, 7},
 bv = {1, 0, 2},
 k = 2,
 d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003 ( \u0006\u0006"},
 d2 = {"swap", "", "", "", "index1", "index2", "production sources for module app"}
)

public final class MutableListDemoKt { 
 public static final void swap(@NotNull List $receiver, int index1, int index2) {
  Intrinsics.checkParameterIsNotNull($receiver, "$receiver");  
 int tmp = ((Number)$receiver.get(index1)).intValue();
  $receiver.set(index1, $receiver.get(index2));
  $receiver.set(index2, Integer.valueOf(tmp));
 }
}

From the analysis of the JAVA file, the implementation of the extension function is very simple. It does not modify the members of the recipient type, but is only implemented by static methods. In this way, although we don't have to worry about the extra performance consumption caused by extension functions, it will not bring performance optimization.

3. More complicated situations

Let's discuss one more special case.

3.1 When inheritance occurs, because the extension function is a static method in nature, it will execute the call strictly according to the parameter type, and will not give priority to or actively execute the method of the parent class, as shown in the following example:


open class A
class B:A()
fun A.foo() = "a"
fun B.foo() = "b"
fun printFoo(a:A){
 println(a.foo())
}
println(B())

The output of the above example is a, because the extension function's input type is A, and it will execute the function call exactly according to the input type.

3.2 If the extension function conflicts with an existing class member, kotlin will use the class member by default. This 1-step selection is processed at compile time, and the generated bytecode will be the method that calls the class member, as shown in the following example:


class C{ 
 fun foo() {println("Member")}
}
fun C.foo() {println("Extension")}
println(C().foo())

The above example will output Member. Kotlin does not allow extension of an existing member, and the reason is understandable. We do not want extension functions to be a vulnerability for calling 3-party sdk, but if you try to create extension functions by overloading, this is feasible.

3.3 Kotlin strictly distinguishes between possible null and non-null input parameter types, and is also applied to extension functions. In order to declare a possible null receiver type, you can refer to the following example:


fun <T> MutableList<T>?.swap(index1:Int,index2:Int){
 if(this == null){
  println(null)
  return
 }

 val tmp = this[index1] 
 this[index1] = this[index2] 
 this[index2] = tmp
}

3.4 We sometimes want to be able to add extension functions like "static functions" of JAVA, which need to be implemented with "companion objects (Companion Object)", as follows:


class D{
 companion object{
  val m = 1
 }
}

fun D.Companion.foo(){
 println("$m in extension")
}

D.foo()

The above example will output 1 in extension. Note that when calling the extension function foo, you don't need an instance of the class D, which is similar to the static method of JAVA.

3.5 If we pay attention to the previous example, we will find that the syntax of this of kotlin is different from that of JAVA, and its scope of use is more flexible. Take the extension function as an example. When calling this in the extension function, it refers to an instance of the recipient type. If this extension function is declared in a class, how can we get an instance of the class through this? You can refer to the following example:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
0

The this specified syntax of kotlin is used here, the keyword is @, followed by the specified type, and the output of the above example is


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
1

4. Extend the scope of the function

1 Generally speaking, we are used to defining extension functions directly within packages, such as:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
2

In this way, the extension function can be called directly in the same package. If we need to call the extension function across packages, we need to specify it through import. Taking the above example as an example, we can specify this extension function through import com. example. extension. swap, or we can indicate all extension functions introduced into the package through import com. example. extension. *. Thanks to the automatic association ability of Android Studio, we usually don't need to actively input import instructions.

Sometimes, we will define extension functions inside the class, for example:


class G {
 fun Int.foo(){
  println("foo in Class G")
 }
}

Here Int. foo () is an extension function defined inside the class G, In this extension function, we directly use the Int type as the recipient type, because we define the extension function inside the class. Even if we set the access permission to public, it can only be accessed in the class or its subclasses. If we set the access permission to private, then the extension function cannot be accessed in the subclasses.

5. Practical application of spread function

5.1 Utils tool class

In JAVA, we are used to naming the tool class * Utils, such as FileUtils, StringUtils, etc. The famous java. util. Collections is also implemented in this way. When you call these methods, you always feel that these class names are in the way, such as this:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
4

Through static references, you can make the situation look better by one point, such as this:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
5

However, there is no IDE's automatic association hint, and the body of the method call is not clear. It would be nice if it could be made as follows:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
6

However, list is the default basic class of JAVA. In JAVA language, if inheritance is not used, it is definitely impossible to do this, but in Kotlin, it can be realized with the help of extension functions.

5.2 Android View Glue Code

Back to the initial example, for Android development, the method 1 of findViewById () is no stranger. In order to obtain an View object, we must first call findViewById () and then perform type conversion. This meaningless glue code makes Activity or Fragment look bloated, for example:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
7

We consider using extension functions combined with generics to avoid frequent type conversion. The extension functions are defined as follows:


//kotlin
fun <T : View> Activity.find(@IdRes id: Int): T { 
 return findViewById(id) as T
}

Called, as follows:


// Kotlin, There is no redundancy findViewById , we can directly to the resources id It does not need the declaration of anonymous inner class, pays more attention to the implementation of function itself, and abandons the complex format 
class MainKotlinActivity:Activity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)

 R.id.label.setText("hello")
 R.id.label.onClick { Log.d("Glen","onClick TextView") }
 R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}
9

However, we still need to get label, btn, which are meaningless intermediate variables. If extended on Int class, we can directly operate on R.id.*, which is more direct. Combined with higher-order functions, the function is defined as follows:


//Kotlin
fun Int.setText(str:String){
 val label = find<TextView>(this).apply {
  text = str
 }
}

fun Int.onClick(click: ()->Unit){
 val tmp = find<View>(this).apply {
  setOnClickListener{
   click()
  }
 }
}

We can call this:


//Kotlin
R.id.label.setText("hello")
R.id.label.onClick { Log.d("Glen","onClick TextView") }
R.id.btn.onClick { Log.d("Glen","onClick Button") }

Usually these extension functions can be put into the base class. According to the scope knowledge of extension functions, we can call these methods in all subclasses, so Activity of kotlin can be written as:


// Kotlin
class MainKotlinActivity:KotlinBaseActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  R.id.label.setText("hello")
  R.id.label.onClick { Log.d("Glen","onClick TextView") }
  R.id.btn.onClick { Log.d("Glen","onClick Button") }
 }
}

From the original JAVA redundant more than 20 lines of code, it only needs 3 lines of code, and the code is more readable and intuitive, which is the powerful power of functional programming language Kotlin.

Summarize


Related articles: