Translation note: the translation level is limited, the article may appear inaccurate or even wrong understanding, please forgive me! Criticism and correction are welcome. Each article will combine its own understanding and examples, hoping to help you learn Kotlin.
[Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly
Uberto Barbini
preface
Extension methods (and attributes) are new to Java developers and have actually been around in C# for a long time, but their JVM support first appeared in Kotlin.
Ps: Extension functions are not difficult to understand, but there are not many explanations and tutorials on usage scenarios and specifications. If you are not familiar with Kotlin's extensions, it is recommended to read the official documentation for the extensions. If you are familiar with extensions, this article will help you get a handle on them.
Official documentation – Extensions
After studying the vast Kotlin codels and browsing the open source Kotlin code, I noticed that extensions often make code more readable while others make it harder to understand.
This was also a hot topic of discussion for the first time with Kotlin as a team developer, so I thought it would be worthwhile to summarize the extension based on my successes and failures.
If you have a different opinion or you have a good example, please do contact me.
The body of the
-
A brief introduction to the extension
There are two types of extensions in Kotlin: extension functions and extension properties.
First let’s look at what an extension function is and how to declare it in Kotlin.
fun String.tail() = this.substring(1) fun String.head() = this.substring(0, 1) Copy the code
I’ve just written two functions, one that returns the first character of a string and the other that returns the rest of the string.
The important point here is that our function name (such as tail) comes after the type we want to call the function, in this case String.
Let’s write a test method that looks something like this:
@Test fun `head and tail`() {t+ assertThat("abcde".head()).isEqualTo("a") assertThat("abcde".tail()).isEqualTo("bcde")}Copy the code
Apparently, as you would expect. We’re just adding two methods to String. Let’s convert the code to Java code and see:
@NotNull public static final String head(@NotNull String $this$head) { String var10000 = $this$head.substring(0, 1); return var10000; } Copy the code
Find that head has become a static method with a String argument. So this is a grammar candy? Yes, this is grammar candy!
Still, extensions can improve readability and make it harder to understand.
The extension function type looks like this: String.() -> String
The String to the left of String.() we call the function receiver
-
After reading the above, we know that extension functions can be applied to any class. Are there more application scenarios? There are! This is the generic extension function.
fun <T> T.foo() : String = "foo $this" Copy the code
Once foo is in your scope, you can call this method with any object, including NULL.
Here’s an example of proof:
assertThat(123.foo()).isEqualTo("foo 123") val frank = User(1, "Frank") assertThat(frank.foo()).isEqualTo("foo User(id=1, name=Frank)") assertThat(null.foo()).isEqualTo("foo null") Copy the code
We can also extend the limits by changing the generic parameters. Let’s say we only want some classes and their subclasses to be called:
Fun
t.doubleit (): Double = this.todouble () * 2 (nullable Number). Then null is not an acceptable receiver type for this extension function.
AssertThat (123. DoubleIt ()). IsEqualTo assertThat (246.0) (123.4 doubleIt ()). IsEqualTo (246.8) AssertThat (123L.doubleit ()).isequalto (246.0) //assertThat(null.doubleit ()).isequalto (null) compilation failed!Copy the code
So, if we wanted to restrict the function foo mentioned above to function recipients of non-empty types, we could declare it like this:
fun <T: Any> T.foo() = "foo $this"
-
Application of extension function in infix notation
If you are unfamiliar with infixes, you can take a look at the explanation of the official documents-infix notation
For example, I’m not a big fan of Kotlin’s nullable string concatenation. I thought the result would be null + NULL == NULL and NULL + “A” == “A” but in Kotlin the result would be “nullNULL” and “nullA”.
So we can achieve the desired result by using the infix function + extension function ++ (in backquotes) :
infix fun String? .`++`(s:String?) :String? =if (this == null) s else if (s == null) this else this + s Copy the code
Through verification, the tone effect is achieved:
assertThat(null `++` null).isNull() assertThat("A" `++` null).isEqualTo("A") assertThat(null `++` "B").isEqualTo("B") assertThat("A"` + + `"B").isEqualTo("AB") Copy the code
Infix notation is useful for developing DSLS for internal use.
-
Extended attributes
Now let’s take a look at what extended properties do.
Very simple! Extension functions allow us to add methods to existing classes, so extension properties allow us to add properties to existing classes.
As you probably already know, the Kotlin compiler generates properties whenever we access a Java Bean object.
public class JavaPerson { private int age; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}Copy the code
When we use the bean object in Kotlin, all getters and setters become properties:
val p = JavaPerson() p.name = "Fred" p.age = 32 assertThat(p.name).isEqualTo("Fred") assertThat(p.age).isEqualTo(32) Copy the code
As can be seen from transcoding, these attributes are obtained by mapping directly to getters and setters:
L1 LINENUMBER 15 L1 ALOAD 1 LDC "Fred"INVOKEVIRTUAL com/ubertob/extensions/JavaPerson.setName (Ljava/lang/String;) VCopy the code
How do you declare new attributes? We add a millis attribute to the Java Date class:
var Date.millis: Long get() = this.getTime() set(x) = this.setTime(x) Copy the code
The test code
val d = Date() d.millis = 1001 assertThat(d.millis ).isEqualTo(1001L) assertThat(d.millis ).isEqualTo(d.time) Copy the code
-
Extended application
Now let me show you an example of how to extend and optimize your code. FizzBuzz is a common interview question. FizzBuzz: Write a program to print the numbers from 1 to 100. For multiples of 3, print “Fizz” and for multiples of 5, print “Buzz”.)
Now first we can find the number of Fizz and Buzz printed by attribute:
val Int.isFizz: Boolean get() = this % 3 == 0 val Int.isBuzz: Boolean get() = this % 5 == 0 Copy the code
Using the nullable String extension ++ we defined above, we can implement FizzBuzz in one line of code:
fun Int.fizzBuzz(): String = "Fizz".takeIf { isFizz } `++` "Buzz".takeIf { isBuzz } ? : toString()Copy the code
Int. FizzBuzz () extension function of Int. FizzBuzz ()Int; Fizz”. TakeIf {isFizz} an Int call isFizz method return string “Fizz” if true, otherwise null; Buzz”. TakeIf {isBuzz} an Int to call isBuzz. If true, return string “Buzz”; ++ splice left and right results; ? :toString() Calls toString if null.
Here is the test code:
val res = (1.. 15).map { it.fizzBuzz()}.joinToString() val expected ="1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz" assertThat ( res ).isEqualTo(expected) Copy the code
-
summary
When should you use extensions and when should you not? Here is my personal opinion. This summary is entirely my personal opinion and you may have different views and interpretations. However, these summaries were also made by reviewing other people’s code, so they should be of some value. As far as I know, there is an official specification for when extensions should be used, but I refer to the following usage patterns summarized by the Kotlin library:
Extend the usage pattern of attributes
Good usage scenarios:
- Replace setters and getters
- Rename fields of properties by mapping
- Simple method calls for a single field (e.g
isFizz
)
Bad usage scenarios:
-
Just to remove the parentheses from the method in the DSL: for example, converting an Int to a Duration 5. ToHours () is easier to understand than 5.hours
-
Map attributes in important methods that use multiple fields
How to use extension functions:
Good usage scenarios:
-
A pure function with one argument, string.reverse ()
-
Type conversions map.tolist (), int.toprice ()
-
Map.assequence (), user.asperson ()
-
Chain programming examples t.apply {… }, T.l et {… }
-
A to B, HttpRoute bind {}
-
Non-invasive syntax for existing classes user.fullname ()
-
Circumventing the covariant and contravariant problems of generics example iterable.flatmap {… }
class MyContainerClass<in T> { fun <U> map(f: (T) -> U): MyContainerClass<U>{...} } Copy the code
The compiler will report an error because T is qualified by the in modifier but appears in the map method at the position of out. We can solve this problem by changing the map method to an extension function:
class MyContainerClass<inT> { ... } fun <U, T> MyContainerClass<T>.map(f: (T) -> U): MyContainerClass<U>{... }Copy the code
This is Kotlin’s knowledge of generics and for those of you who aren’t familiar with it, I’ll do a chapter on generics later on
Bad usage scenarios
- Examples of complex methods involving IO threads and singletons user.savetodb (), 8080.starthttpServer ()
- Example of a method that changes the global state: “12:45” setTime()
- Multi-parameter method example: Joe toUser(” Smith “, “[email protected]”)
- Commonly used type extension on special range (single) method in the Date isFredBirthday (), Double. GetDiscountedPrice ()