"Kotlin Core Programming" notes: ad hoc polymorphism, operator overloading and extension functions

Different forms of polymorphism

Kotlin's extension function is actually just one of the manifestations of polymorphism.

subtype polymorphism

After inheriting from the parent class, use the methods of the parent class with instances of the subclass, for example:

Then we can use all methods of the parent classDatabaseHelper. This behavior of replacing supertype instances with subtypes is what we usually call subtype polymorphism.

class CustomerDatabaseHelper(context: Context) : SQLiteOpenHelper(context) {
    
    

    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);"
        db.execSQL(sql)
    }
}

Then we can use all methods of the parent classDatabaseHelper. This behavior of replacing supertype instances with subtypes is what we usually call subtype polymorphism.

Parametric polymorphism

After completing the creation of the database, now we need to save the customer (Customer) into the client database. You might write a method like this:

fun persist(customer: Customer) {
    
    
	db.save(customer.uniqueKey, customer)
}

As needs change, we may persist multiple types of data. It would be somewhat cumbersome to write a presist method for each type. Usually we abstract a method to handle different types of persistence. Because we use key-value pairs to store, we need to obtain the corresponding uniqueKey of different types:

interface IKey {
    
    
    val uniqueKey: String
}

class ClassA(override val uniqueKey: String) : IKey 
class ClassB(override val uniqueKey: String) : IKey 

In this way, classes A and B are already availableuniqueKey. We can rewrite persist as follows:

fun <T : IKey> persist(t: T) {
    
    
    db.save(t.uniqueKey, t)
}

The above polymorphic form can be calledparametric polymorphism. In fact, the most common form of parametric polymorphism is. Generics

Extend third-party classes

If the corresponding business classesClassA and ClassB are introduced by a third party and cannot be modified, if we want to extend them some Methods, such as converting objects into Json, would be more troublesome using the polymorphic technology introduced earlier.

Using the extended syntax supported by Kotlin, you can add methods or attributes to ClassA and ClassB, thereby solving the above problem in a different way.

fun ClassA.toJson(): String = {
    
     
	......
}

It should be noted that the implementation of extended properties and methods runs on the ClassA instance, and their definition operation does not modify the ClassA class itself. This brings us a great benefit, that is, the extended third-party class is protected from pollution, thereby avoiding some problems that may cause errors in the subclass due to modifications to the parent class.

Of course, in Java we can rely on other methods such as design patterns to solve the problem, but in comparison, relying on extension solutions is more convenient and reasonable. This is actually another method called Ad hoc polymorphism technology.

Ad hoc polymorphism and operator overloading

Maybe you don’t know much about the concept of ad hoc polymorphism. Let’s give a specific example. When you want to define a general sum method, you might write this in Kotlin:

fun <T> sum(x : T, y : T) : T = x + y 

But the compiler will report an error because instances of some typesT may not necessarily support addition operations, and if it is for some custom classes, we prefer to be able to implement their own customized " Operations on additive semantics".

If we compare what parametric polymorphism does: it provides a tool, as long as something can be "cut", use this tool to cut it. However, in reality not everything can be cut, and the materials are not necessarily the same. A more reasonable solution is that you can choose different tools to cut it according to different raw materials.

Another way of thinking, we can define a general Summable interface, and then let the class that needs to support addition operations implement its plusThat method. Like this:

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)
}

You can find that when we customize a data structure that supports the plusThat method, such as Len, there is no problem with this approach. However, this technical means of subtype polymorphism will also encounter problems if we want to extend the addition operation to an unmodifiable third-party class.

So, you think of Kotlin's extensions, which is called " Ad hoc multi-purpose State” technology. Ad hoc polymorphism can be understood as: a polymorphic function has multiple different implementations, and the corresponding version of the function is called depending on its parameters.

For the above example, we can completely use extended syntax to solve the problem. In addition, Kotlin natively supports a language feature to solve the problem well, which is operator overloading. With the help of this syntax, we can achieve our needs perfectly. The code is as follows:

data class Area(val value: Double)

