Kotlin From Scratch: More Fun With Functions
2022-5-25 11:44:32 Author: code.tutsplus.com(查看原文) 阅读量:19 收藏

Kotlin is a modern programming language that compiles to Java bytecode. It is free and open source, and promises to make coding for Android even more fun.

In the previous article, you learned about packages and basic functions in Kotlin. Functions are at the heart of Kotlin, so in this post we'll look more closely at them. We'll be exploring the following kinds of functions in Kotlin:

  • top-level functions
  • lambda expressions or function literals
  • anonymous functions
  • local or nested functions
  • infix functions
  • member functions

You'll be amazed at all the cool things you can do with functions in Kotlin!

1. Top-Level Functions

Top-level functions are functions inside a Kotlin package that are defined outside of any class, object, or interface. This means that they are functions you call directly, without the need to create any object or call any class.

If you're a Java coder, you know that we typically create utility static methods inside helper classes. These helper classes don't really do anything—they don't have any state or instance methods, and they just act as a container for the static methods. A typical example is the Collections class in the java.util package and its static methods.

Top-level functions in Kotlin can be used as a replacement for the static utility methods inside helper classes we code in Java. Let's look at how to define a top-level function in Kotlin.

package com.chikekotlin.projectx.utils

fun checkUserStatus(): String {
    return "online"
}

In the code above, we defined a package com.chikekotlin.projectx.utils inside a file called UserUtils.kt and also defined a top-level utility function called checkUserStatus() inside this same package and file. For brevity's sake, this very simple function returns the string "online".

The next thing we'll do is to use this utility function in another package or file.

package com.chikekotlin.projectx.users

import com.chikekotlin.projectx.utils.checkUserStatus

if (checkUserStatus() == "online") {
    // do something
}

In the preceding code, we imported the function into another package and then executed it! As you can see, we don't have to create an object or reference a class to call this function.

Java Interoperability

Given that Java doesn't support top-level functions, the Kotlin compiler behind the scenes will create a Java class, and the individual top-level functions will be converted to static methods. In our own case, the Java class generated was UserUtilsKt with a static method checkUserStatus().

/* Java */
package com.chikekotlin.projectx.utils

public class UserUtilsKt {

    public static String checkUserStatus() {
        return "online";
    }
}

This means that Java callers can simply call the method by referencing its generated class, just like for any other static method.

/* Java */
import com.chikekotlin.projectx.utils.UserUtilsKt

... 

UserUtilsKt.checkUserStatus()

Note that we can change the Java class name that the Kotlin compiler generates by using the @JvmName annotation.

@file:JvmName("UserUtils")
package com.chikekotlin.projectx.utils

fun checkUserStatus(): String {
    return "online"
}

In the code above, we applied the @JvmName annotation and specified a class name UserUtils for the generated file. Note also that this annotation is placed at the beginning of the Kotlin file, before the package definition.

It can be referenced from Java like this:

/* Java */
import com.chikekotlin.projectx.utils.UserUtils

... 

UserUtils.checkUserStatus()

2. Lambda Expressions

