Kotlin передать функцию как параметр
Перейти к содержимому

Kotlin передать функцию как параметр

Kotlin передать функцию как параметр

Функции высокого порядка (high order function) — это функции, которые либо принимают функцию в качестве параметра, либо возвращают функцию, либо и то, и другое.

Функция как параметр функции

Чтобы функция могла принимать другую функцию через параметр, этот параметр должен представлять тип функции:

В данном случае функция displayMessage() через параметр mes принимает функцию типа () -> Unit , то есть такую функцию, которая не имеет параметров и ничего не возвращает.

При вызове этой функции мы можем передать этому параметру функцию, которая соответствует этому типу:

Рассмотрим пример параметра-функции, которая принимает параметры:

Здесь функция action принимает три параметра. Первые два параметра — значения типа Int. А третий параметр представляет функцию, которая имеет тип (Int, Int)-> Int , то есть принимает два числа и возвращает некоторое число.

В самой функции action вызываем эту параметр-функцию, передавая ей два числа, и полученный результат выводим на консоль.

При вызове функции action мы можем передать для ее третьего параметра конкретную функцию, которая соответствует этому параметру по типу:

Возвращение функции из функции

В более редких случаях может потребоваться возвратить функцию из другой функции. В этом случае для функции в качестве возвращаемого типа устанавливается тип другой функции. А в теле функции возвращается лямбда выражение. Например:

Здесь функция selectAction принимает один параметр — key, который представляет тип Int . В качестве возвращаемого типа у функции указан тип (Int, Int) -> Int . То есть selectAction будет возвращать некую функцию, которая принимает два параметра типа Int и возвращает объект типа Int.

В теле функции selectAction в зависимости от значения параметра key возвращается определенная функция, которая соответствует типу (Int, Int) -> Int .

Далее в функции main определяется переменная action1 хранит результат функции selectAction . Так как selectAction() возвращает функцию, то и переменная action1 будет хранить эту функцию. Затем через переменную action1 можно вызвать эту функцию.

Поскольку возвращаемая функция соответствует типу (Int, Int) -> Int , то при вызове в action1 необходимо передать два числа, и соответственно мы можем получить результат и вывести его на консоль.

Функциональный Kotlin. Во имя добра, радуги и всего такого

Сам по себе Kotlin очень мощный инструмент, но многие часто используют его не на полную мощность, превращая его в какую-то. Java 6. Попробую рассказать почему так делать не надо и как использовать функциональные фичи языка на полную.

Функции высшего порядка

Начну с них. И, заодно, расскажу что это вообще такое: когда функция принимает другую функцию как параметр или возвращает ее — это функция высшего порядка. Просто, не правда ли? Но как этим пользоваться?

То, что функции в Котлине могут получать и возвращать другие функции для нас должно означать то, что мы можем записать их в переменную. Выглядеть это будет как-то так:

Тогда, мы сможем передать ее, используя синтаксис вида:

Отлично, но что если мы хотим использовать функцию, которую мы определили нормальным способом? Или вообще передать метод? Что-ж, для этого тоже есть возможность — ссылка на функцию. Вот так мы можем проверить строку на пустоту:

Перейдем к более конкретному кейсу. Допустим, нам нужно распарсить строки определенным образом, но только в одном месте в программе. Очевидно, что логика будет повторяться. Что делать? Создадим для этого отдельный объект с одним методом? Или пропишем для каждого «ручками» прямо на месте?

Нет, копить технический долг ни к чему, лучше мы сделаем, например, что-то такое:

Функции области видимости

Теперь, когда мы разобрались с ФВП, перейдем к вещам, которыми, скорее всего, пользовались все. let , run , with , apply , и also . Знакомые слова? Надеюсь, но все же разберем их.

Сначала let и also . Они наиболее просты и понятны, потому что все что они делают внутри — это вызов block(this) . По сути, мы просто делаем вызов определенной нами «на месте» функции. Разница лишь в том, что они возвращают. also используется когда вы хотите продолжить работу с тем же объектом, у которого решили вызвать функцию и let , если вам нужно вернуть новый объект.

