编辑本页

函数式编程

函数类型

Like in Python, functions in Kotlin are first-class values - they can be assigned to variables and passed around as parameters. The type a function is a function type, which is indicated with a parenthesized parameter type list and an arrow to the return type. Consider this function:

fun safeDivide(numerator: Int, denominator: Int) =
    if (denominator == 0) 0.0 else numerator.toDouble() / denominator

It takes two Int parameters and returns a Double, so its type is (Int, Int) -> Double. We can reference the function itself by prefixing its name with ::, and we can assign it to a variable (whose type would normally be inferred, but we show the type signature for demonstration):

val f: (Int, Int) -> Double = ::safeDivide

When you have a variable or parameter of function type (sometimes called a function reference), you can call it as if it were an ordinary function, and that will cause the referenced function to be called:

val quotient = f(3, 0)

It is possible for a class to implement a function type as if it were an interface. It must then supply an operator function called invoke with the given signature, and instances of that class may then be assigned to a variable of that function type:

class Divider : (Int, Int) -> Double {
    override fun invoke(numerator: Int, denominator: Int): Double = ...
}

函数字面值:lambda 表达式与匿名函数

Like in Python, you can write lambda expressions: unnamed function declarations with a very compact syntax, which evaluate to callable function objects. In Kotlin, lambdas can contain multiple statements, which make them useful for more complex tasks than the single-expression lambdas of Python. The last statement must be an expression, whose result will become the return value of the lambda (unless Unit is the return type of the variable/parameter that the lambda expression is assigned to, in which case the lambda has no return value). A lambda expression is enclosed in curly braces, and begins by listing its parameter names and possibly their types (unless the types can be inferred from context):

val safeDivide = { numerator: Int, denominator: Int ->
    if (denominator == 0) 0.0 else numerator.toDouble() / denominator
}

The type of safeDivide is (Int, Int) -> Double. Note that unlike function type declarations, the parameter list of a lambda expression must not be enclosed in parentheses.

Note that the other uses of curly braces in Kotlin, such as in function and class definitions and after if/else/for/while statements, are not lambda expressions (so it is not the case that if is a function that conditionally executes a lambda function).

The return type of a lambda expression is inferred from the type of the last expression inside it (or from the function type of the variable/parameter that the lambda expression is assigned to). If a lambda expression is passed as a function parameter (which is the ordinary use) or assigned to a variable with a declared type, Kotlin can infer the parameter types too, and you only need to specify their names:

val safeDivide: (Int, Int) -> Double = { numerator, denominator ->
    if (denominator == 0) 0.0 else numerator.toDouble() / denominator
}

Or:

fun callAndPrint(function: (Int, Int) -> Double) {
    println(function(2, 0))
}

callAndPrint({ numerator, denominator ->
    if (denominator == 0) 0.0 else numerator.toDouble() / denominator
})

A parameterless lambda does not need the arrow. A one-parameter lambda can choose to omit the parameter name and the arrow, in which case the parameter will be called it:

val square: (Double) -> Double = { it * it }

If the type of the last parameter to a function is a function type and you want to supply a lambda expression, you can place the lambda expression outside of the parameter parentheses. If the lambda expression is the only parameter, you can omit the parentheses entirely. This is very useful for constructing DSLs.

fun callWithPi(function: (Double) -> Double) {
    println(function(3.14))
}

callWithPi { it * it }

If you want to be more explicit about the fact that you're creating a function, you can make an anonymous function, which is still an expression rather than a declaration:

callWithPi(fun(x: Double): Double { return x * x })

Or:

callWithPi(fun(x: Double) = x * x)

Lambda expressions and anonymous functions are collectively called function literals.

集合推导

Kotlin can get quite close to the compactness of Python's list/dict/set comprehensions. Assuming that people is a collection of Person objects with a name property:

val shortGreetings = people
    .filter { it.name.length < 10 }
    .map { "Hello, ${it.name}!" }

corresponds to

short_greetings = [
    f"Hello, {p.name}"  # In Python 2, this would be: "Hello, %s!" % p.name
    for p in people
    if len(p.name) < 10
]