Lambda expressions (or function literals) are also not bound to any entity such as a class, object, or interface. They can be passed as arguments to other functions called higher-order functions (we'll discuss these more in the next post). A lambda expression represents just the block of a function, and using them reduces the noise in our code.

If you're a Java coder, you know that Java 8 and above provides support for lambda expressions. To use lambda expressions in a project that supports earlier Java versions such as Java 7, 6, or 5, we can use the popular Retrolambda library.

One of the awesome things about Kotlin is that lambda expressions are supported out of the box. Because lambda is not supported in Java 6 or 7, for Kotlin to interoperate with it, Kotlin creates a Java anonymous class behind the scene. But note that creating a lambda expression in Kotlin is quite different than it is in Java.

Here are the characteristics of a lambda expression in Kotlin:

  • It must be surrounded by curly braces {}.
  • It doesn't have the fun keyword.
  • There is no access modifier (private, public or protected) because it doesn't belong to any class, object, or interface.
  • It has no function name. In other words, it's anonymous.
  • No return type is specified because it will be inferred by the compiler.
  • Parameters are not surrounded by parentheses ().

And, what's more, we can assign a lambda expression to a variable and then execute it.

Creating Lambda Expressions

Let's now see some examples of lambda expressions. In the code below, we created a lambda expression without any parameters and assigned it a variable message. We then executed the lambda expression by calling message().

val message = { println("Hey, Kotlin is really cool!") }
message() // "Hey, Kotlin is really cool!"

Let's also see how to include parameters in a lambda expression.

val message = { myString: String -> println(myString) }
message("I love Kotlin") // "I love Kotlin"
message("How far?") // "How far?"

In the code above, we created a lambda expression with the parameter myString, along with the parameter type String. As you can see, in front of the parameter type, there is an arrow: this refers to the lambda body. In other words, this arrow separates the parameter list from the lambda body. To make it more concise, we can completely ignore the parameter type (already inferred by the compiler).

val message = { myString -> println(myString) } // will still compile

To have multiple parameters, we just separate them with a comma. And remember, we don't wrap the parameter list in parentheses like in Java.

val addNumbers = { number1: Int, number2: Int ->
        println("Adding $number1 and $number2")
        val result = number1 + number2
        println("The result is $result")
    }
addNumbers(1, 3)

However, note that if the parameter types can't be inferred, they must be specified explicitly (as in this example), otherwise the code won't compile.

Adding 1 and 3
The result is 4

Passing Lambdas to Functions

We can pass lambda expressions as parameters to functions: these are called "higher-order functions", because they are functions of functions. These kinds of functions can accept a lambda or an anonymous function as parameter: for example, the last() collection function.

In the code below, we passed in a lambda expression to the last() function. (If you want a refresher on collections in Kotlin, visit the third tutorial in this series) As the name says, it returns the last element in the list. last() accepts a lambda expression as a parameter, and this expression in turn takes one argument of type String. Its function body serves as a predicate to search within a subset of elements in the collection. That means that the lambda expression will decide which elements of the collection will be considered when looking for the last one.

val stringList: List<String> = listOf("in", "the", "club")
print(stringList.last()) // will print "club"
print(stringList.last({ s: String -> s.length == 3})) // will print "the"

Let's see how to make that last line of code above more readable.

stringList.last { s: String -> s.length == 3 } // will also compile and print "the"

The Kotlin compiler allows us to remove the function parentheses if the last argument in the function is a lambda expression. This syntax of passing a function as a lambda expression is also known as trailing lambda. As you can observe in the code above, we were allowed to do this because the last and only argument passed to the last() function is a lambda expression.

Furthermore, we can make it more concise by removing the parameter type.

stringList.last { s -> s.length == 3 } // will also compile print "the"

We don't need to specify the parameter type explicitly, because the parameter type is always the same as the collection element type. In the code above, we're calling last on a list collection of String objects, so the Kotlin compiler is smart enough to know that the parameter will also be a String type.

The it Argument Name

We can even simplify the lambda expression further again by replacing the lambda expression argument with the auto-generated default argument name it.

stringList.last { it.length == 3 }

The it argument name was auto-generated because last can accept a lambda expression or an anonymous function (we'll get to that shortly) with only one argument, and its type can be inferred by the compiler.

Local and Non-Local Return in Lambda Expressions

Let's start with an example. In the code below, we pass a lambda expression to the foreach() function invoked on the intList collection. This function will loop through the collection and execute the lambda on each element in the list. If any element is divisible by 2, it will stop and return from the lambda.

fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach {
        if (it % 2 == 0) {
            return
        } else {
            println("Printing: $it")
        }
    }
    println("End of surroundingFunction()")
}

/* Expected Output:
Printing: 1
Printing: 2
Printing: 3
End of surroundingFunction() */

/* Actual Output:
Printing: 1 */
surroundingFunction()

Running the above code might not have given you the result you might have expected. This is because that return statement won't return from the lambda but instead from the containing function surroundingFunction()! This means that the last code statement in the surroundingFunction() will never execute and the loop itself will only iterate over the first two items.

// ...
println("End of surroundingFunction()") // This won't execute
// ...

To fix this problem, we need to tell it explicitly which function to return from by using a label or name tag.

fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach {
        if (it % 2 == 0) {
            return@forEach
        } else {
            println("Printing: $it")
        }
    }
    println("End of surroundingFunction()") // Now, it will execute
}

/* Expected Output:
Printing: 1
Printing: 3
Printing: 5
End of surroundingFunction() */

/* Actual Output:
Printing: 1
Printing: 3
Printing: 5
End of surroundingFunction() */
surroundingFunction()

In the updated code above, we specified the default tag @forEach immediately after the return keyword inside the lambda. We have now instructed the compiler to return from the lambda instead of the containing function surroundingFunction(). Now the last statement of surroundingFunction() will execute.