Теперь к run , with и apply :
run очень похож на let , apply на also , а with это почти то же самое что и run , просто receiver получаем разными способами. Чем они удобны и зачем нужны, если есть let и also ? Все просто, тут вместо it используется this , который вообще можно опустить и не писать.

И как же эффективно использовать все эти функции? Мы можем и в большинстве случаев даже должны писать все наши функции полностью в функциях областей видимости.

Так мы получим гарантии, что наши промежуточные значения живут ровно столько, сколько нужно, а код становится проще читать, поскольку все преобразования исходных данных теперь вытекают друг из друга, позволяя нам иметь в каждой подзадаче ровно столько информации, сколько нам надо для ее решения. Функциональная «изоляция» позволит в том числе упростить чтение и тестирование своего кода.

Вы же заметили слово inline в объявлении? А оно, кстати, очень важно. К сожалению, использование функций высшего порядка влечёт за собой снижение производительности, так как функция является объектом и происходит захват контекста замыканием, то есть функции становятся доступны переменные, объявленные вне её тела.

Разумное применение встроенных функций позволяет решить эту проблему. Но делать блоки встроенных функций слишком большими все равно не стоит.

Классы и Объекты

Все мы уже должны были понять, что плодить иерархии и создавать экземпляры классов там, где никакого специфического поведения у отдельных экземпляров нет — дурацкая идея (хотя есть и исключения)?

Это лишний раз усложняет структуру и мешает нам писать код из цепочек функций, потому что нам нужно сначала создать бесполезный экземпляр, и только потом мы сможем воспользоваться его функционалом в блоке области видимости:

Использование объекта позволило бы нам сделать наш код проще:

Как хорошо, когда нет ничего лишнего, да?

Но допустим мы все же вынуждены иметь дело с классом, у которого есть поведение, но экземпляр класса создавать не хотим? К счастью, и для этого найдется достаточно простое решение. Мы просто скинем всю статику, которая есть в объекте в companion object :

Теперь мы можем сделать так же, как и с объектом.

Factory методы

Для начала я приведу пример кода:

Лично мне он не нравится. Логическая цепочка тут нарушена, поскольку мы в первую очередь хотим понимать что создание экземпляра класса Other и вызов его метода нужны просто чтобы создать экземпляр класса Some, но запись не дает моментально этого понять.

Конечно, мы можем заинлайнить его:

Но у меня есть вариант получше. Было бы неплохо, если бы мы могли привязать логику, нужную только для создания объекта к. созданию объекта? И мы можем, для этого нам всего лишь нужно написать вот такой Factory-метод:

Теперь мы можем сделать так:

Или если класс Other тоже имеет свой фабричный метод:

Понимаю, на первый взгляд вообще стало хуже. но что если функций и необходимых объектов в иерархии станет сильно больше? Иерархия может быть достаточно большой, а данные могут требовать много преобразований. Не пытайтесь использовать это всегда и везде, но задумайтесь об этом на стадии рефакторинга.

Подводные камни функций-расширений

Предполагаю, что не знать о наличии в Котлине возможности писать функции-расширения и как это делать могут совсем новички, но на всякий случай я объясню. Это функция, с которой вы можете взаимодействовать так же, как с методом класса. Вот как это выглядит:

Или если хотите экзотики:

Первое, что стоит знать о таких функциях — это их приоритет исполнения. Вы не сможете перекрыть метод класса функцией-расширением. Хотя если вы напишете функцию вторым способом, IntelliJ IDEA будет показывать, как будто вы используете именно расширение — не обманывайтесь, я проверял.

Теперь, давайте поговорим о ссылках на функции-расширения. Если мы сделали ее через val то все просто и понятно, передаем ее просто как foo и ничего лишнего, работаем как с любой другой переменной. Но если через fun , появляются нюансы, на которые имеет смысл обратить внимание.

Давайте посмотрим на вот такой код:

В принципе, ничто не мешает нам так сделать, но есть одно «но», из-за которого я считаю это очень плохой практикой.