In some ways, this is easier to read because the operations are specified in the order they are applied to the values. The result will be an immutable List<T>, where T is whichever type is produced by the transformations you use (in this case, String). If you need a mutable list, call toMutableList() at the end. If you want a set, call toSet() or toMutableSet() at the end. If you want to transform a collection into a map, call associateBy(), which takes two lambdas that specify how to extract the key and the value from each element: people.associateBy({it.ssn}, {it.name}) (you can omit the second lambda if you want the entire element to be the value, and you can call toMutableMap() at the end if you want the result to be mutable).

These transformations can also be applied to Sequence<T>, which is similar to Python's generators and allows for lazy evaluation. If you have a huge list and you want to process it lazily, you can call asSequence() on it.

There's a vast collection of functional programming-style operations available in the kotlin.collections package.

接收者

The signature of a member function or an extension function begins with a receiver: the type upon which the function can be invoked. For example, the signature of toString() is Any.() -> String - it can be called on any non-null object (the receiver), it takes no parameters, and it returns a String. It is possible to write a lambda function with such a signature - this is called a function literal with receiver, and is extremely useful for building DSLs.

A function literal with receiver is perhaps easiest to think of as an extension function in the form of a lambda expression. The declaration looks like an ordinary lambda expression; what makes it take a receiver is the context - it must be passed to a function that takes a function with receiver as a parameter, or assigned to a variable/property whose type is a function type with receiver. The only way to use a function with receiver is to invoke it on an instance of the receiver class, as if it were a member function or extension function. For example:

class Car(val horsepowers: Int)

val boast: Car.() -> String = { "I'm a car with $horsepowers HP!"}

val car = Car(120)
println(car.boast())