operator fun Area.plus(that: Area): Area {
    
    
    return Area(this.value + that.value)
}

fun main() {
    
    
    println(Area(1.0) + Area(2.0)) // 运行结果: Area(value=3.0)
}

operatorThe purpose of the keyword is to mark a function as overloading an operator or implementing a convention.

Note that plus here is the function name specified by Kotlin. In addition to overloading addition, we can also overload subtraction (minus), multiplication (times), division (div), Use functions such as remainder (mod) (replaced by rem starting from Kotlin 1.1) to implement overloaded operators.

In addition, some basic syntax of kotlin is also implemented using operator overloading, such as:

Extension: add methods and properties to other classes

For developers, when modifying existing code, they should abide by the OO design principles in the design pattern >Opening and closing principle, however, the actual situation is not optimistic. For example, when developing Android, in order to realize a certain requirement, we introduced a third-party library. But one day the requirements changed, and the current library could not meet them, and the author of the library had no plans to upgrade for the time being. At this time, you may start to try to modify the library source code. This violates the principle of openness and closure. As requirements continue to change, problems can snowball.

A common solution in Java is to have a subclass inherit the class of a third-party library and then add new functionality to it. However, forced inheritance may violate the "Richter Substitution Principle".

Kotlin’s extended language features provide us with a more reasonable solution,by extending the new functionality of a class without inheriting the class, is a better choice in most cases, so that we can reasonably follow OO design principles.

The receiver type of the extension function (recievier type)

TakeMutableList<Int> as an example, we extend it with a exchange method, the code is as follows:

fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) {
    
     
	val tmp = this[fromlndex]
	this[fromlndex] = this[tolndex] 
	this[tolndex] = tmp
}

MutableList<T> is the container class in the Kotlin standard library Collections, here as , is the extension function name. The rest is no different from declaring a normal function in Kotlin. Listrecieviertypeexchange

Kotlin'sthis is more flexible than Java. The this in the extended function body here represents an object of the receiver type. What needs to be noted here is:Kotlin strictly distinguishes whether the receiver is nullable. If your function is nullable, you need to override an extension function of a nullable type.

We can call this function very conveniently, the code is as follows:

val list = mutableListOf(1,2,3) 
list.exchange(1,2)

Implementation mechanism of extension functions

The extension function is so convenient to use, will it have any impact on performance? Let's take the previous MutableList<Int>.exchange as an example. Its corresponding Java code is as follows:

public final class ExSampleKt {
    
    

    public static final void exchange(@NotNull List Sreceiver, int fromlndex, int tolndex) {
    
     
    	Intrinsics.checkParameterlsNotNull($receiver, "$receiver");
        int tmp = ((Number)$receiver.get(fromlndex)).intValue();
        Sreceiver.set(fromlndex, $receiver.get(tolndex));
        Sreceiver.set(tolndex, Integer.valueOf(tmp));
    }
}

It can be seen that the extension function is defined as a static method, and the characteristic of Java's static method is: independent of the class Any instance object that is not tied to a specific instance of a class and is shared by all instances of that class. In addition, static methods modified by public are essentially global methods.

Therefore, we can conclude:The extension function will not bring additional performance consumption.

Extend the scope of a function

Generally speaking, we are used to defining extension functions directly in the package. For example, in the previous exchange example, we can put it in the com.example.extension package Next:

package com.example.extension

fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) {
    
     
	val tmp = this[fromlndex]
	this[fromlndex] = this[tolndex] 
	this[tolndex] = tmp
}

We know that theexchange method can be called directly in the same package. If you need to call it in other packages, you only needimportthe corresponding method, which is similar to calling Java global static method. In addition, during actual development, we may also define extension functions in a Class for unified management.

class Extends {
    
    
	fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) {
    
     
		val tmp = this[fromlndex]
		this[fromlndex] = this[tolndex] 
		this[tolndex] = tmp
	}
}

But when the extension function is defined inside the Extends class, you will find that the previous exchange method cannot be called (the previous calling location is a>Extends outside the class).