So far, we have been using implicit labels for the return points because they share the same name as the function to which we passed our lambda. However, we can also define our own label or name tag.

    // ...
    intList.forEach myLabel@ {
        if (it % 2 == 0) {
            return@myLabel
    // ...            

In the code above, we defined our custom label called myLabel@ and then specified it for the return keyword. The @forEach label generated by the compiler for the forEach function is no longer available because we have defined our own.

You might have observed that using the return keyword with labels above helped us simulate the behavior of the continue keyword in regular loops.

Can you guess how we can simulate the behavior of the break keyword using something similar? We simply nest our original code into another lambda and then using the return labels to execute a non-local return. Here is an example:

fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    run breakHere@ {
        intList.forEach {
            if (it % 2 == 0) {
                return@breakHere
            } else {
                println("Printing: $it")
            }
        }
    }
    println("End of surroundingFunction()") // It will still execute
}

/* Printing: 1
End of surroundingFunction() */
surroundingFunction()

You'll soon see how this local return problem can be solved without labels when we discuss anonymous functions in Kotlin shortly.

3. Member Functions

This kind of function is defined inside a class, object, or interface. Using member functions helps us to modularize our programs further. Let's now see how to create a member function.

class Circle {

    fun calculateArea(radius: Double): Double {
        require(radius > 0, { "Radius must be greater than 0" })
        return Math.PI * Math.pow(radius, 2.0)
    }
}

This code snippet shows a class Circle (we'll discuss Kotlin classes in later posts) that has a member function calculateArea(). This function takes a parameter radius to calculate the area of a circle.

To invoke a member function, we use the name of the containing class or object instance with a dot, followed by the function name, passing any arguments if need be.

val circle = Circle()
print(circle.calculateArea(4.5)) // will print "63.61725123519331"

4. Anonymous Functions

An anonymous function is another way to define a block of code that can be passed to a function. It is not bound to any identifier. Here are the characteristics of an anonymous function in Kotlin:

  • has no name
  • is created with the fun keyword
  • contains a function body
val stringList: List<String> = listOf("in", "the", "club")
print(stringList.last{ it.length == 3}) // will print "the"

Because we passed a lambda to the last() function above, we can't be explicit about the return type. To be explicit about the return type, we need to use an anonymous function instead.

val strLenThree = stringList.last( fun(string): Boolean {
    return string.length == 3
})
print(strLenThree) // will print "the"

In the above code, we have replaced the lambda expression with an anonymous function because we want to be explicit about the return type.

Towards the end of the lambda section in this tutorial, we used a label to specify which function to return from. Using an anonymous function instead of a lambda inside the forEach() function solves this problem more simply. The return expression returns from the anonymous function and not from the surrounding one, which in our case is surroundingFunction().

fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach ( fun(number) {
        if (number % 2 == 0) {
            return
        }
    })
    println("End of surroundingFunction()") // statement executed
}

surroundingFunction() // will print "End of surroundingFunction()"

5. Local or Nested Functions

To take program modularization further, Kotlin provides us with local functions — also known as nested functions. A local function is a function that is declared inside another function.

fun printCircumferenceAndArea(radius: Double): Unit {

    fun calCircumference(radius: Double): Double = (2 * Math.PI) * radius
    val circumference = "%.2f".format(calCircumference(radius))

    fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0)
    val area = "%.2f".format(calArea(radius))

    print("The circle circumference of $radius radius is $circumference and area is $area")
}

printCircumferenceAndArea(3.0) // The circle circumference of 3.0 radius is 18.85 and area is 28.27

As you can observe in the code snippet above, we have two single-line functions: calCircumference() and calArea() nested inside the printCircumferenceAndAread() function. The nested functions can be called only from within the enclosing function and not outside. Again, the use of nested functions makes our program more modular and tidy.

We can make our local functions more concise by not explicitly passing parameters to them. This is possible because local functions have access to all parameters and variables of the enclosing function. Let's see that now in action:

fun printCircumferenceAndArea(radius: Double): Unit {

    fun calCircumference(): Double = (2 * Math.PI) * radius
    val circumference = "%.2f".format(calCircumference())

    fun calArea(): Double = (Math.PI) * Math.pow(radius, 2.0)
    val area = "%.2f".format(calArea())
    // ... 
}

As you can see, this updated code looks more readable and reduces the noise we had before. The enclosing function in this example is small. In a larger enclosing function that can be broken down into smaller nested functions, this feature can really come in handy.

6. Infix Functions

