Extensions (расширения) – это одна из самых известных фич в Kotlin, которая позволяет расширять существующие классы без изменения их исходного кода. Это очень полезно, когда вы хотите добавить новые методы или свойства к сторонним классам. Сами расширения НЕ меняют класс, к которому они применяются. Они применятся только на уровне компиляции и доступны только нам в нашем проекте, где мы их написали.
Давайте создадим простое расширение для класса String – метод, который будет выводить количество котиков в строке:
Здесь мы добавили метод catCount к классу String. Теперь мы можем использовать этот метод в нашем коде:
Как работают расширения под капотом? (Сейчас будет сложно)
Как я написала выше, расширения не меняют сам класс, к которому они применяются. Компилятор Kotlin преобразует их в статические методы (метод, который можно вызывать без создания класса), которые принимают экземпляр класса, который мы расширяем, в качестве параметра.
Вот как выглядит наш пример с catCount после компиляции:
Когда Kotlin компилирует наш код, он создаёт отдельный класс ExtensionsKt, который содержит статический метод catCount (как раз наше расширение) и принимает объект класса String в качестве параметра (потому что наше расширение именно для строк). В принципе, на этом можно остановиться и не копать вглубь, но я еще разобрала строчку "return StringsKt.count((CharSequence)$this$catCount, (Function1)null.INSTANCE);", потому что интересно же:
- StringsKt – это класс, созданный компилятором при компиляции кода Kotlin в байт-код Java. Он генерируется автоматически при создании расширения (для каждого типа свой (если расширяем для BigDecimal, то будет BigDecimalKt)). Это необходимо для совместимости, так как Java не поддерживает функции расширения напрямую.
В нашем случае класс StringsKt содержит только один статический метод catCount. Т.е., когда вы вызываете расширение catCount, в реальности вызывается соответствующий статический метод из сгенерированного класса StringsKt в байт-коде Java. Самое важное: StringsKt содержит только статические методы расширения и никаких методов из настоящего String. - StringsKt.count((CharSequence)$this$catCount, (Function1)null.INSTANCE); – тут происходит вызов статического метода count из класса StringsKt. Это стандартная реализация count, которую мы использовали на самом первом скриншоте. По сути, мы просто вызываем уже существующий метод из библиотеки.
- (CharSequence)$this$catCount - здесь экземпляр класса, для которого делаем расширение, ($this$catCount) явно приводится к типу CharSequence. Такое приведение необходимо, потому что метод count ожидает на входе объект типа CharSequence.
- (Function1)null.INSTANCE - это второй аргумент метода count. Метод count принимает boolean-функцию (Function1) в качестве второго аргумента, которая определяет условие, на основе которого подсчитываются элементы. В данном случае, для примера с котиками, вместо null.INSTANCE должен быть функция, проверяющая наличие символа '🐱'. Вероятно, при декомпиляции кода произошла потеря этой информации. Такое бывает из-за оптимизаций и прочего, что делает компилятор :(
В итоге, эта строка вызывает статический метод count с объектом-получателем (строкой) и функцией-предикатом (которая должна проверять наличие котиков) в качестве аргументов.
Немного про производительность:
Чуть выше мы уже посмотрели, что при создании расширения, компилятор Kotlin преобразует его в статический метод, который принимает объект-получатель (класс, к которому применяется функция расширения) в качестве параметра. Это означает, что с точки зрения производительности, вызов функции расширения равен вызову обычного статического метода с передачей объекта в качестве аргумента. Современные JVM и ART очень хорошо оптимизированы для работы со статическими методами, поэтому использование функций расширения обычно не снижает производительность.
Однако, в некоторых случаях, использование расширений может привести к излишней генерации кода или вызовов, что может сказаться на производительности. Это обычно происходит только при использовании вложенных расширений, большого количества лямбда-функций или вызовах во вложенных циклах.
В целом, если использовать расширения разумно, то они не приведут к существенным проблемам производительности.
И несколько советов:
- Не злоупотребляйте расширениями: используйте функции расширения только тогда, когда они действительно улучшают код. Если использовать слишком много расширений, то код может стать сложным и запутанным.
- Соблюдайте принцип единственной ответственности: расширения должны выполнять одну конкретную задачу. Не создавайте монстров, которые делают сразу несколько вещей.
- Используйте говорящие имена: называйте свои функции расширения так, чтобы их имена отражали их предназначение. Это в целом совет не только для расширений. Всегда используйте хорошие говорящие имена.
- Не заменяйте наследование и композицию: расширения удобны, но они не должны заменять наследование и композиция. Если вам нужно изменить поведение существующего класса, лучше использовать наследование или композицию.
- Создавайте общие и многократно используемые расширения: смело выносите расширения, которые много где используются, в общий модуль или библиотеку, чтобы сделать ваш код более модульным и повторно используемым.
- Используйте расширения с осторожностью для сторонних библиотек: если вы создаете расширения для сторонних библиотек, помните, что изменения в этих библиотеках могут повлиять на ваш код. Будьте готовы к тому, что при обновлении библиотеки ваш код может потребовать изменений или адаптации.
- Тестируйте свои расширения: как и с любым другим кодом, важно тестировать расширения, чтобы гарантировать их корректную работу и отсутствие ошибок. Это, кстати, сильно поможет обнаружить проблемы для 6 пункта.
- Помните о производительности: хотя функции расширения не имеют существенного влияния на производительность, следует всегда помнить о ней и оптимизировать код, когда это возможно. Избегайте создания функций расширения, которые сильно нагружают процессор или используют большие объемы памяти.
Дубль статей в телеграмме — https://t.me/android_junior
Мои заметки в телеграмме — https://t.me/android_junior_notes
P.S. весь код помог сгенерировать ChatGPT. :)