You may wonder, is it declared asprivate a method? But even if you try to add the keyword before the exchange method, it still cannot be called (in fact, the member method in Kotlin uses publicpublic

What is the reason? With IDEA we can view its corresponding Java code. Here are the key parts:

public static final class Extends {
    
    

    public final void exchange(@NotNull List Sreceiver, int fromlndex, int tolndex) {
    
    
        Intrinsics.checkParameterlsNotNull(Sreceiver, "$receiver");
        int tmp = ((Number)Sreceiver.get(fromlndex)).intValue();
        Sreceiver.set(fromlndex, $receiver.get(tolndex));
        Sreceiver.set(tolndex, Integer.valueOf(tmp));
    }
}

We see that the exchange method no longer has the static keyword modification. Sowhen the extension method is inside a Class, we can only call it in that class and its subclasses.

Or, another solution is that we can use Kotlin’s with() function to solve it:

object Extends {
    
    
    fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
    
    
        val tmp = this[fromIndex]
        this[fromIndex] = this[toIndex]
        this[toIndex] = tmp
    }
}

fun main() {
    
    
    val list = mutableListOf(1,2,3)
    with(Extends) {
    
    
        list.exchange(1,2)
    }
}

hereExtends is defined asobject singleton, with(Extends) inside of function {} It automatically becomes the scope of the Extends instance, so the extension function can be accessed normally there. If Extends is an existing class and it is inconvenient to change it to object, you can choose to wrap the definition of the extension function in a companion object:

class Extends {
    
    
    companion object {
    
    
        fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
    
    
            val tmp = this[fromIndex]
            this[fromIndex] = this[toIndex]
            this[toIndex] = tmp
        }
    }
}

fun main() {
    
    
    val list = mutableListOf(1,2,3)
    with(Extends) {
    
    
        list.exchange(1,2)
    }
}

This can also achieve the same effect.

extended attributes

Similar to extension functions, we can also add extension attributes to a class. For example, we want to add an attribute to MutableList<Int> to determine whether the sum is an even number: sumIsEven:

val MutableList<Int>.sumlsEven: Boolean 
	get() = this.sum() % 2 == 0

Just call it like an extension function:

val list = mutableListOf(2,2,4) 
list.sumlsEven

However, extended attributes do not support default values. If you write the following, an error will be reported:

// 编译错误:扩展属性不能有初始化器
val MutableList<Int>.sumlsEven: Boolean = false 
	get() = this.sum() % 2 == 0

Why is this?

In fact, like the extension function, its essence also corresponds to the static method in Java (we will see it after decompiling it into Java code agetSumIsEven static method). Since the extension does not actually insert the member into the class, the behind-the-scenes fields have no effect on the extension properties.

Define extension functions for companion objects

In Kotlin, if you need to declare a static extension function, the developer must define it on the companion object (companion object). So we need to define a class with a companion object like this:

class Son {
    
    
	companion object {
    
    
		val age = 10
	}
}

NowSon already has a companion object in the class. If we do not want to define the extension function in Son now, but in < It is defined on the companion object of a i=3>, which can be written as follows:Son

fun Son.Companion.foo() {
    
    
	println("age = Sage")
}

In this way, we can call this extension function even if there is no instance objectSon. The syntax is similar to Java's static method:

fun main() {
    
     
	Son.foo()
}

Everything seemed to be going well, but when we wanted the third-party class library to also support this writing method, we found that not all classes in the third-party class library have companion objects, and we can only use its instances. Make the call, but this will cause a lot of unnecessary trouble.

Member methods always have higher priority than extension functions

The following classes are known:

class Son {
    
    
	fun foo() = println("son called member foo") 
}

Suppose we accidentally write an extension function with the same name forSon:

fun Son.foo() = println("son called extention foo") 

When calling, we hope to call the extension functionfoo(), but the output result is that of a member function and will not meet our expectations.

This means:When there are both an extension function with the same name and a member method of an existing class, Kotlin will use the member method of the class to override the extension method with the same name by default.

