В этой статье я дам несколько советов с примерами, как сделать код на Kotlin более понятным для чтения.
Другие полезные статьи на тему Kotlin:
- Шпаргалка по коллекциям Kotlin
- Хорошие и плохие приемы программирования на Kotlin
- Основы функционального программирования на Kotlin
Практические советы
Используйте require и check:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
require(arg.length < 10) { "message" } val result = checkNotNull(bar(arg)) { "message" } /// вместо /// if (arg.length < 10) { throw IllegalArgumentException("message") } val result = bar(arg) ?: throw IllegalStateException("message") |
Используйте функции вместо комментариев:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
val user = getUser(id) validate(user) activate(user) private fun validate(user: User) { // код валидации } private fun activate(user: User) { // код активации } /// вместо /// val user = getUser(id) /* * Validate user */ // код валидации /* * Activate user */ // код активации |
А лучше — функции-расширения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private fun User.validate(): User { // код валидации return this } private fun User.activate(): User { // код активации return this } ... val user = getUser(id) .validate() .activate() |
Инфиксные функции делают код более легким для чтения:
1 2 3 4 5 6 7 8 9 |
val x = mapOf(1 to "a") val range = 1 until 10 val loop = listOf(...) zip listOf(...) /// вместо /// val x = mapOf(1.to("a")) val range = 1.until(10) val loop = listOf(...).zip(listOf(...)) |
Делайе функции инфиксными, если:
- функция не имеет побочных эффектов;
- имеет простую логику;
- имеет короткое имя;
- используется в местах, где скобки будут мешать чтению.
Используйте функции with, apply и also:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
...какой-то код... with(foo.id) { LOGGER.info("id is $this") doSomething() // method of id doSomethingElse(this) } ...какой-то код... /// вместо /// ...какой-то код... val id = foo.id LOGGER.info("id is $id") id.doSomething() doSomethingElse(id) ...какой-то код... |
Не указывайте тип там, где его можно не указывать:
1 2 3 4 5 6 7 |
val x = "a" override fun foo() = 1 /// вместо /// val x: String = "a" override fun foo(): Int = 1 |
Исключения:
- возвращаемый функцией тип слишком сложный, например Map<Int, Map<String, String>>;
- когда вызываешь функции, не имеющие nullable-аннотации (например, обычные функции Java).
Используйте присваивание при создании функции, состоящей из одного выражения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
fun foo(id: Int) = getFoo(id) .chain1() .chain2() .chain3() .chain4 { // лямбда } /// вместо /// fun foo(id: Int): Bar { return getFoo(id) .chain1() .chain2() .chain3() .chain4 { // лямбда } } |
Typealias упростит работу со сложными типами:
1 2 3 4 5 6 7 8 9 10 |
typealias CustomerId = Int typealias PurchaseId = String typealias StoreName = String typealias Report = Map<CustomerId, Map<PurchaseId, StoreName>> fun(report: Report) = // ... /// вместо /// fun(report: Map<Int, Map<String, String>>) = // ... |
Используйте метки точности для уточнения типа присваиваемого значения:
1 2 3 4 5 6 7 |
val x = 1L val y = 1.2f /// вместо /// val x: Long = 1 val y: Float = 1.2 |
Пользуйтесь подчеркиванием, чтобы сделать длинные числа более читаемыми:
1 2 3 4 5 |
val x = 1_000_000 /// вместо /// val x = 1000000 |
Применяйте интерполяцию строк, чтобы сделать их более читаемыми:
1 2 3 4 5 6 7 |
val x = "customer $id bought ${purchases.count()} items" val y = """He said "I’m tired"""" /// вместо /// val x = "customer " + id + " bought " + purchases.count() + " items" val y = "He said \"I’m tired\"" |
Используйте оператор ? для возврата управления:
1 2 3 4 5 6 7 8 9 |
val user = getUser() ?: return 0 /// вместо /// val user = getUser() if (user == null) { return 0 } |
Применяйте тип Sequence для оптимизации обработки очень длинных списков (более 1000 элементов):
1 2 3 4 5 6 7 8 9 10 11 |
listOf(1).asSequence() .filter { ... } .map { ... } .maxBy { ... } /// вместо /// listOf(1) .filter { ... } .map { ... } .maxBy { ... } |
Используйте обратные кавычки при написании имен тестов:
1 2 3 4 5 |
fun `test foo - when foo increases by 3% - returns true`() { ... } /// вместо /// fun testFoo_whenFooIncreasesBy3Percent_returnsTrue() { ... } |
Проблемы и решения Kotlin
Проблема номер 1: let. Многие программисты привыкли использовать let в качестве простой и удобной альтернативы if (x == null):
1 2 3 4 5 6 |
fun deleteImage(){ var imageFile : File ? = ... imageFile?.let { if(it.exists()) it.delete() } } |
Так делать не стоит. Использование it — дурной тон, потому что множественные it могут смешаться, если в коде появится еще одна подобная лямбда. Ты можешь попробовать исправить это с помощью конструкции imageFile?.let { image ->, но в итоге сделаешь еще хуже, потому что в той же области видимости появится еще одна переменная, которая ссылается на то же значение, но имеет другое имя. И это имя надо будет придумать!
На самом деле в большинстве случаев исправить эту проблему можно простым отказом от let:
1 2 3 4 5 6 |
fun deleteImage(){ val imageFile : File ? = ... if(imageFile != null) { if(imageFile.exists()) imageFile.delete() } } |
С этим кодом все в порядке. Умное приведение типов сделает свою работу, и ты сможешь ссылаться на imageFile после проверки как на не nullable-переменную.
Но! Такой прием не сработает, если речь идет не о локальной переменной, а о полях класса. Из‑за того что поля класса могут изменяться несколькими методами, работающими в разных потоках, компилятор не сможет использовать смарткастинг.
Как раз здесь и можно использовать let.
Но есть и более необычные способы. Например, выходить из функции, если значение поля null:
1 2 3 4 |
fun deleteImage() { val imageFile = getImage() ?: return ... } |
Или использовать метод takeIf:
1 2 3 |
fun deleteImage() { getImage()?.takeIf { it.exists }?.let {it.delete()} } |
Можно даже скомбинировать оба подхода:
1 2 3 4 5 |
fun deleteImage() { val image = getImage()?.takeIf { it.exists } ?: return image.delete() }/pre> <strong>Проблема номер 2: also и apply.</strong> Синтаксис Kotlin поощряет использование лямбд везде, где только возможно. Иногда это приводит к созданию весьма монструозных конструкций: |
1 2 3 4 5 6 |
Intent(context, MyActivity::class.java).apply { putExtra("data", 123) addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) }).also { intent -> startActivity(this@FooActivity, intent) } |
Смысл такого кода в том, чтобы ограничить создание интента и его использование двумя различными областями видимости, что в теории должно благотворно повлиять на его модульность.
На самом деле такие конструкции только захламляют код. Гораздо красивее выглядит его более прямолинейная версия:
1 2 3 4 5 |
val intent = Intent(context, MyActivity::class.java).apply { putExtra("data", 123) addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) }) startActivity(this@FooActivity,intent) |
А еще лучше вынести код конфигурирования объекта в отдельную функцию:
1 2 3 4 5 6 |
fun startSomeActivity() { startActivity(getSomeIntent()) } fun getSomeIntent() = Intent(context, SomeActivity::class.java).apply { // ... } |
Проблема номер 3: run. Распространенный прием — использовать функцию run для обрамления блоков кода:
1 2 3 4 5 6 7 8 |
val userName: String get() { preferenceManager.getInstance()?.run { getName() } ?: run { getString(R.string.stranger) } } |
Это абсолютно бессмысленное захламление кода. Оно затрудняет чтение простого по своей сути кода:
1 2 |
val userName: String get() = preferenceManager.getInstance()?.getName() ?: getString(R.string.stranger) |
Если же кода в блоке «если null» больше одной строчки, то можно использовать такую конструкцию:
1 2 3 4 |
preferenceManager.getInstance()?.getName().orDefault { Log.w(“Name not found, returning default”) getString(R.string.stranger) } |
Проблема номер 4: with. В данном случае проблема не в самой функции with, а в ее игнорировании. Разработчики просто не используют эту функцию, несмотря на всю ее красоту:
1 2 3 4 5 |
val binding = MainLayoutBinding.inflate(layoutInflater) with(binding) { textName.text = "ch8n" textTwitter.text = "twitter@ch8n2" }) |
Причин не использовать ее обычно две:
- ее нельзя использовать в цепочках вызовов функций;
- она плохо дружит с nullable-переменными.
С этими советами вы сможете сделать код на Kotlin чище, понятнее и однозначнее..