Статьи

Scala Tutorial – объекты, классы, наследование, черты, списки с несколькими связанными типами, применение

Предисловие

Это 9 часть руководств для начинающих программистов, попадающих в Scala. Другие посты находятся в этом блоге, и вы можете получить ссылки на эти и другие ресурсы на странице ссылок курса по компьютерной лингвистике, для которого я их создаю. Кроме того, вы можете найти эту и другие серии учебников на странице JCG Java Tutorials .

Это руководство посвящено объектно-ориентированному программированию в Scala. Большая часть того, что мы видели до сих пор, это программирование с использованием функций и использованием базовых типов, таких как Int, Double и String, и с предопределенными типами, такими как List и Map. Как оказалось, это все классы или типы структур данных Scala, которые позволяют создавать объекты или экземпляры типа. Этот учебник не даст широкого введения в объектно-ориентированное программирование, но он даст некоторые практические примеры классов и объектов и как их использовать. Я заранее прошу прощения за некоторую неряшливость в представлении объектно-ориентированных концепций; цель состоит в том, чтобы донести идеи для начинающих, в основном, на интуитивно понятных примерах, не погружаясь в множество технических деталей. См. Страницу Wikipedia по объектно-ориентированному программированию для более подробной информации.

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

Объекты

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
object JohnSmith {
  val firstName = "John"
  val lastName = "Smith"
  val age = 37
  val occupation = "linguist"
 
  def fullName: String = firstName + " " + lastName
 
  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }
 
}

Если вы поместите это в Scala REPL, вы сможете получить доступ к полям ( firstName , lastName , age и профессия ) и функциям ( fullName и greet ).

01
02
03
04
05
06
07
08
09
10
11
scala> JohnSmith.firstName
res0: java.lang.String = John
 
scala> JohnSmith.fullName
res1: String = John Smith
 
scala> JohnSmith.greet(true)
res2: String = Hello, my name is John Smith. I'm a linguist.
 
scala> JohnSmith.greet(false)
res3: String = Hi, I'm John!

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

Мы, конечно, могли бы быть заинтересованы в инкапсуляции информации о другом человеке таким образом. Мы могли бы сделать это, подражая определению Джона Смита.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
object JaneDoe {
  val firstName = "Jane"
  val lastName = "Doe"
  val age = 34
  val occupation = "computer scientist"
 
  def fullName: String = firstName + " " + lastName
 
  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }
 
}

После добавления вышеуказанного кода в REPL теперь Джейн Доу может приветствовать нас.

1
2
3
4
5
scala> JaneDoe.greet(true)
res4: String = Hello, my name is Jane Doe. I'm a computer scientist.
 
scala> JaneDoe.greet(false)
res5: String = Hi, I'm Jane!

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

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

1
2
3
4
5
6
7
scala> val people = List(JohnSmith, JaneDoe)
people: List[ScalaObject] = List(JohnSmith$@698fcb66, JaneDoe$@5f72cbae)
 
scala> people.map(person => person.firstName)
<console>:11: error: value firstName is not a member of ScalaObject
people.map(person => person.firstName)
                                          ^

Единственное, что Scala знает о JohnSmith и JaneDoe, это то, что они ScalaObjects . Это означает, что список таких объектов может просто содержать их и позволяет перемещать их как группу. Итак, нужно сделать что-то большее, чтобы сделать эти коллекции более полезными и более общими.

Классы

Из приведенного выше списка нам хотелось бы получить List [Person] , где Person — это тип, имеющий известные поля и функции. Мы можем сделать это, определив класс Person, а затем определив Джона и Джейн как членов этого класса. Это также уменьшает проблему дублирования, которая отмечалась ранее. Вот как это выглядит.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class Person (
  val firstName: String,
  val lastName: String,
  val age: Int,
  val occupation: String
) {
 
  def fullName: String = firstName + " " + lastName
 
  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }
 
}

Ключевое слово class указывает, что это определение класса, а Person — это имя класса. Следующая часть определения представляет собой набор параметров для класса, которые позволяют нам создавать объекты, которые являются экземплярами класса — другими словами, они являются заполнителями, которые позволяют нам использовать класс Person в качестве фабрики для создания объектов Person . Мы делаем это, используя ключевое слово new , давая имя класса и предоставляя значения для каждого из параметров. Например, вот как мы можем создать Джона Смита сейчас.

1
2
scala> val johnSmith = new Person("John", "Smith", 37, "linguist")
johnSmith: Person = Person@1979d4fb

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

1
2
3
4
5
scala> johnSmith.age
res8: Int = 37
 
scala> johnSmith.greet(true)
res9: String = Hello, my name is John Smith. I'm a linguist.

Определение других людей теперь легко и не требует каких-либо вырезок и вставок.