Inside a lambda expression with receiver, you can use this to refer to the receiver object (in this case, car). As usual, you can omit this if there are no naming conflicts, which is why we can simply say $horsepowers instead of ${this.horsepowers}. So beware that in Kotlin, this can have different meanings depending on the context: if used inside (possibly nested) lambda expressions with receivers, it refers to the receiver object of the innermost enclosing lambda expression with receiver. If you need to "break out" of the function literal and get the "original" this (the instance the member function you're inside is executing on), mention the containing class name after [email protected] - so if you're inside a function literal with receiver inside a member function of Car, use [email protected].

As with other function literals, if the function takes one parameter (other than the receiver object that it is invoked on), the single parameter is implicitly called it, unless you declare another name. If it takes more than one parameter, you must declare their names.

Here's a small DSL example for constructing tree structures:

class TreeNode(val name: String) {
    val children = mutableListOf<TreeNode>()

    fun node(name: String, initialize: (TreeNode.() -> Unit)? = null) {
        val child = TreeNode(name)
        children.add(child)
        if (initialize != null) {
            child.initialize()
        }
    }
}

fun tree(name: String, initialize: (TreeNode.() -> Unit)? = null): TreeNode {
    val root = TreeNode(name)
    if (initialize != null) {
        root.initialize()
    }
    return root
}

val t = tree("root") {
    node("math") {
        node("algebra")
        node("trigonometry")
    }
    node("science") {
        node("physics")
    }
}

The block after tree("root") is the first function literal with receiver, which will be passed to tree() as the initialize parameter. According to the parameter list of tree(), the receiver is of type TreeNode, and therefore, tree() can call initialize() on root. root then becomes this inside the scope of that lambda expression, so when we call node("math"), it implicitly says this.node("math"), where this refers to the same TreeNode as root. The next block is passed to TreeNode.node(), and is invoked on the first child of the root node, namely math, and inside it, this will refer to math.

If we had wanted to express the same thing in Python, it would have looked like this, and we would be hamstrung by the fact that lambda functions can only contain one expression, so we need explicit function definitions for everything but the oneliners:

class TreeNode:
    def __init__(self, name):
        self.name = name
        self.children = []

    def node(self, name, initialize=None):
        child = TreeNode(name)
        self.children.append(child)
        if initialize:
            initialize(child)

def tree(name, initialize=None):
    root = TreeNode(name)
    if initialize:
        initialize(root)
    return root

def init_root(root):
    root.node("math", init_math)
    root.node("science",
              lambda science: science.node("physics"))

def init_math(math):
    math.node("algebra")
    math.node("trigonometry")

t = tree("root", init_root)

The official docs also have a very cool example with a DSL for constructing HTML documents.

内联函数

There's a little bit of runtime overhead associated with lambda functions: they are really objects, so they must be instantiated, and (like other functions) calling them takes a little bit of time too. If we use the inline keyword on a function, we tell the compiler to inline both the function and its lambda parameters (if any) - that is, the compiler will copy the code of the function (and its lambda parameters) into every callsite, thus eliminating the overhead of both the lambda instantiation and the calling of the function and the lambdas. This will happen unconditionally, unlike in C and C++, where inline is more of a hint to the compiler. This will cause the size of the compiled code to grow, but it may be worth it for certain small but frequently-called functions.

inline fun time(action: () -> Unit): Long {
    val start = Instant.now().toEpochMilli()
    action()
    return Instant.now().toEpochMilli() - start
}

Now, if you do:

val t = time { println("Lots of code") }
println(t)

The compiler will generate something like this (except that start won't collide with any other identifiers with the same name):

val start = Instant.now().toEpochMilli()
println("Lots of code")
val t = Instant.now().toEpochMilli() - start
println(t)

In an inline function definition, you can use noinline in front of any function-typed parameter to prevent the lambda that will be passed to it from also being inlined.

不错的工具函数

run()let()with()

?. is nice if you want to call a function on something that might be null. But what if you want to call a function that takes a non-null parameter, but the value you want to pass for that parameter might be null? Try run(), which is an extension function on Any? that takes a lambda with receiver as a parameter and invokes it on the value that it's called on, and use ?. to call run() only if the object is non-null:

val result = maybeNull?.run { functionThatCanNotHandleNull(this) }

If maybeNull is null, the function won't be called, and result will be null; otherwise, it will be the return value of functionThatCanNotHandleNull(this), where this refers to maybeNull. You can chain run() calls with ?. - each one will be called on the previous result if it's not null:

val result = maybeNull
    ?.run { firstFunction(this) }
    ?.run { secondFunction(this) }

The first this refers to maybeNull, the second one refers to the result of firstFunction(), and result will be the result of secondFunction() (or null if maybeNull or any of the intermediate results were null).

A syntactic variation of run() is let(), which takes an ordinary function type instead of a function type with receiver, so the expression that might be null will be referred to as it instead of this.

Both run() and let() are also useful if you've got an expression that you need to use multiple times, but you don't care to come up with a variable name for it and make a null check:

val result = someExpression?.let {
   firstFunction(it)
   it.memberFunction() + it.memberProperty
}

Yet another version is with(), which you can also use to avoid coming up with a variable name for an expression, but only if you know that its result will be non-null:

val result = with(someExpression) {
   firstFunction(this)
   memberFunction() + memberProperty
}

In the last line, there's an implicit this. in front of both memberFunction() and memberProperty (if these exist on the type of someExpression). The return value is that of the last expression.

apply()also()

If you don't care about the return value from the function, but you want to make one or more calls involving something that might be null and then keep on using that value, try apply(), which returns the value it's called on. This is particularly useful if you want to work with many members of the object in question:

maybeNull?.apply {
    firstFunction(this)
    secondFunction(this)
    memberPropertyA = memberPropertyB + memberFunctionA()
}?.memberFunctionB()

Inside the apply block, this refers to maybeNull. There's an implicit this in front of memberPropertyA, memberPropertyB, and memberFunctionA (unless these don't exist on maybeNull, in which case they'll be looked for in the containing scopes). Afterwards, memberFunctionB() is also invoked on maybeNull.

If you find the this syntax to be confusing, you can use also instead, which takes ordinary lambdas:

maybeNull?.also {
    firstFunction(it)
    secondFunction(it)
    it.memberPropertyA = it.memberPropertyB + it.memberFunctionA()
}?.memberFunctionB()

takeIf()takeUnless()

If you want to use a value only if it satisfies a certain condition, try takeIf(), which returns the value it's called on if it satisfies the given predicate, and null otherwise. There's also takeUnless(), which inverts the logic. You can follow this with a ?. to perform an operation on the value only if it satisfies the predicate. Below, we compute the square of some expression, but only if the expression value is at least 42:

val result = someExpression.takeIf { it >= 42 } ?.let { it * it }

← 上一节:空安全 下一节:包与导入 →

本资料英文原文的作者是 Aasmund Eldhuset;其所有权属于可汗学院(Khan Academy),授权许可为 CC BY-NC-SA 3.0 US(署名-非商业-相同方式共享)。请注意,这并不是可汗学院官方产品的一部分。中文版由灰蓝天际译,遵循相同授权方式。