It seems unreasonable, and it can easily lead to some questions: I defined a new method, why is the old method still called?

But think about it from another angle. When developing with multiple people, if everyone extends the Sonmethodfoo, is it easy to cause problems? Confused. It's even a disaster for third-party libraries: we changed a method that shouldn't have been changed. So when using it, we must pay attention to: The priority of class member methods with the same name is always higher than that of extension functions.

Class instances and receiver instances

When this is called in an extension function, it refers to an instance of the receiver type. So if this extension function is declared inside a object, how do we get the instance of the through this? Refer to the following code:object

class Son {
    
    
    fun foo() {
    
    
        println("foo in Class Son")
    }
}
object Parent {
    
    
    fun foo() {
    
    
        println("foo in Class Parent")
    }
    fun Son.foo2() {
    
    
        this.foo()
        this@Parent.foo()
    }
}
fun main() {
    
     
    val son = Son()
    with(Parent) {
    
    
        son.foo2() 
    } 
}

Here we can usethis@类名 to forcefully specify the calling this.

Extension functions in the standard library: run, let, also, takeIf

There are some very practical extension functions in the Kotlin standard library. In addition to the apply and with functions we have come across before, let’s learn more about them. Next let, run, also, takeIf.

Let’s take a look at therun method first. It is implemented using extensions and is defined as follows:

public inline fun <T, R> T.run(block: T.() -> R): R {
    
     
    return block()
}

Simply put, run is a universal extension function of any typeT, and the return type executed in run is R's extension functionblock, and finally returns the result of the extension function.

In therun function we have a separate scope in which we can define a new variable, and its scope only exists inrunIn function.

fun testFoo() {
    
    
    val nickName = "Prefert"
    run {
    
    
        val nickName = "YarenTang"
        println(nickName) // YarenTang
    }
    println(nickName) // Prefert
}

The range function itself doesn't seem very useful, but one nice thing about ranges is that it returns the last object in the range.

For example, there is a scenario now: the user clicks the button to receive rewards for new users, and if the user is not logged in, a popup will appearloginDialog; if the user has logged in, a button to receive rewards will pop upgetNewAccountDialog. We can use the following code to handle this logic:

run {
    
    
    if (!islogin) loginDialog else getNewAccountDialog 
}.show()

let: let is similar to apply, the only difference is the return value: apply returns the original object, And let returns the value inside the closure.

public inline fun <T, R> T.let(block: (T) -> R): R {
    
     
    return block(this)
}
data class Student(val age: Int)

class Kot {
    
    
    val student: Student? = getStu() 
    fun dealStu() {
    
    
        val result = student?.let {
    
    
            println(it.age)
            it.age
        } 
    }
}

Since the let function returns the last line of the closure, only when student is not null will print and return its age. Like run, it also limits the scope of the variable.

also: 它shotideletJapaneseapplyenhanced version

public inline fun <T> T.also(block: (T) -> Unit): T {
    
     
    block(this)
    return this
}

is the same as apply, it returns the receiver of the function

class Kot {
    
    
    val student: Student? = getStu()
    var age = 0

    fun dealStu() {
    
    
        val result = student?.also {
    
     stu ->
            this.age += stu.age
            println(this.age)
            println(stu.age) 
        } 
    }
}

I specify its implicit parameter asstu, assuming that student? is not empty, we will find that increased. studentage

It is worth noting that if apply is used here, because the block it executes is an extension function of type T , this will point to stu instead of Kot, here we will not be able to call Kot ofage.

takeIf: If we not only want to judge empty, but also want to add conditions, thenlet may seem a bit insufficient. Let's take a looktakeIf.

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    
     
    return if (predicate(this)) this else null
}

If we want to judge adult students before performing operations, we can write like this:

val result = student.takelf {
    
    it.age >= 18}.let {
    
    ...} 

We found that this is the same as filter in the collection, but takeIf only operates on a single piece of data. The opposite of takeIf is takeUnless, that is, the receiver will not execute until it meets specific conditions.

In addition to the above, there are many other convenient extension functions in Kotlin.