1
2
3
4
5
6
7
8
scala> val janeDoe = new Person("Jane", "Doe", 34, "computer scientist")
janeDoe: Person = Person@7ff5376c
 
scala> val johnDoe = new Person("John", "Doe", 43, "philosopher")
johnDoe: Person = Person@6544c984
 
scala> val johnBrown = new Person("John", "Brown", 28, "mathematician")
johnBrown: Person = Person@4076a247

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

01
02
03
04
05
06
07
08
09
10
11
scala> val people = List(johnSmith, janeDoe, johnDoe, johnBrown)
people: List[Person] = List(Person@1979d4fb, Person@7ff5376c, Person@6544c984, Person@4076a247)
 
scala> people.map(person => person.firstName)
res10: List[String] = List(John, Jane, John, John)
 
scala> people.map(person => person.age)
res11: List[Int] = List(37, 34, 43, 28)
 
scala> people.map(person => person.age).sum/people.length.toDouble
res12: Double = 35.5

Мы можем отсортировать их по возрасту.

1
2
3
4
5
scala> val ageSortedPeople = people.sortBy(_.age)
ageSortedPeople: List[Person] = List(Person@4076a247, Person@7ff5376c, Person@1979d4fb, Person@6544c984)
 
scala> ageSortedPeople.map(person => person.fullName + ":" + person.age)
res13: List[java.lang.String] = List(John Brown:28, Jane Doe:34, John Smith:37, John Doe:43)

Мы также можем группировать людей по имени, фамилии и т. Д.

1
2
3
4
5
scala> people.groupBy(person => person.firstName)
res14: scala.collection.immutable.Map[String,List[Person]] = Map(Jane -> List(Person@7ff5376c), John -> List(Person@1979d4fb, Person@6544c984, Person@4076a247))
 
scala> people.groupBy(person => person.lastName)
res15: scala.collection.immutable.Map[String,List[Person]] = Map(Brown -> List(Person@4076a247), Smith -> List(Person@1979d4fb), Doe -> List(Person@7ff5376c, Person@6544c984))

С этим мы можем сделать так, чтобы все Джонс приветствовали нас.

1
2
3
4
scala> people.groupBy(person => person.firstName)("John").foreach(john => println(john.greet(true)))
Hello, my name is John Smith. I'm a linguist.
Hello, my name is John Doe. I'm a philosopher.
Hello, my name is John Brown. I'm a mathematician.

Автономные объекты

Выше мы видели, как создавать экземпляры класса Person , используя ключевое слово new и назначая полученный объект переменной. Мы можем вернуться к полному кругу первого созданного нами объекта JohnSmith , который был автономным ScalaObject . Вместо этого мы можем создать такой отдельный объект, расширив класс Person .

1
2
3
4
5
scala> object ThomYorke extends Person("Thom", "Yorke", 43, "musician")
defined module ThomYorke
 
scala> ThomYorke.greet(true)
res25: String = Hello, my name is Thom Yorke. I'm a musician.

Расширяя класс Person для создания объекта, мы говорим, что объект является разновидностью Person — подробнее о наследовании см. Ниже. Итак, ThomYorke — это объект Person , как и другие, которые мы создали, но для другого случая использования мы увидим больше в следующем уроке. А пока я подведу итоги очень грубо, сказав, что объект ThomYorke можно сделать более доступным с помощью другого кода, который может использовать мой код, тогда как объекты johnSmith и janeDoe будут более локально содержаться.

наследование

Автономные объекты естественным образом приводят нас к идее наследования. Во многих областях существуют естественные иерархии типов, такие, что свойства супертипа наследуются его подтипами (например, у рыб есть жабры и они плавают, а у лосося есть жабры и они плавают). Например, у нас может быть тип Linguist, который является типом Person , тип ComputerScientist, который является типом Person , и так далее. Чтобы смоделировать это, мы создаем один класс, который расширяет другой и, возможно, предоставляет некоторые дополнительные параметры, такие как следующее определение лингвистического подтипа Person .

01
02
03
04
05
06
07
08
09
10
11
12
class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") {
 
  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."
 
}

Класс Linguist имеет свой собственный список параметров: некоторые из них, такие как firstName , lastName и age , передаются Person , и есть новые поля параметров specialty и favourfulLanguage . Расширяемая часть определения передает соответствующие параметры, необходимые для построения всей информации, чтобы сделать Персона , и для лингвиста она непосредственно устанавливает параметр занятия как «лингвистический» — таким образом, нам не нужно предоставлять это, когда мы строим лингвиста , такого как Ноам Хомский.

1
scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")noamChomsky: Linguist = Linguist@54c0627f

Определив таким образом объект лингвиста , мы можем попросить его приветствовать его работу.

1
2
scala> noamChomsky.workGreeting
res26: java.lang.String = As a linguist, I am a syntactician who likes to study the language English.