Все дело в том, что передать ссылку на эту функцию просто не получится. Никак. Даже внутри самого класса. Если так сделать, то Котлин просто не поймет, как обращаться к этой функции — как к методу класса Some или Other.

Однако, если мы, например, вынесем расширения в отдельный файлик под расширения определённого класса или вообще растащим их по пакетам на свое усмотрение — сможем легко обратиться к функции как Some::someExtention . Естественно, в данном случае класс или объект не важно — поведение будет одинаковым.

Кстати, если вы немножко копались в устройстве Котлина, то вот вам еще немного бесполезной информации. Допустим, вы решили непонятно зачем использовать вместо нормального способа передачи функции как параметра класс KFunction.

В этом случае у меня две новости — первая в том, что ваши вкусы весьма специфичны, а вторая, что в этом случае наша функция-расширение обрабатывается в качестве самой обычной функции, у которой первый параметр это экземпляр класса, который она расширяет.

High-order functions and lambdas

Kotlin functions are first-class, which means they can be stored in variables and data structures, and can be passed as arguments to and returned from other higher-order functions. You can perform any operations on functions that are possible for other non-function values.

To facilitate this, Kotlin, as a statically typed programming language, uses a family of function types to represent functions, and provides a set of specialized language constructs, such as lambda expressions.

Higher-order functions

A higher-order function is a function that takes functions as parameters, or returns a function.

A good example of a higher-order function is the functional programming idiom fold for collections. It takes an initial accumulator value and a combining function and builds its return value by consecutively combining the current accumulator value with each collection element, replacing the accumulator value each time:

In the code above, the combine parameter has the function type (R, T) -> R , so it accepts a function that takes two arguments of types R and T and returns a value of type R . It is invoked inside the for loop, and the return value is then assigned to accumulator .

To call fold , you need to pass an instance of the function type to it as an argument, and lambda expressions (described in more detail below) are widely used for this purpose at higher-order function call sites:

Function types

Kotlin uses function types, such as (Int) -> String , for declarations that deal with functions: val onClick: () -> Unit = . .

These types have a special notation that corresponds to the signatures of the functions — their parameters and return values:

All function types have a parenthesized list of parameter types and a return type: (A, B) -> C denotes a type that represents functions that take two arguments of types A and B and return a value of type C . The list of parameter types may be empty, as in () -> A . The Unit return type cannot be omitted.

Function types can optionally have an additional receiver type, which is specified before the dot in the notation: the type A.(B) -> C represents functions that can be called on a receiver object A with a parameter B and return a value C . Function literals with receiver are often used along with these types.

Suspending functions belong to a special kind of function type that have a suspend modifier in their notation, such as suspend () -> Unit or suspend A.(B) -> C .

The function type notation can optionally include names for the function parameters: (x: Int, y: Int) -> Point . These names can be used for documenting the meaning of the parameters.

To specify that a function type is nullable, use parentheses as follows: ((Int, Int) -> Int)? .

Function types can also be combined using parentheses: (Int) -> ((Int) -> Unit) .

The arrow notation is right-associative, (Int) -> (Int) -> Unit is equivalent to the previous example, but not to ((Int) -> (Int)) -> Unit .

You can also give a function type an alternative name by using a type alias:

Instantiating a function type

There are several ways to obtain an instance of a function type:

Use a code block within a function literal, in one of the following forms:

Function literals with receiver can be used as values of function types with receiver.

Use a callable reference to an existing declaration:

a top-level, local, member, or extension function: ::isOdd , String::toInt ,

a top-level, member, or extension property: List<Int>::size ,

These include bound callable references that point to a member of a particular instance: foo::toString .

Use instances of a custom class that implements a function type as an interface:

The compiler can infer the function types for variables if there is enough information:

Non-literal values of function types with and without a receiver are interchangeable, so the receiver can stand in for the first parameter, and vice versa. For instance, a value of type (A, B) -> C can be passed or assigned where a value of type A.(B) -> C is expected, and the other way around:

A function type with no receiver is inferred by default, even if a variable is initialized with a reference to an extension function. To alter that, specify the variable type explicitly.

