В преддверии старта курса "Kotlin Backend Developer" приглашаем будущих студентов и всех желающих посмотреть открытый урок на тему "Пересмотр «12 факторов»: создаём современный микросервис на Kotlin".
Также делимся с вами традиционным переводом полезного материала.
Терминология Kotlin: функции-расширения и свойства-расширения
Когда вы использовали какой-либо API-интерфейс, хотелось ли вам добавить в него новые функции или свойства?
Для решения этой задачи вы можете использовать наследование (создать новый класс на базе существующего) или функцию, которая получает в качестве входного параметра экземпляр класса. В языке программирования Java эта задача обычно решается с помощью класса Utils
, но он не виден при использовании функции автозавершения кода, что затрудняет поиск и делает использование этого класса менее интуитивно понятным. Оба этих подхода можно использовать для решения нашей задачи, но ни один из них не дает понятный и хорошо читаемый код.
К счастью, на помощь приходит Kotlin с функциями-расширениями и свойствами-расширениями. Они позволяют добавлять в класс нужный функционал без необходимости использовать наследование или создавать функцию, принимающую экземпляр класса в качестве параметра. В Android Studio эти расширения видны при использовании функции автозавершения кода, в отличие от соответствующего аналога в языке Java. Расширения можно использовать в сторонних библиотеках, Android SDK или пользовательских классах.
Читайте дальше, если хотите узнать, как повысить читаемость вашего кода с помощью расширений!
Использование функций-расширений
Представим, что у вас есть класс Dog
, описывающий собаку, у которой есть имя, порода и возраст.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
data class Dog(val name: String, val breed: String, val age: Int)
Допустим, некий приют для животных хочет расширить класс Dog
, чтобы в нем была функция, которая печатает информацию о собаке, если кто-то захочет забрать ее себе. Для этого мы реализуем функцию-расширение, которая объявляется как обычная функция, но с одной особенностью: перед именем функции добавляется имя расширяемого класса с точкой. В коде функции вы можете использовать служебное слово this
для обращения к объекту-получателю, и у вас есть доступ ко всем членам класса-получателя в пределах функции.
Вы можете вызвать функцию printDogInformation()
так же, как вы вызываете любую другую функцию в классе Dog
.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
fun main() {
val dog = Dog("Jen", "Pomeranian", 13)
dog.printDogInformation()
}
Вызов функций-расширений из кода на языке Java
Функции-расширения не являются частью расширяемого класса, поэтому при попытке вызвать их из Java мы не найдем их среди методов расширяемого класса. Как мы увидим позже, расширения декомпилируются в статические методы файла, в котором вы их определили, и получают в качестве входного параметра экземпляр расширяемого класса. Вот как бы выглядел вызов функции-расширения printDogInformation()
из Java:
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
DogExtensionKt.printDogInformation(dog);
Функции-расширения для типов, допускающих неопределенное значение
Расширения можно также использовать для работы с типами, допускающими неопределенное значение (nullable). Вместо того чтобы делать проверку на null
перед вызовом функции-расширения, мы можем создать функцию-расширение для nullable-типа и реализовать проверку на null
в коде этой функции. Вот так будет выглядеть функция printInformation()
, использующая тип, допускающий неопределенное значение.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
fun Dog?.printInformation() {
if (this == null){
println("No dog found")
return
}
println("Meet ${this.name} a ${this.age} year old ${this.breed}")
}
Как видите, не нужно делать проверку на null перед вызовом функции printInformation()
.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
fun main() {
val dog : Dog? = null
dog.printInformation() // prints "No dog found"
}
Использование свойств-расширений
Представим, что нашему приюту для животных также нужно знать, подходит ли собака по возрасту для передачи в новую семью. Для этого мы реализуем свойство-расширение isReadyToAdopt, которое будет показывать, превышает ли возраст собаки 1 год.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
val Dog.isReadyToAdopt: Boolean
get() = this.age > 1
Вы можете обратиться к этому свойству-расширению так же, как вы обращаетесь к любому другому свойству в классе Dog
.
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
fun main() {
val dog1 = Dog("Jen", "Pomeranian", 13)
if(dog1.isReadyToAdopt){
print("${dog1.name} is ready to be adopted")
}
}
Переопределение функций-расширений
Невозможно переопределить существующую функцию-член класса. Если определить функцию-расширение с такой же сигнатурой, что и у существующей функции-члена класса, то всегда будет вызываться функция-член, так как то, какая именно функция вызывается, зависит от объявленного статического типа переменной, а не от типа значения данной переменной во время выполнения кода. Например, нельзя расширить функцию toUppercase()
, применяемую к строковому типу (String), но можно расширить функцию convertToUppercase()
.
Следствие такого поведения можно увидеть, если попытаться расширить библиотечный тип, которым вы не владеете, когда владелец библиотеки добавил в библиотеку метод с той же сигнатурой, что и у вашего расширения. В этом случае будет вызываться библиотечное расширение, а вы получите только информацию о том, что ваша функция-расширение стала неиспользуемым методом.
Внутреннее устройство расширений
Мы можем декомпилировать функцию printDogInformation()
в Android Studio. Для этого нужно выбрать в меню пункт Tools/Kotlin/Show Kotlin Bytecode (Инструменты/Kotlin/Показать байт-код Kotlin) и нажать кнопку Decompile (Декомпиляция). В декомпилированном виде метод printDogInformation()
будет выглядеть так:
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
public static final void printDogInformation(@NotNull Dog $this$printDogInformation) {
Intrinsics.checkParameterIsNotNull($this$printDogInformation, "$this$printDogInformation");
String var1 = "Meet " + $this$printDogInformation.getName() + ", a " + $this$printDogInformation.getAge() + " year old " + $this$printDogInformation.getBreed();
boolean var2 = false;
System.out.println(var1);
}
В реальности функции-расширения являются обычными статическими функциями, которым в качестве входного параметра передается экземпляр класса-получателя. Они не имеют никакой другой связи с классами-получателями. Именно поэтому отсутствуют резервные поля — такие функции на самом деле не добавляют члены в класс.
Заключение
В целом расширения являются полезным инструментом, который следует использовать внимательно. Используя их, следуйте указанным ниже рекомендациям, и ваш код станет более понятным и читаемым.
Что следует помнить:
Расширения преобразуются в статические функции.
Функциям-членам класса всегда отдается предпочтение.
Возьмите собаку из приюта!
Успехов в программировании!
Узнать подробнее о курсе "Kotlin Backend Developer".
Посмотреть открытый урок на тему "Пересмотр «12 факторов»: создаём современный микросервис на Kotlin".