Мы также можем получить доступ к полям и функциям объектов Person , таким как age и greet .

1
2
3
4
5
scala> noamChomsky.age
res27: Int = 83
 
scala> noamChomsky.greet(true)
res28: String = Hello, my name is Noam Chomsky. I'm a linguist.

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

1
2
scala> noamChomsky.favoriteLanguage
res29: String = English

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

Черты

Теперь мы можем, конечно, перейти к определению класса ComputerScientist , который также будет иметь функцию workGreeting , но Linguist.workGreeting и ComputerScientist.workGreeting будут полностью отделены друг от друга. Чтобы включить это, мы можем использовать признаки, которые похожи на классы, но которые определяют интерфейс функций и полей, для которых классы могут предоставлять конкретные значения и реализации. (Примечание: черты могут также определять конкретные поля и функции, поэтому они не ограничиваются функциями-заполнителями, как показано ниже.)

В качестве примера приведу черту Worker , которая просто определяет функцию workGreeting и объявляет, что она должна возвращать строку .

1
2
3
trait Worker {
  def workGreeting: String
}

Класс Linguist, определенный ранее, уже обеспечивает реализацию этой функции. Чтобы позволить лингвисту считаться типом работника , мы добавляем « Работник» после расширения « Персона» .

01
02
03
04
05
06
07
08
09
10
11
12
class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") with Worker {
 
  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."
 
}

Это называется «смешивание» черты Worker , потому что класс Linguist смешивает поля и функции Worker с функциями Person .

Обратите внимание, что мы также можем создавать классы, которые просто расширяют такую ​​черту, как Worker .

1
2
3
class Student (school: String, subject: String) extends Worker {
  def workGreeting = "I'm studying " + subject + " at " + school + "!"
}

Теперь мы можем создать объект Student и запросить их приветствие.

1
2
3
4
5
scala> val anonymousStudent = new Student("The University of Texas at Austin", "history")
anonymousStudent: Student = Student@734445b5
 
scala> anonymousStudent.workGreeting
res32: java.lang.String = I'm studying history at The University of Texas at Austin!

Обратите внимание, что параметрам школы и предмета не предшествовал val в определении Student . Это означает, что они не являются полями-членами класса Student , что означает, что к ним нельзя получить доступ извне. Например, попытка получить доступ к значению, указанному для школы для anonymousStudent, не удалась.

1
2
3
scala> anonymousStudent.school
<console>:11: error: value school is not a member of Student
anonymousStudent.school

Конечно, внутри Студент может использовать значения, предоставленные таким параметрам, например, при определении результата workGreeting . Этот вид инкапсуляции скрывает свойства объектов класса от кода, который находится вне класса; эта стратегия может помочь уменьшить степень свободы, доступную пользователям вашего кода, чтобы они использовали только то, что вам нужно. В общем, если другим не нужно его использовать, вы не должны делать его доступным для них.

Возвращаясь к классам, которые являются персонами и работниками , когда мы определяем ComputerScientist , мы делаем аналогичные расширения … с объявлением, как мы делали для Linguist .

01
02
03
04
05
06
07
08
09
10
11
12
class ComputerScientist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  favoriteProgrammingLanguage: String
) extends Person(firstName, lastName, age, "computer scientist") with Worker {
 
  def workGreeting =
    "As a " + occupation + ", I work on " + speciality + ". Much of my code is written in " + favoriteProgrammingLanguage + "."
 
}

Давайте создадим Эндрю МакКалума как объект ComputerScientist .

1
2
3
4
5
scala> val andrewMcCallum = new ComputerScientist("Andrew", "McCallum", 44, "machine learning", "Scala")
andrewMcCallum: ComputerScientist = ComputerScientist@493cd5ba
 
scala> andrewMcCallum.workGreeting
res31: java.lang.String = As a computer scientist, I work on machine learning. Much of my code is written in Scala.

Поскольку мы заново определили Linguist как Работника , нам нужно воссоздать Ноама Хомского, используя новое определение. (Создание выглядит так же, как и раньше, но оно использует новое определение класса, которое было обновлено в REPL.)

1
2
scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")
noamChomsky: Linguist = Linguist@6fccaf14

Незначительное, что следует отметить: специальность ComputerScientist не связана с лингвистической , поэтому нет особой надежды на согласованность использования между этими двумя понятиями : для Linguist это описание человека, работающего в подобласти, но для ComputerScientist это описание подобласти.

Итак, что произойдет, если мы поместим noamChomsky и andrewMcCallum в список вместе?

1
2
scala> val professors = List(noamChomsky, andrewMcCallum)
professors: List[Person with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba)

Scala создала список с типом List [Person with Worker] ; это наиболее конкретный тип, который действителен для всех элементов списка. Это означает, что мы можем рассматривать все элементы как Персоны , например, доступ к их профессии (которая является полем члена Персоны ).

