Найти тему

Функции расширения в Kotlin

1. Введение

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

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

Например, нам может понадобиться выполнить экранирование XML для строки. В стандартном Java-коде нам нужно будет написать метод, который может выполнять это, и вызвать его:

String escaped = escapeStringForXml(input);

Несмотря на то, что этот фрагмент написан на Kotlin, он может быть заменен на:

val escaped = input.escapeForXml()

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

2. Функции расширения стандартной библиотеки

Стандартная библиотека Kotlin поставляется с некоторыми расширительными функциями "из коробки".

2.1. Функции расширения, настраивающие контекст

Существуют некоторые универсальные расширения, которые могут быть применены ко всем типам в нашем приложении. Они могут использоваться для обеспечения выполнения кода в соответствующем контексте, а в некоторых случаях и для обеспечения того, чтобы переменная не была равна null.

Получается, что, скорее всего, мы используем расширения, не осознавая этого.

Одной из самых популярных, возможно, является функция let(), которая может быть вызвана для любого типа в Kotlin – давайте передадим ей функцию, которая будет выполняться при начальном значении:

val name = "Baeldung"
val uppercase = name
.let { n -> n.toUpperCase() }

Это похоже на метод map() из классов Optional или Stream – в этом случае мы передаем функцию, представляющую действие, которое преобразует заданную строку в ее представление в верхнем регистре.

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

Это прекрасно работает с оператором безопасного вызова:

val name = maybeGetName()
val uppercase = name?.let { n -> n.toUpperCase() }

В этом случае блок, переданный в let(), вычисляется только в том случае, если имя переменной не равно null. Это означает, что внутри блока значение n гарантированно будет ненулевым. Подробнее об этом здесь.

Существуют и другие альтернативы let(), которые также могут быть полезны, в зависимости от наших потребностей.

Расширение run() работает так же, как и let(), но в качестве этого значения внутри вызываемого блока предоставляется получатель:

val name = "Baeldung"
val uppercase = name.run { toUpperCase() }

функция apply() работает так же, как и функция run(), но возвращает получателя, а не значение из предоставленного блока.

Давайте воспользуемся преимуществами функции apply() для связанных с цепочкой вызовов:

val languages = mutableListOf<String>()
languages.apply {
add("Java")
add("Kotlin")
add("Groovy")
add("Python")
}.apply {
remove("Python")
}

Обратите внимание, что наш код становится более кратким и выразительным без необходимости явно использовать this или it.

Расширение also() работает так же, как и let(), но возвращает получатель таким же образом, как и apply().:

val languages = mutableListOf<String>()
languages.also { list ->
list.add("Java")
list.add("Kotlin")
list.add("Groovy")
}

Расширение takeIf() снабжено предикатом, действующим на получателя, и если этот предикат возвращает значение true, то он возвращает значение receiver или null в противном случае – это работает аналогично комбинации методов common map() и filter().:

val language = getLanguageUsed()
val coolLanguage = language.takeIf { l -> l == "Kotlin" }

Расширение take Unless() аналогично takeoff(), но с обратной логикой предикатов.

val language = getLanguageUsed()
val oldLanguage = language.takeUnless { l -> l == "Kotlin" }

2.2. Дополнительные функции для коллекций

Kotlin добавляет большое количество дополнительных функций к стандартным коллекциям Java, которые могут упростить работу с нашим кодом.

Эти методы расположены внутри _Collections.kt, _Ranges.kt и _Sequences.kt, а также _Arrays.kt, чтобы вместо них можно было применять эквивалентные методы к массивам. (Помните, что в Kotlin массивы можно рассматривать так же, как коллекции)

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

В дополнение к коллекциям, Kotlin добавляет значительное количество функций расширения к классу String, определяемому в _Strings.kt. Они позволяют нам обрабатывать строки так, как если бы они были коллекциями символов.

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

3. Написание наших дополнительных функций

Итак, что, если нам нужно расширить класс с помощью новой функциональности – либо из стандартной библиотеки Java или Kotlin, либо из зависимой библиотеки, которую мы используем?

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

Например:

fun String.escapeForXml() : String {
....
}

Это определит новую функцию под названием escapeForXml как расширение класса String, позволяющее нам вызывать ее, как описано выше.

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

fun String.escapeForXml() : String {
return this
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}

3.1. Написание универсальных функций расширения