Invoking a function type instance

A value of a function type can be invoked by using its invoke(. ) operator: f.invoke(x) or just f(x) .

If the value has a receiver type, the receiver object should be passed as the first argument. Another way to invoke a value of a function type with receiver is to prepend it with the receiver object, as if the value were an extension function: 1.foo(2) .

Inline functions

Sometimes it is beneficial to use inline functions, which provide flexible control flow, for higher-order functions.

Lambda expressions and anonymous functions

Lambda expressions and anonymous functions are function literals. Function literals are functions that are not declared but are passed immediately as an expression. Consider the following example:

The function max is a higher-order function, as it takes a function value as its second argument. This second argument is an expression that is itself a function, called a function literal, which is equivalent to the following named function:

Lambda expression syntax

The full syntactic form of lambda expressions is as follows:

A lambda expression is always surrounded by curly braces.

Parameter declarations in the full syntactic form go inside curly braces and have optional type annotations.

The body goes after the -> .

If the inferred return type of the lambda is not Unit , the last (or possibly single) expression inside the lambda body is treated as the return value.

If you leave all the optional annotations out, what’s left looks like this:

Passing trailing lambdas

According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses:

Such syntax is also known as trailing lambda.

If the lambda is the only argument in that call, the parentheses can be omitted entirely:

it: implicit name of a single parameter

It’s very common for a lambda expression to have only one parameter.

If the compiler can parse the signature without any parameters, the parameter does not need to be declared and -> can be omitted. The parameter will be implicitly declared under the name it :

Returning a value from a lambda expression

You can explicitly return a value from the lambda using the qualified return syntax. Otherwise, the value of the last expression is implicitly returned.

Therefore, the two following snippets are equivalent:

Underscore for unused variables

If the lambda parameter is unused, you can place an underscore instead of its name:

Destructuring in lambdas

Destructuring in lambdas is described as a part of destructuring declarations.

Anonymous functions

The lambda expression syntax above is missing one thing – the ability to specify the function’s return type. In most cases, this is unnecessary because the return type can be inferred automatically. However, if you do need to specify it explicitly, you can use an alternative syntax: an anonymous function.

An anonymous function looks very much like a regular function declaration, except its name is omitted. Its body can be either an expression (as shown above) or a block:

The parameters and the return type are specified in the same way as for regular functions, except the parameter types can be omitted if they can be inferred from the context:

The return type inference for anonymous functions works just like for normal functions: the return type is inferred automatically for anonymous functions with an expression body, but it has to be specified explicitly (or is assumed to be Unit ) for anonymous functions with a block body.

When passing anonymous functions as parameters, place them inside the parentheses. The shorthand syntax that allows you to leave the function outside the parentheses works only for lambda expressions.

Another difference between lambda expressions and anonymous functions is the behavior of non-local returns. A return statement without a label always returns from the function declared with the fun keyword. This means that a return inside a lambda expression will return from the enclosing function, whereas a return inside an anonymous function will return from the anonymous function itself.

Closures

A lambda expression or anonymous function (as well as a local function and an object expression) can access its closure, which includes the variables declared in the outer scope. The variables captured in the closure can be modified in the lambda:

Function literals with receiver

Function types with receiver, such as A.(B) -> C , can be instantiated with a special form of function literals – function literals with receiver.

As mentioned above, Kotlin provides the ability to call an instance of a function type with receiver while providing the receiver object.

Inside the body of the function literal, the receiver object passed to a call becomes an implicit this , so that you can access the members of that receiver object without any additional qualifiers, or access the receiver object using a this expression.

This behavior is similar to that of extension functions, which also allow you to access the members of the receiver object inside the function body.

Here is an example of a function literal with receiver along with its type, where plus is called on the receiver object:

The anonymous function syntax allows you to specify the receiver type of a function literal directly. This can be useful if you need to declare a variable of a function type with receiver, and then to use it later.

Lambda expressions can be used as function literals with receiver when the receiver type can be inferred from the context. One of the most important examples of their usage is type-safe builders:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *