Статьи

Kotlin From Scratch: дополнительные свойства и классы

Kotlin — это современный язык программирования, который компилируется в байт-код Java. Он бесплатный и с открытым исходным кодом , и обещает сделать кодирование для Android еще более увлекательным.

В предыдущей статье вы узнали о классах и объектах в Kotlin. В этом уроке мы продолжим больше узнавать о свойствах, а также рассмотрим расширенные типы классов в Kotlin, изучив следующее:

  • позднеинициализированные свойства
  • встроенные свойства
  • свойства расширения
  • классы данных, перечисления, вложенные и запечатанные

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

Давайте рассмотрим пример, чтобы понять этот уникальный модификатор свойства.

01
02
03
04
05
06
07
08
09
10
11
class Presenter {
    private var repository: Repository?
     
    fun initRepository(repo: Repository): Unit {
        this.repository = repo
    }
}
 
class Repository {
    fun saveAmount(amount: Double) {}
}

В приведенном выше коде мы объявили изменяемое свойство Repository нулевым значением, которое имеет тип Repository внутри класса Presenter — и затем мы инициализировали это свойство значением null во время объявления. У нас есть метод initRepository() в классе Presenter который позже initRepository() инициализирует это свойство с фактическим экземпляром Repository . Обратите внимание, что этому свойству также может быть присвоено значение с помощью инжектора зависимостей, такого как Dagger.

Теперь для того, чтобы мы могли вызывать методы или свойства этого свойства repository , мы должны выполнить нулевую проверку или использовать оператор безопасного вызова. Почему? Потому что свойство repository имеет тип NULL ( Repository? ). (Если вам нужно освежить в Nullability, циклы и условия ).

1
2
3
4
// Inside Presenter class
fun save(amount: Double) {
    repository?.saveAmount(amount)
}

Чтобы избежать необходимости выполнять нулевые проверки каждый раз, когда нам нужно вызвать метод свойства, мы можем пометить это свойство модификатором lateinit — это означает, что мы объявили это свойство (которое является экземпляром другого класса) как позднюю инициализацию (что означает свойство будет инициализировано позже).

1
2
3
4
5
class Presenter {
 
    private lateinit var repository: Repository
    //…
}

Теперь, пока мы ждем, пока свойству будет присвоено значение, мы можем безопасно получить доступ к методам свойства, не делая никаких нулевых проверок. Инициализация свойства может происходить либо в методе установки, либо через внедрение зависимостей.

1
repository.saveAmount(amount)

Обратите внимание, что если мы попытаемся получить доступ к методам свойства до его инициализации, мы получим kotlin.UninitializedPropertyAccessException вместо NullPointerException . В этом случае сообщением об исключении будет «Латинское хранилище свойств не было инициализировано».

Обратите внимание также на следующие ограничения, накладываемые при задержке инициализации свойства с lateinit :

  • Он должен быть изменяемым (объявлено с помощью var ).
  • Тип свойства не может быть примитивным типом, например, Int , Double , Float и так далее.
  • Свойство не может иметь пользовательский метод получения или установки.

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

В Kotlin мы также можем использовать этот inline модификатор свойств. Использование этого модификатора оптимизирует доступ к свойству.

Давайте посмотрим на практический пример.

01
02
03
04
05
06
07
08
09
10
11
12
13
class Student {
    val nickName: String
        get() {
            println(«Nick name retrieved»)
            return «koloCoder»
        }
}
 
 
fun main(args: Array<String>) {
    val student = Student()
    print(student.nickName)
}

В приведенном выше коде у нас есть обычное свойство nickName , у которого нет inline модификатора. Если мы декомпилируем фрагмент кода, используя функцию Показать байт-код Kotlin (если вы находитесь в IntelliJ IDEA или Android Studio, используйте Инструменты > Kotlin > Показать байт-код Kotlin ), мы увидим следующий код Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public final class Student {
   @NotNull
   public final String getNickName() {
      String var1 = «Nick name retrieved»;
      System.out.println(var1);
      return «koloCoder»;
   }
}
 
public final class InlineFunctionKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, «args»);
      Student student = new Student();
      String var2 = student.getNickName();
      System.out.print(var2);
   }
}

В сгенерированном Java-коде выше (некоторые элементы сгенерированного кода были удалены для краткости), вы можете видеть, что внутри метода main() компилятор создал объект Student , названный getNickName() , и затем напечатал его возвращаемое значение ,

Давайте теперь вместо этого определим свойство как inline и сравним сгенерированный байт-код.

1
2
3
// …
inline val nickName: String
// …

Мы просто вставляем inline модификатор перед модификатором переменной: var или val . Вот байт-код, сгенерированный для этого встроенного свойства:

01
02
03
04
05
06
07
08
09
10
// …
public static final void main(@NotNull String[] args) {
  Intrinsics.checkParameterIsNotNull(args, «args»);
  Student student = new Student();
  String var3 = «Nick name retrieved»;
  System.out.println(var3);
  String var2 = «koloCoder»;
  System.out.print(var2);
}
// …

Снова был удален некоторый код, но ключевой момент, на который следует обратить внимание, — это метод main() . Компилятор скопировал тело функции свойства get() и вставил его в сайт вызова (этот механизм аналогичен встроенным функциям).

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

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

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

1
2
val String.upperCaseFirstLetter: String
   get() = this.substring(0, 1).toUpperCase().plus(this.substring(1))

В посте « Дополнительные функции» мы определили функцию расширения uppercaseFirstLetter() с типом получателя String . Здесь мы преобразовали его в свойство расширения верхнего уровня. Обратите внимание, что вы должны определить метод получения для вашего свойства, чтобы это работало.

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

Начнем с типичного Java-класса или POJO (Plain Old Java Object).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class BlogPost {
    private final String title;
    private final URI url;
    private final String description;
    private final Date publishDate;
 
    //.. constructor not included for brevity’s sake
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
 
        BlogPost blogPost = (BlogPost) o;
 
        if (title != null ? !title.equals(blogPost.title) : blogPost.title != null) return false;
        if (url != null ? !url.equals(blogPost.url) : blogPost.url != null) return false;
        if (description != null ? !description.equals(blogPost.description) : blogPost.description != null)
            return false;
        return publishDate != null ?
    }
 
    @Override
    public int hashCode() {
        int result = title != null ?
        result = 31 * result + (url != null ? url.hashCode() : 0);
        result = 31 * result + (description != null ? description.hashCode() : 0);
        result = 31 * result + (publishDate != null ? publishDate.hashCode() : 0);
        return result;
    }
 
    @Override
    public String toString() {
        return «BlogPost{» +
                «title=’» + title + ‘\» +
                «, url=» + url +
                «, description=’» + description + ‘\» +
                «, publishDate=» + publishDate +
                ‘}’;
    }
    //.. setters and getters also ignored for brevity’s sake
}

Как видите, нам нужно явно кодировать методы доступа к свойствам класса: методы получения и установки, а также методы hashcode , equals и toString (хотя IntelliJ IDEA, Android Studio или библиотека AutoValue могут помочь нам сгенерировать их). Мы видим этот тип шаблонного кода в основном на уровне данных типичного проекта Java. (Ради краткости я удалил полевые методы доступа и конструктор).

Круто то, что команда Kotlin предоставила нам модификатор data для классов, чтобы исключить написание этих шаблонов.

Давайте вместо этого напишем предыдущий код на Kotlin.

1
data class BlogPost(var title: String, var url: URI, var description: String, var publishDate: Date)

Потрясающие! Мы просто указываем модификатор data перед ключевым словом class для создания класса данных — точно так же, как мы делали в нашем классе BlogPost Kotlin выше. Теперь для нас будут созданы методы equals , hashcode , toString , copy и множественные компоненты. Обратите внимание, что класс данных может расширять другие классы (это новая функция Kotlin 1.1).

Этот метод сравнивает два объекта на равенство и возвращает true, если они равны, или false в противном случае. Другими словами, он сравнивает, если два экземпляра класса содержат одинаковые данные.

1
2
3
student.equals(student3)
// using the == in Kotlin
student == student3 // same as using equals()

В Kotlin использование оператора равенства == вызовет метод equals за кулисами.

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

Этот метод возвращает String представление объекта.

1
2
3
4
data class Person(var firstName: String, var lastName: String)
 
val person = Person(«Chike», «Mgbemena»)
println(person) // prints «Person(firstName=Chike, lastName=Mgbemena)»

