Функции-расширения (Extension functions)
Java содержит множество классов с замечательными методами. Но разработчику всегда не хватает ещё одного метода, который ему нужен для собственного проекта. Функции-расширения позволяют создавать видимость внедрения в существующий класс нового метода. Это позволяет более элегантно решить проблему с классами Utils, которые создают разработчики для подобных целей. Кроме того, созданные функции будут выводиться в подсказках при наборе кода вместе с стандартными методами класса.
Рассмотрим пример на Java. Как узнать, является ли указанная дата субкотой (иногда люди используют слово «суббота»)? У класса Date нет соответствующего метода и мы можем определить субботу только по методу getDay(), который возвращает число 6 для субботы.
Создадим класс Utils.java и поместим свой метод.
static boolean isSaturday(Date date)
Вызываем метод по щелчку кнопки.
public void onClick(View view)
Теперь рассмотрим, как это можно сделать на Kotlin.
Добавим функцию в активность и вызовем её по щелчку кнопки.
fun Date.isSaturday(): Boolean < return getDay() == 6 >button_choose.setOnClickListener
Теперь, если смотреть на код, то создаётся ощущение, что isSaturday() является частью класса Date и мы вызываем его прямо из класса.
Предыдущий пример с функцией был написан в Java-стиле. В Kotlin можно заменить метод getDay() на свойство day.
fun Date.isSaturday(): Boolean
Впрочем и это не предел. Подобную функцию можно переписать в одну строку.
fun Date.isSaturday(): Boolean = day == 6
Если у вас будет несколько подобных функций-расширений, то удобнее их хранить в отдельном файле (не обязательно в классе). Создадим дополнительный пакет utils с файлом Utils.kt.
package ru.alexanderklimov.counter.utils import java.util.* fun Date.isSaturday() = day == 6 // другие функции-расширения
В активности при вызове функции следует импортировать её (возможно это сделает сама студия автоматически).
import ru.alexanderklimov.counter.utils.isSaturday // код для кнопки останется прежним
Можно переопределить имя функции при помощи as:
import ru.alexanderklimov.counter.utils.isSaturday as caturday val saturday = now.caturday()
Другие примеры из различных докладов и презентаций.
Функция для получения последнего символа из строки.
fun String.lastChar() = get(length - 1) // Применяем к строке val cat = "Мурзик" val c: Char = cat.lastChar() textview_info.text = cat.lastChar().toString() // длинный вариант
Знакомый нам пример. Мы добавляем в класс Context новую функцию toast(), которая вызывает метод Toast.makeText() с использованием параметров по умолчанию.
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) < Toast.makeText(this, message, duration).show() >// вызываем в активности toast("Hello Kitty")
JetBrains добавила свой набор расширений для различных классов Java и Android. Самый показательный случай — набор расширений для коллекций — функции filter(), map(), count() и т.д. в функциональном стиле, даже используя старую Java 6.
Помните, что функции-расширения класса не могут обращаться к его членам с модификаторами protected или private.
Ключевое слово inflix
Если ваша функция-расширение использует только один аргумент, то можно вызвать функцию через ключевое слово infix.
Создадим функцию-расширение для Int, которое будет сравнивать число с аргументом, чтобы узнать, больше оно или меньше.
fun Int.isGreater(value: Int): Boolean < return this >value > // вызываем функцию 3.isGreater(9) // false
Перепишем функцию с ключевым словом infix.
infix fun Int.isGreater(value: Int): Boolean < return this >value > // вызываем функцию 12 isGreater 9 // true
Как реализованы функции расширения в kotlin
Функции расширения (extension function) позволяют добавить функционал к уже определенным типам. При этом типы могут быть определены где-то в другом месте, например, в стандартной библиотеке.
Функция расширения определяется следующим образом:
fun тип.имя_функции(параметры) : возвращаемый_тип
По большому счету определение аналогично определению обычной функции за тем исключением, что после слова fun идет название типа, для которого определяется функция, и через точку название функции.
Определим пару функций расширения к стандартным типам Int и String:
fun main() < val hello: String = "hello world" println(hello.wordCount('l')) // 3 println(hello.wordCount('o')) // 2 println(4.square()) // 16 println(6.square()) // 36 >fun String.wordCount(c: Char) : Int < var count = 0 for(n in this)< if(n == c) count++ >return count > fun Int.square(): Int
Для типа Int определена функция возведения в квадрат. В каждой функции расширения через ключевое слово this мы можем ссылаться на текущий объект того типа, для которого создается функция. Например, в функции:
fun Int.square(): Int
Через this обращаемся к тому объекту, для которого будет вызывться функция. И затем вы можем вызвать ее следующим образом:
4.square() // 16
Для типа String определена функция wordCount, которая подсчитывает, сколько встречается определенный символ в строке.
Следует учитывать, что в функциях расширения мы можем обращаться к любым общедоступным свойствам и методам объекта, однако не можем обращаться к свойствам и методам с модификаторами private и protected .
Также следует учитывать, что функции расширения не переопределяют функции, которые уже определены в классе. Если функция расширения имеет ту же сигнатуру, что и уже имеющаяся функция класса, то компилятор просто будет игнорировать подобную функцию расширения.
Расширения (extensions)
Kotlin позволяет расширять класс путём добавления нового функционала без необходимости наследования от такого класса и использования паттернов, таких как Decorator. Это реализовано с помощью специальных выражений, называемых расширения.
Например, вы можете написать новые функции для класса из сторонней библиотеки, которую вы не можете изменить. Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса. Этот механизм называется функцией расширения. Существуют также свойства расширения, которые позволяют определять новые свойства для существующих классов.
Функции-расширения
Для того чтобы объявить функцию-расширение, укажите в качестве префикса расширяемый тип, то есть тип, который мы расширяем. Следующий пример добавляет функцию swap к MutableList :
fun MutableList.swap(index1: Int, index2: Int) < val tmp = this[index1] // 'this' даёт ссылку на список this[index1] = this[index2] this[index2] = tmp >
Ключевое слово this внутри функции-расширения соотносится с объектом расширяемого типа (этот тип ставится перед точкой). Теперь мы можем вызывать такую функцию в любом MutableList .
val list = mutableListOf(1, 2, 3) list.swap(0, 2) // 'this' внутри 'swap()' будет содержать значение 'list'
`, and you can make it generic: —>
Следующая функция имеет смысл для любого MutableList , и вы можете сделать её обобщённой:
fun MutableList.swap(index1: Int, index2: Int) < val tmp = this[index1] // 'this' относится к списку this[index1] = this[index2] this[index2] = tmp >
Вам нужно объявлять обобщённый тип-параметр перед именем функции для того, чтобы он был доступен в получаемом типе-выражении. См. Обобщения.
Расширения вычисляются статически
Расширения на самом деле не проводят никаких модификаций с классами, которые они расширяют. Объявляя расширение, вы создаёте новую функцию, а не новый член класса. Такие функции могут быть вызваны через точку, применимо к конкретному типу.
Расширения имеют статическую диспетчеризацию: это значит, что вызванная функция-расширение определяется типом её выражения, из которого она вызвана, а не типом выражения, вычисленным в ходе выполнения программы, как при вызове виртуальных функций.
open class Shape class Rectangle: Shape() fun Shape.getName() = "Shape" fun Rectangle.getName() = "Rectangle" fun printClassName(s: Shape) < println(s.getName()) >printClassName(Rectangle())
Этот пример выведет нам Shape на экран потому, что вызванная функция-расширение зависит только от объявленного параметризованного типа s , который является Shape классом.
Если в классе есть и функция-член, и функция-расширение с тем же возвращаемым типом, таким же именем и применяется с такими же аргументами, то функция-член имеет более высокий приоритет.
class Example < fun printFunctionType() < println("Class method") >> fun Example.printFunctionType() < println("Extension function") >Example().printFunctionType()
Этот код выведет Class method.
Однако для функций-расширений совершенно нормально перегружать функции-члены, которые имеют такое же имя, но другую сигнатуру.
class Example < fun printFunctionType() < println("Class method") >> fun Example.printFunctionType(i: Int) < println("Extension function #$i") >Example().printFunctionType(1)
Обращение к Example().printFunctionType(1) выведет на экран надпись Extension function #1.
Расширение null-допустимых типов
Обратите внимание, что расширения могут быть объявлены для null-допустимых типов. Такие расширения могут ссылаться на переменные объекта, даже если значение переменной равно null и есть возможность провести проверку this == null внутри тела функции.
Благодаря этому метод toString() в Kotlin вызывается без проверки на null : она проходит внутри функции-расширения.
fun Any?.toString(): String < if (this == null) return "null" // после проверки на null, `this` автоматически приводится к не-null типу, // поэтому toString() обращается (ориг.: resolves) к функции-члену класса Any return toString() >
Свойства-расширения
Аналогично функциям, Kotlin поддерживает расширения свойств.
val List.lastIndex: Int get() = size - 1
Since extensions do not actually insert members into classes, there’s no efficient way for an extension > property to have a [backing field](properties.md#backing-fields). This is why _initializers are not allowed for > extension properties_. Their behavior can only be defined by explicitly providing getters/setters. —>
Поскольку расширения фактически не добавляют никаких членов к классам, свойство-расширение не может иметь теневого поля. Вот почему запрещено использовать инициализаторы для свойств-расширений. Их поведение может быть определено только явным образом, с указанием геттеров/сеттеров.
val House.number = 1 // ошибка: запрещено инициализировать значения // в свойствах-расширениях
Расширения для вспомогательных объектов (ориг.: companion object extensions)
Если у класса есть вспомогательный объект, вы также можете определить функции и свойства расширения для такого объекта. Как и обычные члены вспомогательного объекта, их можно вызывать, используя в качестве определителя только имя класса.
class MyClass < companion object < >// называется "Companion" > fun MyClass.Companion.printCompanion()
Область видимости расширений
В большинстве случаев вы определяете расширения на верхнем уровне, непосредственно в разделе пакетов.
package org.example.declarations fun List.getLongestString() < /*. */>
Для того, чтобы использовать такое расширение вне пакета, в котором оно было объявлено, импортируйте его на месте вызова.
package org.example.usage import org.example.declarations.getLongestString fun main()
См. Импорт для более подробной информации.
Объявление расширений в качестве членов класса
Внутри класса вы можете объявить расширение для другого класса. Внутри такого объявления существует несколько неявных объектов-приёмников (ориг.: implicit receivers), доступ к членам которых может быть произведён без квалификатора. Экземпляр класса, в котором расширение объявлено, называется диспетчером приёмников (ориг.: dispatch receiver), а экземпляр класса, для которого вызывается расширение, называется приёмником расширения (ориг.: extension receiver).
class Host(val hostname: String) < fun printHostname() < print(hostname) >> class Connection(val host: Host, val port: Int) < fun printPort() < print(port) >fun Host.printConnectionString() < printHostname() // вызывает Host.printHostname() print(":") printPort() // вызывает Connection.printPort() >fun connect() < /*. */ host.printConnectionString() // вызов функции-расширения >> fun main() < Connection(Host("kotl.in"), 443).connect() // Host("kotl.in").printConnectionString() // ошибка, функция расширения недоступна вне подключения >
В случае конфликта имён между членами классов диспетчера приёмников и приёмников расширения, приоритет имеет приёмник расширения. Чтобы обратиться к члену класса диспетчера приёмников, можно использовать синтаксис this с квалификатором.
class Connection < fun Host.getConnectionString() < toString() // вызывает Host.toString() this@Connection.toString() // вызывает Connection.toString() >>
Расширения, объявленные как члены класса, могут иметь модификатор видимости open и быть переопределены в унаследованных классах. Это означает, что диспечеризация таких функций является виртуальной по отношению к типу диспетчера приёмников, но статической по отношению к типам приёмников расширения.
open class Base < >class Derived : Base() < >open class BaseCaller < open fun Base.printFunctionInfo() < println("Base extension function in BaseCaller") >open fun Derived.printFunctionInfo() < println("Derived extension function in BaseCaller") >fun call(b: Base) < b.printFunctionInfo() // вызов функции расширения >> class DerivedCaller: BaseCaller() < override fun Base.printFunctionInfo() < println("Base extension function in DerivedCaller") >override fun Derived.printFunctionInfo() < println("Derived extension function in DerivedCaller") >> fun main() < BaseCaller().call(Base()) // "Base extension function in BaseCaller" DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - приемник отправки является виртуальным DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - приемник расширения является статическим >
Примечание о видимости
Расширения используют те же модификаторы видимости как и обычные функции, объявленные в той же области видимости. Например:
- Расширение, объявленное на верхнем уровне файла, имеет доступ к другим private объявлениям верхнего уровня в том же файле;
- Если расширение объявлено вне своего типа приёмника, оно не может получить доступ к private или protected членам приёмника.
© 2015—2023 Open Source Community
Расширения в Kotlin. Опасный атавизм или полезный инструмент?

Kotlin — еще молодой язык, но уже стремительно ворвался в нашу жизнь. Из-за этого не всегда понятно, каким образом правильно реализовать тот или иной функционал и какие best practice применять.
Особенно тяжело обстоит дело с возможностями языка, которых нет в Java. Одним из таких камней преткновения оказались расширения.
Это удобный инструмент, который делает код более читаемым, практически ничего не требуя взамен. Но в то же время знаю как минимум одного человека, который если и не считает расширения злом, то точно относится к ним скептически. Ниже я хотел бы обсудить особенности этого механизма, которые могут вызвать споры и недопонимание.
Расширения на DTO — нарушение шаблона Data Transfer Object
Например, есть класс User
class User(val name: String, val age: Int, val sex: String)
Вполне себе DTO! Далее в коде в нескольких местах нужна проверка, является ли пользователь совершеннолетним. Самый простой вариант — во всех местах сделать условие
if (user.age >= 18)
Но, поскольку таких мест может быть сколь угодно много, разумно вынести эту проверку в метод.
Тут видится три варианта:
- Функция fun isAdult(user: User) — из таких функций обычно состоят утилитные классы.
- Поместить функцию isAdult внутрь класса User
class User(val name: String, val age: Int, val sex: String) < fun isAdult() = age >= 18 >
Третий вариант не нарушает ни принципов ООП, ни шаблонов, но приходится каждый раз создавать обертку, если хотим использовать подобные функции. Такой вариант тоже не очень нравится. В итоге получается, что все равно придется идти на жертвы.
На мой взгляд, проще пожертвовать шаблоном DTO. Во-первых, я не нашел ни одного объяснения, почему нельзя делать функции (кроме геттеров и сеттеров) в DTO. А во-вторых, просто по смыслу подобный код удобно иметь рядом с данными, которыми оперируем.
Но не всегда есть возможность поместить такой код внутрь DTO-шек, так как не всегда у разработчика есть возможность редактировать классы, с которыми он работает. Например, это могут быть классы, генерируемые из xsd. К тому же для кого-то может быть непривычно и некомфортно писать подобный код в Data-классах. Kotlin предлагает решение для подобных ситуаций в виде функций и полей расширений:
fun User.isAdult() = age >= 18
Этот код можно использовать так, как если бы он был объявлен внутри класса User:
if(user.isAdult())
В итоге получается достаточно аккуратное решение, которое с наименьшими компромиссами удовлетворяет нашим потребностям. Если говорить о том, что нарушается шаблон DTO, то хочется напомнить, что в Java это будет обычный статический метод вида:
public static final boolean isAdult(@NotNull User receiver)
Как видим, формально даже шаблон не нарушается. Использование этой функции выглядит так, как если бы она была объявлена в User и Idea будет предлагать ее при автодополнении. Это очень удобно.
Расширения специфичны. Об их существовании можно не знать и путать методы и поля сущности с расширениями
Идея в том, что разработчик пришел на проект, а вокруг код, реализованный в расширениях, и непонятно, какой метод оригинальный, а какой — метод расширения.
Это не проблема, так как Idea помогает разработчику в этом вопросе и подсвечивает такие функции. Хотя справедливости ради надо сказать, что различие лучше заметно при теме Darcula. Если ее сменить на Light, все становится менее очевидно и расширение отличается лишь курсивным шрифтом.
Ниже видим пример вызова двух методов: isAdult — метод расширения, isMale — обычный метод внутри класса User. Скриншот слева — тема Darcula, справа — обычная Light тема.


Несколько хуже дела обстоят с полями. Если, например, решим реализовать isAdult как поле-расширение, то отличить его от обычного поля можно будет только по типу шрифта. В данном примере name — обычное поле. Поле-расширение выдает только курсивный шрифт.


Среда разработки Idea помогает определить, какой метод является расширением, а какой — оригиналом при автодополнении. Это удобно.

Аналогично обстоят дела с полями.

«for User in » означает, что это — расширение.
Плюс сам факт, что Idea «привязывает» расширение к расширяемой сущности, очень сильно помогает при разработке, так как методы и поля расширения предлагаются при автодополнении.
Расширения разбросаны по всему проекту, образуя помойку
У нас на проектах такой проблемы нет, так как мы не бросаем расширения на произвол и выносим код с public-расширениями в отдельные файлы или пакеты.
Например, функция isAdult из примера выше могла бы оказаться в файле User пакета extensions. Если пакета недостаточно и хочется точно не путать, где класс, а где файл с функциями, можно его назвать, например, _User.kt. Так сделали разработчики из JetBrains для коллекций. Или, если совесть запрещает начинать файл с подчеркивания, можно назвать user.kt. На самом деле нет разницы, каким из способов пользоваться, главное, чтобы было единообразие, которого придерживалась бы вся команда.
Создатели языка при разработке методов расширений для коллекций поместили их в файл _Collections.kt.
Вообще это вопрос организации кода, а не проблема расширений. Статические функции в Java, да и не только статические, можно разбросать не менее беспорядочно, чем расширения.
Не замокать функции расширения при юнит-тестировании
На мой взгляд, в моканье функций расширений нужды нет, как нет нужды и в моканье статических методов. В функции расширения следует помещать логику работы с уже имеющимися данными. Например, в случае с функцией isAdult для класса User все необходимое есть в isAdult. Ничего мокать не нужно.
Рассмотрим немного более сложный пример. Есть некий компонент, который служит для получения пользователей из внешней системы, — UserComponent. Метод получения пользователей называется getUsers. Предположим, что появилась необходимость получать всех активных пользователей и решили добавить логику фильтрации в виде функции — расширения. В итоге получили функцию:
fun UserComponent.getActiveUsers(): List = this.getUsers().filter
Может показаться, что вот она — ситуация, когда нужен mock для расширения. Но если вспомнить, что getActiveUsers — просто статический метод, оказывается, что mock не нужен. Мокать следует методы и функции, которые вызываются в расширении, и не более того.
Есть вероятность перекрытия функции расширения одноименной функцией, размещенной внутри расширенного класса
Данный кейс рассмотрим на примере из первого пункта. Допустим, есть функция расширение isAdult, которая проверяет, является ли пользователь совершеннолетним:
fun User.isAdult() = age >= 18
После этого реализуем одноименную функцию внутри User:
class User(val name: String, val age: Int, val sex: String)< fun isAdult() = age >= 21 >
При вызове user.isAdult() будет вызвана функция из класса, несмотря на то что есть одноименная и подходящая функция расширения. Такой кейс может запутать, так как пользователи, не знающие о функции, объявленной внутри класса, будут ожидать выполнения функции расширения. Это неприятная ситуация, которая может иметь крайне серьезные последствия. В данном случае речь идет не о возможных неудобствах ревью или нарушении шаблона, а о потенциально ошибочном поведении кода.
Описанная выше ситуация показывает, что при использовании функций расширений могут возникнуть реальные проблемы.
Чтобы их избежать, следует не забывать максимально покрывать функции расширения юнит-тестами. В самом страшном случае при не упавших тестах окажется две функции, работающие одинаково. Одна — расширение, а другая в самом классе. Если тесты упадут, это привлечет внимание к тому, что одна функция перекрывает другую.
Расширение привязано к классу, а не к объекту, и это может вызвать путаницу
Для примера рассмотрим класс User из первого пункта. Сделаем его open и создадим его наследника Student:
class Student(name: String, age: Int, sex: String): User(name, age, sex)
Определим функцию расширения для Student, которая тоже будет определять, является студент совершеннолетним или нет. Только для студента изменим условие:
fun Student.isAdult() = this.age >= 16
И теперь напишем следующий код:
val user: User = Student("Петя", 17, "M")
Что же вернет user.isAdult())?
Казалось бы, объект типа Student и функция должна вернуть true. Но все не так просто. Расширения привязаны к классу, а не к объекту, и результат будет false.
В этом нет ничего странного, если вспомнить, что расширения — это статические методы, а расширяемая сущность — первый параметр в этом методе. Это еще один момент, о котором следует помнить при использовании этого механизма. Иначе можно получить неприятный и неожиданный эффект.
Вместо вывода
Эти спорные моменты не кажутся опасными, если помнить, что говорим расширение — подразумеваем статический метод. Плюс покрытие такого функционала юнит-тестами поможет минимизировать возможную путаницу, связанную со статической природой расширений.
На мой взгляд, расширения являются мощным и удобным инструментом, который позволяет улучшить качество и читабельность кода, практически ничего не требуя взамен. Вот почему я их люблю:
- Расширения позволяют писать логику, специфичную для контекста расширяемого класса. Благодаря этому поля и методы расширения читаются так, будто они всегда присутствовали в расширенной сущности, что, в свою очередь, улучшает верхнеуровневое понимание кода. В Java, увы, так сделать нельзя. Причем расширения имеют те же модификаторы доступа, что и обычные функции. Это позволяет писать подобный код с той областью видимости, которая действительно необходима для конкретной функции.
- Удобно использовать функции расширения для маппингов, которых достаточно много приходится видеть при решении повседневных задач. Например, в проекте есть класс UserFromExternalSystem, который используется при вызове внешней системы, и было бы здорово вынести маппинг в функцию расширения, забыть о нем и использовать так, будто он изначально был в User.
callExternalSystem(user.getUserFromExternalSystem())
Конечно, то же самое можно сделать обычным методом, но такой вариант менее читабелен:
callExternalSystem(getUserFromExternalSystem(user))
или такой вариант:
val externalUser = getUserFromExternalSystem(user) callExternalSystem(externalUser)
Ниже приведены ссылки на материалы, которые использовались для подготовки этой статьи:
- proandroiddev.com/kotlin-extension-functions-more-than-sugar-1f04ca7189ff — отсюда взяты интересные мысли про то, что, используя расширения, мы ближе работаем с контекстом.
- www.nikialeksey.com/2017/11/14/kotlin-is-bad.html — тут автор выступает против расширений и приводит интересный пример, который рассматривается в одном из пунктов выше.
- medium.com/@elizarov/i-do-not-see-much-reason-to-mock-extension-functions-7f24d88a188a — мнение Романа Елизарова насчет моканья методов расширений.