Что, если мы хотим написать функцию расширения, которая, в общем, предназначена для применения к нескольким типам? Мы могли бы просто расширить любой тип, который является эквивалентом класса Object в Java, но есть способ получше.

Функции расширения могут быть применены как к универсальному приемнику, так и к конкретному:

fun <T> T.concatAsString(b: T) : String {
return this.toString() + b.toString()
}

Это может быть применено к любому типу, который соответствует общим требованиям, и внутри функции это значение является типобезопасным.

Например, используя приведенный выше пример:

5.concatAsString(10) // compiles
"5".concatAsString("10") // compiles
5.concatAsString("10") // doesn't compile

3.2. Написание функций расширения инфикса

Инфиксные методы полезны для написания кода в стиле DSL, поскольку они позволяют вызывать методы без точки или квадратных скобок:

infix fun Number.toPowerOf(exponent: Number): Double {
return Math.pow(this.toDouble(), exponent.toDouble())
}

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

3 toPowerOf 2 // 9
9 toPowerOf 0.5 // 3

3.3. Написание функций расширения оператора

Мы также могли бы написать операторную функцию в качестве расширения.

Операторные методы – это те, которые позволяют нам использовать сокращенное название оператора вместо полного имени метода - например, метод оператора plus может быть вызван с помощью оператора +:

operator fun List<Int>.times(by: Int): List<Int> {
return this.map { it * by }
}

Опять же, это работает так же, как и любой другой операторный метод:

listOf(1, 2, 3) * 4 // [4, 8, 12]

4. Вызов функции расширения Kotlin из Java

Давайте теперь посмотрим, как Java работает с функциями расширения Kotlin.

В общем, каждая функция расширения, которую мы определяем в Kotlin, доступна для использования в Java. Однако мы должны помнить, что метод infix по-прежнему должен вызываться с точкой и круглыми скобками. То же самое и с расширениями операторов — мы не можем использовать только символ плюс (+). Эти возможности доступны только в Kotlin.

Однако мы не можем вызывать некоторые стандартные методы библиотеки Kotlin в Java, такие как let или apply, потому что они помечены символом @InlineOnly.

4.1. Видимость пользовательской функции расширения в Java

Давайте воспользуемся одной из ранее определенных функций расширения — String.escapeXml(). Наш файл, содержащий метод расширения, называется StringUtil.kt.

Теперь, когда нам нужно вызвать метод расширения из Java, нам нужно использовать имя класса Stringutil.
Обратите внимание, что мы должны добавить суффикс Kt:

String xml = "<a>hi</a>";

String escapedXml = StringUtilKt.escapeForXml(xml);

assertEquals("&lt;a&gt;hi&lt;/a&gt;", escapedXml);

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

И, конечно, как и в Java, мы можем использовать статический импорт:

import static com.baeldung.kotlin.StringUtilKt.*;

4.2. Вызов встроенной функции расширения Kotlin

Kotlin помогает нам писать код проще и быстрее, предоставляя множество встроенных функций расширения. Например, существует метод String.capitalize(), который можно вызвать непосредственно из Java:

String name = "john";

String capitalizedName = StringsKt.capitalize(name);

assertEquals("John", capitalizedName);

Однако, мы не можем вызывать методы расширения, помеченные символом @InlineOnly, например, из Java:

inline fun <T, R> T.let(block: (T) -> R): R

4.3. Переименование сгенерированного статического класса Java

Мы уже знаем, что функция расширения Kotlin является статическим Java-методом. Давайте переименуем сгенерированный Java-класс с помощью аннотации @file:JvmName(имя: строка).

Это должно быть добавлено в начало файла:

@file:JvmName("Strings")
package com.baeldung.kotlin

fun String.escapeForXml() : String {
return this
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}

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

Strings.escapeForXml(xml);

Кроме того, мы все еще можем добавить статический импорт:

import static com.baeldung.kotlin.Strings.*;

5. Краткое содержание

Расширяющие функции – это полезные инструменты для расширения типов, которые уже существуют в системе, либо потому, что они не обладают необходимой нам функциональностью, либо просто для упрощения управления какой-либо конкретной областью кода.

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

Оригинал статьи: https://www.baeldung.com/kotlin/extension-methods

С подпиской рекламы не будет

Подключите Дзен Про за 159 ₽ в месяц