Просто вызывая экземпляр класса, мы получаем возвращаемый нам строковый объект — Kotlin вызывает для нас объект toString() . Но если мы не введем ключевое слово data , посмотрим, каким будет наше строковое представление объекта:

1
com.chike.kotlin.classes.Person@2f0e140b

Гораздо менее информативно!

Этот метод позволяет нам создать новый экземпляр объекта с одинаковыми значениями свойств. Другими словами, он создает копию объекта.

1
2
3
4
val person1 = Person(«Chike», «Mgbemena»)
println(person1) // Person(firstName=Chike, lastName=Mgbemena)
val person2 = person1.copy()
println(person2) // Person(firstName=Chike, lastName=Mgbemena)

Одна из замечательных особенностей метода copy в Kotlin — это возможность изменять свойства во время копирования.

1
2
val person3 = person1.copy(lastName = «Onu»)
println(person3) //Person3(firstName=Chike, lastName=Onu)

Если вы Java-кодер, этот метод похож на метод clone() вы уже знакомы. Но метод copy Kotlin имеет более мощные функции.

В классе Person у нас также есть два метода, автоматически сгенерированных для нас компилятором из-за ключевого слова data помещенного в класс. Эти два метода имеют префикс «component», затем следует суффикс числа: component1() , component2() . Каждый из этих методов представляет отдельные свойства типа. Обратите внимание, что суффикс соответствует порядку свойств, объявленных в первичном конструкторе.

Итак, в нашем примере вызов component1() вернет имя, а вызов component2() вернет фамилию.

1
2
println(person3.component1()) // Chike
println(person3.component2()) // Onu

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

1
2
val (firstName, lastName) = Person(«Angelina», «Jolie»)
println(firstName + » » + lastName) // Angelina Jolie

То, что мы сделали здесь, — это непосредственное назначение первого и второго свойств ( firstName и lastName ) типа Person переменным firstName и lastName соответственно. Я также обсуждал этот механизм, известный как объявление деструктурирования, в последнем разделе поста « Пакеты и основные функции» .

В посте « Больше удовольствия с функциями» я говорил вам, что Kotlin имеет поддержку локальных или вложенных функций — функции, которая объявлена ​​внутри другой функции. Ну, Kotlin также аналогично поддерживает вложенные классы — класс, созданный внутри другого класса.

1
2
3
4
5
6
class OuterClass {
 
    class NestedClass {
        fun nestedClassFunc() { }
    }
}

Мы даже вызываем открытые функции вложенного класса, как показано ниже — вложенный класс в Kotlin эквивалентен static вложенному классу в Java. Обратите внимание, что вложенные классы не могут хранить ссылку на их внешний класс.

1
2
val nestedClass = OuterClass.NestedClass()
nestedClass.nestedClassFunc()

Мы также можем установить вложенный класс как закрытый — это означает, что мы можем создать только экземпляр NestedClass в рамках OuterClass .

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

01
02
03
04
05
06
07
08
09
10
11
class OuterClass() {
    val oCPropt: String = «Yo»
 
    inner class InnerClass {
         
        fun innerClassFunc() {
            val outerClass = this@OuterClass
            print(outerClass.oCPropt) // prints «Yo»
        }
    }
}

Здесь мы OuterClass на OuterClass из InnerClass , используя this@OuterClass .

Тип enum объявляет набор констант, представленных идентификаторами. Этот особый вид класса создается ключевым словом enum , которое указывается перед ключевым словом class .

1
2
3
4
5
enum class Country {
    NIGERIA,
    GHANA,
    CANADA
}

Чтобы получить значение перечисления на основе его имени (как в Java), мы делаем это:

1
Country.valueOf(«NIGERIA»)

Или мы можем использовать вспомогательный метод Kotlin enumValueOf<T>() для общего доступа к константам:

1
enumValueOf<Country>(«NIGERIA»)

Кроме того, мы можем получить все значения (например, для перечисления Java) следующим образом:

1
Country.values()

Наконец, мы можем использовать вспомогательный метод Kotlin enumValues<T>() чтобы получить все записи enum общим способом:

1
enumValues<Country>()

Это возвращает массив, содержащий записи enum.

Как и обычный класс, тип enum может иметь свой собственный конструктор со свойствами, связанными с каждой константой enum.

1
2
3
4
5
enum class Country(val callingCode: Int) {
    NIGERIA (234),
    USA (1),
    GHANA (233)
}