1
2
scala> professors.map(prof => prof.occupation)
res34: List[String] = List(linguist, computer scientist)

И мы можем рассматривать каждый элемент списка как человека и работника , например, распечатывая их полное имя (от персоны ) и их workGreeting (от работника ).

1
2
3
scala> professors.foreach(prof => println(prof.fullName + ": " + prof.workGreeting))
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.

Однако мы не можем получить доступ к полям и функциям, которые являются специфическими для лингвистов или компьютерных специалистов , таких как FavoritesLanguage от Linguist .

1
2
3
scala> professors.map(prof => prof.favoriteLanguage)
<console>:15: error: value favoriteLanguage is not a member of Person with Worker
professors.map(prof => prof.favoriteLanguage)

Легко понять, почему у Scala такое поведение: хотя это было бы верно для noamChomsky , это не было бы для andrewMcCallum (в соответствии с тем, как мы определили Linguist и ComputerScientist ).

Соответствие типам в полиморфных списках

Подумайте, что происходит, когда anonymousStudent находится в списке вместе с профессорами.

1
2
scala> val workers = List(noamChomsky, andrewMcCallum, anonymousStudent)
workers: List[ScalaObject with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba, Student@734445b5)

Тип Person пропал, и теперь у нас есть список более общего типа ScalaObject с Worker . Теперь мы можем использовать только метод workGreeting от Worker .

Однако стоит отметить, что операторы соответствия пригодятся, когда у вас есть коллекции разнородных объектов. Например, поместите следующий код в REPL.

1
2
3
4
5
6
7
8
9
val people = List(johnSmith, noamChomsky, andrewMcCallum, anonymousStudent)
 
people.foreach { person =>
  person match {
    case x: Person with Worker => println(x.fullName + ": " + x.workGreeting)
    case x: Person => println(x.fullName + ": " + x.greet(true))
    case x: Worker => println("Anonymous:" + x.workGreeting)
  }
}

В результате получается следующее (помните, что Джон Смит никогда не определялся как лингвист — он был определен как человек, чья профессия «лингвист»).

1
2
3
4
John Smith: Hello, my name is John Smith. I'm a linguist.
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.
Anonymous:I'm studying history at The University of Texas at Austin!

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

Функция применения

Scala предоставляет простую, но невероятно приятную функцию: если вы определяете функцию apply в классе или объекте, вам на самом деле не нужно писать «apply», чтобы использовать ее. Например, следующий объект добавляет единицу к аргументу, предоставленному его методу apply .

1
2
3
object AddOne {
  def apply (x: Int): Int = x+1
}

Итак, мы можем использовать его так, как вы обычно ожидаете.

1
2
scala> AddOne.apply(3)
res41: Int = 4

Но мы также можем обойтись без части «.apply» и получить тот же результат.

1
2
scala> AddOne(3)
res42: Int = 4

Если у класса есть метод apply , мы можем проделать тот же трюк с любым объектом этого класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class AddN (amountToAdd: Int) {
  def apply (x: Int): Int = x + amountToAdd
}
 
scala> val add2 = new AddN(2)
add2: AddN = AddN@43ca04a1
 
scala> add2(5)
res43: Int = 7
 
scala> val add42 = new AddN(42)
add42: AddN = AddN@83e591f
 
scala> add42(8)
res44: Int = 50

Оказывается, вы использовали методы apply довольно часто, даже не подозревая об этом! Когда у вас есть список и вы обращаетесь к элементу по индексу, вы используете метод apply класса List .

1
2
3
4
5
6
7
8
scala> val numbers = 10 to 20 toList
numbers: List[Int] = List(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
 
scala> numbers(3)
res46: Int = 13
 
scala> numbers.apply(3)
res47: Int = 13

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

Заворачивать

Этот учебник охватил основы объектно-ориентированного программирования в Scala. Надеюсь, этого достаточно, чтобы дать достойное представление о том, что такое объекты и классы, и как вы можете с ними что-то делать. О них можно узнать гораздо больше, но этого должно быть достаточно, чтобы вы могли начать, чтобы дальнейшее изучение могло быть сделано осмысленно. Важно понимать эти концепции, так как Scala является объектно-ориентированным с самого начала. На самом деле, во многих предыдущих уроках я иногда проходил через несколько дополнительных упражнений, чтобы попытаться описать происходящее без необходимости говорить об объектной ориентации. Но теперь вы можете видеть такие вещи, как Int, Double, List, Map и т. Д., Каковы они: классы, которые содержат определенные поля и функции, которые можно использовать для достижения цели. Теперь вы можете начать программировать свои собственные классы, чтобы включить ваши собственные пользовательские поведения в ваших приложениях.

Ссылка: Первые шаги в Scala для начинающих программистов, часть 9 от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .

Статьи по Теме :