Методы расширения в Котлине

Методы расширения в Котлине

1. Вступление

Kotlin вводит концепцию методов расширения -a handy way of extending existing classes with a new functionality without using inheritance or any forms of the Decorator pattern - после определения расширения. мы можем по существу использовать это - поскольку это было частью оригинального API.

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

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

String escaped = escapeStringForXml(input);

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

val escaped = input.escapeForXml()

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

2. Стандартные методы расширения библиотеки

Стандартная библиотека Kotlin поставляется с некоторыми методами расширения.

2.1. Методы расширения, регулирующие контекст

Some generic extensions exist and can be applied to all types in our application. Их можно использовать для обеспечения того, чтобы код запускался в соответствующем контексте, а в некоторых случаях чтобы гарантировать, что переменная не имеет значения NULL.

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

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

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

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

The variable name is known as the receiver of the call, потому что это переменная, на которую действует метод расширения.

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

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

In this case, the block passed to let() is only evaluated if the variable name was non-null. Это означает, что внутри блока значениеn гарантированно ненулевое. Подробнее об этомhere.

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

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

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

apply() works the same as run(), but it returns a receiver instead of returning the value from the provided block.

Давайте воспользуемсяapply() для объединения связанных вызовов:

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

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

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

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

The takeIf() extension is provided with a predicate acting on the receiver, and if this predicate returns true then it returns the receiver илиnull в противном случае - это работает аналогично комбинации общих методов map() иfilter():

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

РасширениеThe takeUnless() такое же, какtakeIf(), но с обратной логикой предиката.

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

2.2. Методы расширения для коллекций

Kotlin adds a large number of extension methods to the standard Java Collections which can make our code easier to work with.

Эти методы расположены внутриCollections.kt,_ Ranges.kt_ иSequences.kt_, as well as Arrays.kt_ для эквивалентных методов, применяемых вместо этого кArrays. (Помните, что в KotlinArrays можно рассматривать так же, какCollections)

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

In addition to Collections, Kotlin adds a significant number of extension methods to the String class - определяется вStrings.kt_. These allow us to treat Strings as if they were collections of characters .__

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

3. Написание наших методов расширения

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

Extension methods are written as any other method, но класс получателя предоставляется как часть имени функции, разделенная точкой.

Например:

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

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

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

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

3.1. Написание общих методов расширения

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

Extension methods can be applied to a generic receiver as well as a concrete one:

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

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

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

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

3.2. Написание методов расширения Infix

Методы Infix полезны для написания кода в стиле 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. Написание методов расширения оператора

Мы также можем написать операторный метод как расширение.

Operator methods are ones that allow us to take advantage of the operator shorthand instead of the full method name - например, метод оператораplus может быть вызван с помощью оператора+:

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

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

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

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

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

In general, every extension method we define in Kotlin is available for us to use in Java. Однако мы должны помнить, что методinfix по-прежнему нужно вызывать с точкой и круглыми скобками. То же самое и с расширениями операторов - мы не можем использовать только знак плюса (+). Эти возможности доступны только вKotlin.

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

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

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

Теперь, когда нам нужно вызвать метод расширения из Java, нам нужно использовать имя классаStringUtilKt. Note that we have to add the Kt suffix:

String xml = "hi";

String escapedXml = StringUtilKt.escapeForXml(xml);

assertEquals("hi", escapedXml);

Обратите внимание на первый параметрescapeForXml. Этот дополнительный аргумент является типом получателя функции расширения. Kotlin with top-level extension function is a pure Java class with a static method. , поэтому ему нужно каким-то образом передать исходныйString.

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

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

4.2. Вызов встроенного метода расширения Kotlin

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

String name = "john";

String capitalizedName = StringsKt.capitalize(name);

assertEquals("John", capitalizedName);

Однакоwe can’t call extension methods marked with @InlineOnly from Java, for example:

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

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

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

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

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

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

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

Strings.escapeForXml(xml);

Также мы можем добавить статический импорт:

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

5. Резюме

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

Мы видели здесь несколько методов расширения, готовых к использованию в системе. Кроме того, мы исследовали различные возможности методов расширения. Некоторые примеры этой функциональности можно найти вover on GitHub.