The infix notation allows us to easily call a one-argument member function or extension function. In addition to a function being one-argument, you must also define the function using the infix modifier. To create an infix function, two parameters are involved. The first parameter is the target object, while the second parameter is just a single parameter passed to the function.

Creating an Infix Member Function

Let's look at how to create an infix function in a class. In the code example below, we created a Student class with a mutable kotlinScore instance field. We created an infix function by using the infix modifier before the fun keyword. As you can see below, we created an infix function addKotlinScore() that takes a score and adds to the kotlinScore instance field.

class Student {
    var kotlinScore = 0.0
    
    infix fun addKotlinScore(score: Double): Unit {
        this.kotlinScore = kotlinScore + score
    }
}

Calling an Infix Function

Let's also see how to invoke the infix function we have created. To call an infix function in Kotlin, we don't need to use the dot notation, and we don't need to wrap the parameter with parentheses.

val student = Student()
student addKotlinScore 95.00
print(student.kotlinScore) // will print "95.0"

In the code above, we called the infix function, the target object is student, and the double 95.00 is the parameter passed to the function.

Using infix functions wisely can make our code more expressive and clearer than the normal style. This is greatly appreciated when writing unit tests in Kotlin (we'll discuss testing in Kotlin in a future post).

"Chike" should startWith("ch")
myList should contain(myElement) 
"Chike" should haveLength(5)
myMap should haveKey(myKey) 

The to Infix Function

In Kotlin, we can make the creation of a Pair instance more succinct by using the to infix function instead of the Pair constructor. (Behind the scenes, to also creates a Pair instance.) Note that the to function is also an extension function (we'll discuss these more in the next post).

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

The to infix function has been implemented in the Kotlin standard library itself. The line above comes from the source in Kotlin Tuples. It is very easy for someone who is just starting out to get confused and think that to is a special reserved keyword in Kotlin.

Let's now compare the creation of a Pair instance using both the to infix function and directly using the Pair constructor, which performs the same operation, and see which one is better.

val nigeriaCallingCodePair = 234 to "Nigeria"
val nigeriaCallingCodePair2 = Pair(234, "Nigeria") // Same as above

As you can see in the code above, using the to infix function is more concise than directly using the Pair constructor to create a Pair instance. Remember that using the to infix function, 234 is the target object and the String "Nigeria" is the parameter passed to the function. Moreover, note that we can also do this to create a Pair type:

val nigeriaCallingCodePair3 = 234.to("Nigeria") // same as using 234 to "Nigeria"

In the Ranges and Collections post, we created a map collection in Kotlin by giving it a list of pairs—the first item being the key, and the second one being the value. Let's also compare the creation of a map by using both the to infix function and the Pair constructor to create the individual pairs.

val callingCodesMap: Map<Int, String> = mapOf(234 to "Nigeria", 1 to "USA", 233 to "Ghana")

In the code above, we created a comma-separated list of Pair types using the to infix function and passed them to the mapOf() function. We can also create the same map by directly using the Pair constructor for each pair.

val callingCodesPairMap: Map<Int, String> = mapOf(Pair(234, "Nigeria"), Pair(1, "USA"), Pair(233, "Ghana"))

As you can see again, sticking with the to infix function has less noise than using the Pair constructor.

Infix Functions for Creating Ranges

In the tutorial on Ranges and Collections, we used the infix functions until and step to create a range of numbers and then step through it. This allows us to write our program in a way that mimics natural language to some extent. Here is an example:

// 1--1  2--4  3--9  4--16  5--25  6--36  7--49  8--64  9--81  10--100 
for (i in 1 until 11) {
    print(" $i--${i*i} ")
}

// 1--1  3--27  5--125  7--343  9--729 
for (i in 1 until 11 step 2) {
    print(" $i--${i*i*i} ")
}

There are many other built-in infix functions defined in Kotlin for a variety of purposes. I recommend that you take a look at the source code of these functions to see what they are doing behind the scenes and learn more about the language.

Conclusion

In this tutorial, you learned about some of the cool things you can do with functions in Kotlin. We covered:

  • top-level functions
  • lambda expressions or function literals
  • member functions
  • anonymous functions
  • local or nested functions
  • infix functions

But that's not all! There is still more to learn about functions in Kotlin. So in the next post, you'll learn some advanced uses of functions, such as extension functions, higher-order functions, and closures. See you soon!

To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts+!


文章来源: https://code.tutsplus.com/tutorials/kotlin-from-scratch-more-functions--cms-29479
如有侵权请联系:admin#unsafe.sh