Extension apps in Android

Optimize Snackbar

It was added to the Android Support Library a few years ago, replacingToast. It solves some problems and introduces a new look, and the basic way to use it is as follows:

Snackbar.make(parentView, message_text, duration) 
	.setAction(action_text, click_listener)
	.show(); 

But in practice using its API adds unnecessary complexity to the code: we don't want to define every time when we want to display the message, and after filling in a bunch of parameters, why do we have to extra Callshow()?

The well-known open source project Anko hasSnackbar helper functions to make it easier to use and make the code cleaner:

snackbar(parentView, action_text, message_text) {
    
     click_listener } 

Some of the parameters are optional, so we generally use them like this:

snackbar(parentView, "message") 

Part of its source code is as follows:

inline fun View.snackbar(message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) {
    
    
    Snackbar.make(this, message, Snackbar.LENGTH_SHORT)
        .setAction(actionText, action)
        .apply {
    
     show() }
}

If you want to make it shorter:

snackbar("message") 

You can define extension functions yourself:

inline fun Activity.snackbar(message: String) = snackbar(find(R.id.content), message)
inline fun Fragment.snackbar(message: String) = snackbar(activity.find(R.id.content), message)

and View are not necessarily attached to Activity. We have to make a defensive judgment, that is: when we try to display < a i=3> Before, we must ensure that the attribute of hides an instance: SnackbarViewcontextActivity

inline fun View.snackbar(message: String) {
    
    
    val activity = context 
    if (activity is Activity) {
    
    
        snackbar(activity.find(android.R.id.content), message)
    } else {
    
    
        throw IllegalStateException("视图必须要承载在Activity上.")
    }
}

Wrapping Utils with extension functions

For example, we now have a method to determine whether the mobile phone network is available:

Boolean isConnected = NetworkUtils.isMobileConnected(context); 

As users of the code, we prefer to omit the NetworkUtils class name when calling, and let isMobileConnected look like < a property or method of i=3>. context

What we expect is the following usage:

Boolean isConnected= context.isMobileConnected(); 

SinceContext is a class that comes with the Andorid SDK, we cannot modify it. In Kotlin, we can simply implement it through the extension function:

It is worth mentioning that the life cycle of Context needs to be well controlled in Android. Here we should use ApplicationContext to prevent memory leaks or other problems caused by inconsistent life cycles.

In addition to the above methods, we also have many such common codes, which we can put under different files. Including the Snackbar mentioned above, we can also create a SnackbarUtils for it, which will provide a lot of convenience. But it should be noted that we cannot abuse this feature.

Solve annoying findViewById

For Android developers, this method will be familiar tofindViewById(): before we operate the view control, we need to passfindViewById method to find its corresponding instance.

Because the number of controls in an interface may be very large, in the early stages of Android development we usually see a large area of ​​findViewById(R.id.view_id) boilerplate code. In the old version of the SDK, after findBiewById obtains View, we even need to perform forced type conversion.

In Kotlin we can use extension functions to simplify:

fun <T : View> Activity._view(@ldRes id: Int): T {
    
     
	return findViewByld(id) as T
}

transfer:

loginButton = _view(R.id.btn_login); 
nameEditText = _view(R.id.et_name); 

is now more convenient to call, but some minimalist readers may think: currently we still need to create instances of loginButton and nameEditText , but these instances seem to only play the role of a "temporary variable". We rely on it to perform some click event binding (onlick) and assignment operations, and it seems to be of little use. Can you omit it and operate on R.id.* directly? The answer is yes, in Kotlin we can use higher-order functions to make the following changes (here is simplifiedonclick as an example):

fun Int.onClick(click: () -> Unit) {
    
    
    // _view 为我们之前定义的简化版 findViewByld
    val tmp = _view<View>(this).apply {
    
    
        setOnClickListener {
    
     
            click()
        }
    }
}

We can bind the click event of the login button like this:

R.id.btn_login.onClick {
    
     println("Login...") } 

Readers with obsessive-compulsive disorder may not be able to stand the way of writing R.id.xx and have to write the R.id prefix every time. In some cases, Cause trouble.

Is there a more concise way to write it? The answer is yes, Kotlin provides us with an extension:

apply plugin: 'kotlin-android-extensions'  
btn_login.setOnClickListener {
    
    
	println("MainKotlinActivity onClick Button") 
}

Although several charactersR.id. are omitted, will the introduction cause performance problems? Let's decompile it first and see how it is implemented in the corresponding Java code:

public final class MainActivity extends BaseActivity {
    
    
    private HashMap<Integer, View> _$_findViewCache;

    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        this.setContentView(2131296283);
        ((TextView) this._$_findCachedViewById(id.label)).setText((CharSequence));
        ((TextView) this._$_findCachedViewById(id.label)).setOnClickListener((OnClickListener));
        ((Button) this._$_findCachedViewById(id.btn)).setOnClickListener((OnClickListener));
    }

    public View _$_findCachedViewById(int var1) {
    
    
        if (this._$_findViewCache == null) {
    
    
            this._$_findViewCache = new HashMap();
        }

        View var2 = (View) this._$_findViewCache.get(var1);
        if (var2 == null) {
    
    
            var2 = this.findViewById(var1);
            this._$_findViewCache.put(var1, var2);
        }

        return var2;
    }

    public void _$_clearFindViewByIdCache() {
    
    
        if (this._$_findViewCache != null) {
    
    
            this._$_findViewCache.clear();
        }
    }
}

You will be pleasantly surprised to find that when you use the control for the first time, you can search it in the cache collection. If it is available, use it directly. If not, search it throughfindViewById and add it to in the cache collection. It also provides the $clearFindViewByIdCache() method for clearing the cache, which can be used when we want to completely replace the interface controls.

Note: Fragment’s onDestroyView() method calls $clearFindViewByIdCache() to clear the cache by default, and ActivityNo.

AlthoughKAE(Kotlin Android Extensions) is very convenient, but unfortunately, for some reasons,KAE was officially declared obsolete starting from Kotlin 1.4.2, and in Kotlin1.7 version has been directly removed. . (For details, please see here)

But if you like, you can still imitate its source code to implement a set of maintenance yourself.

Alternatively, we can use other alternatives, such as viewbinding, and add the following configuration in gradle:< /span>

android {
    
    
     viewBinding {
    
    
        enabled = true
     }
}

Then the usage effect is the same as KAE, but this has little to do with kotlin features. (The system will generate a binding class for each XML layout file in this module)

Expansion is not a panacea

Static and dynamic scheduling

It is known that we have the following Java classes:

class Base {
    
    
    public void foo() {
    
    
        System.out.println(("I'm Base foo!"));
    }
}
class Extended extends Base {
    
    
    @Override
    public void foo() {
    
    
        System.out.println(("I'm Extended foo!"));
    }
}

Base base = new Extended();
base.foo();

We declare a variable namedbase, which has compile-time typeBase and run-time typeExtended. When we call base.foo() the method will be dynamically dispatched, which means that the method of the runtime type (Extended) is called.

When we call an overloaded method, the dispatch becomes static and depends only on the compile-time type.

void foo(Base base) {
    
    
	...
}
void foo(Extended extended) {
    
    
	...
}
public static void main(String] args) {
    
    
	Base base = new Extended();
	foo(base);
}

In this case, even thoughbase is essentially an instance of Extended, Base will eventually be executed method.

Extension functions are always statically dispatched

Maybe you are curious, what does this have to do with expansion? We know that extension functions have a receiver (receiver). Since the receiver is actually just a parameter of the compiled method in byte code, you can overload it, but you cannot override it. This is probably the most important difference between members and extension functions: the former are dynamically dispatched, the latter are always statically dispatched.

To make it easier to understand, let’s give an example:

open class Base
class Extended : Base()

fun Base.foo() = "I'm Base.foo!"
fun Extended.foo() = "I'm Extended.foo!"

fun main() {
    
    
    val instance: Base = Extended()
    val instance2 = Extended()
    println(instance.foo()) // Output: I'm Base.foo!
    println(instance2.foo()) // Output: I'm Extended.foo!
}

As we said, since only compile-time types are considered, the 1st print will call Base.foo() and the 2nd print will call Extended.foo() .

extension function in class

If we declare an extension function inside a class, then it will not be static. If the extension function is added with the open keyword, we can override it in the subclass (override). Does this mean it will be dynamically scheduled? This is an embarrassing problem:When an extension function is declared inside a class, it has both a dispatch receiver and an extension receiver.

Scheduling Receiver and Extended Receiver Concepts

  • Extension receiver: A receiver closely related to Kotlin extensions, representing the object for which we define the extension.
  • dispatch receiver: A special receiver that exists when an extension is declared as a member. It represents an instance of the class in which the extension is declared.
class X {
    
    
	fun Y.foo() = " I'm Y.foo" 
}

In the above example, X is the scheduled receiver and Y is the extended receiver. If an extension function is declared as open, its dispatch receiver can only be dynamic, while extension receivers are always resolved at compile time.

You may not quite understand this, so let’s give an example to help you understand:

open class Base
class Extended : Base()

open class X {
    
    
    open fun Base.foo() {
    
    
        println("I'm Base.foo in X")
    }
    open fun Extended.foo() {
    
    
        println("I'm Extended.foo in X")
    }
    fun deal(base: Base) {
    
    
        base.foo()
    }
}
class Y : X() {
    
    
    override fun Base.foo() {
    
    
        println("I'm Base.foo in Y")
    }
    override fun Extended.foo() {
    
    
        println("I'm Extended.foo in Y")
    }
}

fun main() {
    
    
    X().deal(Base()) // 输出: I'm Base.foo in X
    Y().deal(Base()) // 输出: I'm Base.foo in Y 即 dispatch receiver 被动态调度

    X().deal(Extended()) // 输出: I'm Base.foo in X 即 extension receiver 被静态调度
    Y().deal(Extended()) // 输出: I'm Base.foo in Y
}

If you are smart, you might notice thatExtendedthe extension function is never called, and this behavior is consistent with what we saw earlier in the static dispatch example. The direct factor that determines which of the two Base class extension functions is executed is the runtime type of the class that executes the deal method.

Through the above examples, we can summarize several things that need to be paid attention to in the extension function:

  • If the extension function is a top-level function or member function, it cannot be overridden;
  • We cannot access the non-public properties of its receiver;
  • Extension receivers are always statically scheduled.

Abused extension functions

fun Context.loadImage(url: String, imageView: ImageView) {
    
    
    GlideApp.with(this)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(imageView) 
}

// ImageActivity.kt 中使用
...
this.loadlmage(url, imgView) 
...

Maybe you don't feel anything strange when using it, but in fact, we are not extending the existing class in any way. The above code is just to omit parameters when calling the function, which is an abuse of the expansion mechanism.

We know thatContextbeing the “God Object” comes with a lot of responsibilities.

We are based on Context expansion, and it is also likely to produce many problems caused by ImageView inconsistency with the incoming context cycle.

The correct approach should be to extend onImageView:

fun ImageView.loadImage(url: String) {
    
    
    GlideApp.with(this.context)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(this)
}

// Example usage
imageView.loadImage("https://example.com/image.jpg")

This not only saves more parameters when calling, but also ensures the life cycle of ImageView.

In actual projects, we also need to consider the replacement and maintenance of the network request framework. Generally, the image request framework will be re-encapsulated:

object ImageLoader {
    
    
    fun with(context: Context, url: String, imageView: ImageView) {
    
    
        GlideApp.with(context)
            .load(url)
            .placeholder(R.mipmap.img_default)
            .error(R.mipmap.ic_error)
            .into(imageView)
    }
}

// Example usage
ImageLoader.with(context, "https://example.com/image.jpg", imageView)

Therefore, although extension functions can provide a lot of convenience, we should still pay attention to using it in the appropriate place, otherwise it will cause unnecessary trouble.

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/135034921