В первичном конструкторе перечислимого типа Country мы определили неизменяемое свойство callingCodes для каждой константы перечисления. В каждой из констант мы передали аргумент конструктору.

Затем мы можем получить доступ к свойству констант следующим образом:

1
2
val country = Country.NIGERIA
print(country.callingCode) // 234

Запечатанный класс в Kotlin — это абстрактный класс (вы никогда не собираетесь создавать объекты из него), который могут расширяться другими классами. Эти подклассы определены в теле запечатанного класса — в том же файле. Поскольку все эти подклассы определены внутри тела запечатанного класса, мы можем узнать все возможные подклассы, просто просмотрев файл.

Давайте посмотрим на практический пример.

1
2
3
4
5
6
7
// shape.kt
 
sealed class Shape
 
class Circle : Shape()
class Triangle : Shape()
class Rectangle: Shape()

Чтобы объявить класс как закрытый, мы вставляем модификатор sealed перед модификатором класса в заголовок объявления класса — в нашем случае мы объявили класс Shape как sealed . Запечатанный класс неполон без своих подклассов — так же, как и типичный абстрактный класс — поэтому мы должны объявить отдельные подклассы внутри одного и того же файла (в данном случае shape.kt ). Обратите внимание, что вы не можете определить подкласс запечатанного класса из другого файла.

В нашем коде выше мы указали, что класс Shape может быть расширен только классами Circle , Triangle и Rectangle .

Запечатанные классы в Kotlin имеют следующие дополнительные правила:

  • Мы можем добавить модификатор abstract в запечатанный класс, но это избыточно, потому что запечатанные классы являются абстрактными по умолчанию.
  • Запечатанные классы не могут иметь модификатор open или final .
  • Мы также можем свободно объявлять классы данных и объекты как подклассы для запечатанного класса (они все еще должны быть объявлены в том же файле).
  • Запечатанным классам не разрешено иметь открытые конструкторы — их конструкторы по умолчанию являются закрытыми.

Классы, расширяющие подклассы запечатанного класса, могут быть помещены либо в тот же файл, либо в другой файл. Подкласс запечатанного класса должен быть помечен модификатором open (вы узнаете больше о наследовании в Kotlin в следующем посте).

1
2
3
4
5
6
// employee.kt
sealed class Employee
open class Artist : Employee()
 
// musician.kt
class Musician : Artist()

Запечатанный класс и его подклассы действительно удобны в выражении when . Например:

1
2
3
4
5
fun whatIsIt(shape: Shape) = when (shape) {
    is Circle -> println(«A circle»)
    is Triangle -> println(«A triangle»)
    is Rectangle -> println(«A rectangle»)
}

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

Если бы мы вместо этого сделали следующее:

1
2
3
4
fun whatIsIt(shape: Shape) = when (shape) {
    is Circle -> println(«A circle»)
    is Triangle -> println(«A triangle»)
}

Код не будет компилироваться, потому что мы не включили все возможные варианты. У нас будет следующая ошибка:

1
Kotlin: ‘when’ expression must be exhaustive, add necessary ‘is Rectangle’ branch or ‘else’ branch instead.

Таким образом, мы можем либо включить регистр is Rectangle либо включить условие else для завершения выражения when .

В этом уроке вы узнали больше о занятиях в Котлине. Мы рассмотрели следующее о свойствах класса:

  • поздняя инициализация
  • встроенные свойства
  • свойства расширения

Кроме того, вы узнали о некоторых классных и продвинутых классах, таких как data, enum, nested и sealed. В следующем уроке из серии Kotlin From Scratch вы познакомитесь с интерфейсами и наследованием в Kotlin. До скорого!

Чтобы узнать больше о языке Kotlin, я рекомендую посетить документацию Kotlin . Или посмотрите некоторые другие наши посты по разработке приложений для Android здесь на Envato Tuts!

  • Android SDK
    Параллелизм в RxJava 2
  • Android SDK
    Отправка данных с помощью HTTP-клиента Retrofit 2 для Android
  • Android SDK
    Java против Kotlin: стоит ли использовать Kotlin для разработки под Android?
    Джессика Торнсби
  • Android SDK
    Как создать приложение для Android-чата с помощью Firebase
    Ашраф Хатхибелагал