В этом разделе представлены различные аспекты определения и вызова методов в Scala 3.
Определение методов
В Scala методы обладают множеством особенностей, в том числе:
- Generic (типовые) параметры
- Значения параметров по умолчанию
- Несколько групп параметров
- Контекстные параметры
- Параметры по имени
- и другие…
Некоторые из этих функций демонстрируются в этом разделе, но когда вы определяете “простой” метод, который не использует эти функции, синтаксис выглядит следующим образом:
def methodName(param1: Type1, param2: Type2): ReturnType = {
// тело метода
// находится здесь
}
def methodName(param1: Type1, param2: Type2): ReturnType =
// тело метода
// находится здесь
end methodName // опционально
В этом синтаксисе:
- ключевое слово
def
используется для определения метода - для наименования методов согласно стандартам Scala используется camel case convention
- у параметров метода необходимо всегда указывать тип
- возвращаемый тип метода указывать необязательно
- методы могут состоять как только из одной строки, так и из нескольких строк
- метку окончания метода
end methodName
указывать необязательно, её рекомендуется указывать только для длинных методов
Вот два примера однострочного метода с именем add
, который принимает два входных параметра Int
.
Первая версия явно показывает возвращаемый тип метода - Int
, а вторая - нет:
def add(a: Int, b: Int): Int = a + b
def add(a: Int, b: Int) = a + b
У публичных методов рекомендуется всегда указывать тип возвращаемого значения. Объявление возвращаемого типа может упростить его понимание при просмотре кода другого человека или своего кода спустя некоторое время.
Вызов методов
Вызов методов прост:
val x = add(1, 2) // 3
Коллекции Scala имеют десятки встроенных методов. Эти примеры показывают, как их вызывать:
val x = List(1, 2, 3)
x.size // 3
x.contains(1) // true
x.map(_ * 10) // List(10, 20, 30)
Внимание:
size
не принимает аргументов и возвращает количество элементов в списке- метод
contains
принимает один аргумент — значение для поиска map
принимает один аргумент - функцию; в данном случае в него передается анонимная функция
Многострочные методы
Если метод длиннее одной строки, начинайте тело метода со второй строки с отступом вправо:
def addThenDouble(a: Int, b: Int): Int = {
// представим, что это тело метода требует несколько строк
val sum = a + b
sum * 2
}
def addThenDouble(a: Int, b: Int): Int =
// представим, что это тело метода требует несколько строк
val sum = a + b
sum * 2
В этом методе:
sum
— неизменяемая локальная переменная; к ней нельзя получить доступ вне метода- последняя строка удваивает значение
sum
- именно это значение возвращается из метода
Когда вы вставите этот код в REPL, то увидите, что он работает как требовалось:
scala> addThenDouble(1, 1)
res0: Int = 4
Обратите внимание, что нет необходимости в операторе return
в конце метода.
Поскольку почти все в Scala является выражением — то это означает,
что каждая строка кода возвращает (или вычисляет) значение — нет необходимости использовать return
.
Это видно на примере того же метода, но в более сжатой форме:
def addThenDouble(a: Int, b: Int): Int = (a + b) * 2
В теле метода можно использовать все возможности Scala:
if
/else
выраженияmatch
выражения- циклы
while
- циклы
for
иfor
выражения - присвоение переменных
- вызовы других методов
- определения других методов
В качестве ещё одного примера многострочного метода,
getStackTraceAsString
преобразует свой входной параметр Throwable
в правильно отформатированную строку:
def getStackTraceAsString(t: Throwable): String = {
val sw = new StringWriter()
t.printStackTrace(new PrintWriter(sw))
sw.toString
}
def getStackTraceAsString(t: Throwable): String =
val sw = StringWriter()
t.printStackTrace(PrintWriter(sw))
sw.toString
В этом методе:
- в первой строке переменная
sw
принимает значение нового экземпляраStringWriter
- вторая строка сохраняет содержимое трассировки стека в
StringWriter
- третья строка возвращает строковое представление трассировки стека
Значения параметров по умолчанию
Параметры метода могут иметь значения по умолчанию.
В этом примере для параметров timeout
и protocol
заданы значения по умолчанию:
def makeConnection(timeout: Int = 5_000, protocol: String = "http") = {
println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
// здесь ещё какой-то код ...
}
def makeConnection(timeout: Int = 5_000, protocol: String = "http") =
println(f"timeout = ${timeout}%d, protocol = ${protocol}%s")
// здесь ещё какой-то код ...
Поскольку параметры имеют значения по умолчанию, метод можно вызвать следующими способами:
makeConnection() // timeout = 5000, protocol = http
makeConnection(2_000) // timeout = 2000, protocol = http
makeConnection(3_000, "https") // timeout = 3000, protocol = https
Вот несколько ключевых моментов об этих примерах:
- В первом примере аргументы не предоставляются, поэтому метод использует значения параметров по умолчанию:
5_000
иhttp
- Во втором примере для параметра
timeout
указывается значение2_000
, поэтому оно используется вместе со значением по умолчанию дляprotocol
- В третьем примере значения указаны для обоих параметров, поэтому используются они.
Обратите внимание, что при использовании значений параметров по умолчанию потребителю кажется, что он может работать с тремя разными переопределенными методами.
Именованные параметры
При желании вы также можете использовать имена параметров метода при его вызове.
Например, makeConnection
может также вызываться следующими способами:
makeConnection(timeout=10_000)
makeConnection(protocol="https")
makeConnection(timeout=10_000, protocol="https")
makeConnection(protocol="https", timeout=10_000)
В некоторых фреймворках именованные параметры используются постоянно. Они также очень полезны, когда несколько параметров метода имеют один и тот же тип:
engage(true, true, true, false)
Без помощи IDE этот код может быть трудночитаемым, но так он становится намного понятнее и очевиднее:
engage(
speedIsSet = true,
directionIsSet = true,
picardSaidMakeItSo = true,
turnedOffParkingBrake = false
)
Рекомендации о методах, которые не принимают параметров
Когда метод не принимает параметров, говорят, что он имеет arity уровень 0 (arity-0). Аналогично, если метод принимает один параметр - это метод с arity-1.
Когда создаются методы arity-0:
- если метод выполняет побочные эффекты, такие как вызов
println
, метод объявляется с пустыми скобками. - если метод не выполняет побочных эффектов, например, получение размера коллекции, что аналогично доступу к полю в коллекции, круглые скобки опускаются.
Например, этот метод выполняет побочный эффект, поэтому он объявлен с пустыми скобками:
def speak() = println("hi")
При вызове метода нужно обязательно указывать круглые скобки, если он был объявлен с ними:
speak // ошибка: "method speak must be called with () argument"
speak() // печатает "hi"
Хотя это всего лишь соглашение, его соблюдение значительно улучшает читаемость кода: с первого взгляда становится понятно, что метод с arity-0 имеет побочные эффекты.
Использование if
в качестве тела метода
Поскольку выражения if
/else
возвращают значение, их можно использовать в качестве тела метода.
Вот метод с именем isTruthy
, реализующий Perl-определения true
и false
:
def isTruthy(a: Any) = {
if (a == 0 || a == "" || a == false)
false
else
true
}
def isTruthy(a: Any) =
if a == 0 || a == "" || a == false then
false
else
true
Примеры показывают, как работает метод:
isTruthy(0) // false
isTruthy("") // false
isTruthy("hi") // true
isTruthy(1.0) // true
Использование match
в качестве тела метода
Довольно часто в качестве тела метода используются match
-выражения.
Вот еще одна версия isTruthy
, написанная с match
выражением:
def isTruthy(a: Any) = a match {
case 0 | "" | false => false
case _ => true
}
def isTruthy(a: Matchable) = a match
case 0 | "" | false => false
case _ => true
Этот метод работает точно так же, как и предыдущий, в котором использовалось выражение
if
/else
. ВместоAny
в качестве типа параметра используетсяMatchable
, чтобы принять любое значение, поддерживающее сопоставление с образцом (pattern matching).
См. дополнительную информацию о trait
Matchable
в Справочной документации.
Контроль видимости методов в классах
В классах, объектах, trait-ах и enum-ах методы Scala по умолчанию общедоступны,
поэтому созданный здесь экземпляр Dog
может получить доступ к методу speak
:
class Dog {
def speak() = println("Woof")
}
val d = new Dog
d.speak() // печатает "Woof"
class Dog:
def speak() = println("Woof")
val d = new Dog
d.speak() // печатает "Woof"
Также методы можно помечать как private
.
Это делает их закрытыми в текущем классе, поэтому их нельзя вызвать или переопределить в подклассах:
class Animal {
private def breathe() = println("I’m breathing")
}
class Cat extends Animal {
// этот метод не скомпилируется
override def breathe() = println("Yo, I’m totally breathing")
}
class Animal:
private def breathe() = println("I’m breathing")
class Cat extends Animal:
// этот метод не скомпилируется
override def breathe() = println("Yo, I’m totally breathing")
Если необходимо сделать метод закрытым в текущем классе, но разрешить подклассам вызывать или переопределять его,
метод помечается как protected
, как показано в примере с методом speak
:
class Animal {
private def breathe() = println("I’m breathing")
def walk() = {
breathe()
println("I’m walking")
}
protected def speak() = println("Hello?")
}
class Cat extends Animal {
override def speak() = println("Meow")
}
val cat = new Cat
cat.walk()
cat.speak()
cat.breathe() // не скомпилируется, потому что private
class Animal:
private def breathe() = println("I’m breathing")
def walk() =
breathe()
println("I’m walking")
protected def speak() = println("Hello?")
class Cat extends Animal:
override def speak() = println("Meow")
val cat = new Cat
cat.walk()
cat.speak()
cat.breathe() // не скомпилируется, потому что private
Настройка protected
означает:
- к методу (или полю) могут обращаться другие экземпляры того же класса
- метод (или поле) не виден в текущем пакете
- он доступен для подклассов
Методы в объектах
Ранее было показано, что trait-ы и классы могут иметь методы.
Ключевое слово object
используется для создания одноэлементного класса, и объект также может содержать методы.
Это хороший способ сгруппировать набор “служебных” методов.
Например, этот объект содержит набор методов, которые работают со строками:
object StringUtils {
/**
* Returns a string that is the same as the input string, but
* truncated to the specified length.
*/
def truncate(s: String, length: Int): String = s.take(length)
/**
* Returns true if the string contains only letters and numbers.
*/
def lettersAndNumbersOnly_?(s: String): Boolean =
s.matches("[a-zA-Z0-9]+")
/**
* Returns true if the given string contains any whitespace
* at all. Assumes that `s` is not null.
*/
def containsWhitespace(s: String): Boolean =
s.matches(".*\\s.*")
}
object StringUtils:
/**
* Returns a string that is the same as the input string, but
* truncated to the specified length.
*/
def truncate(s: String, length: Int): String = s.take(length)
/**
* Returns true if the string contains only letters and numbers.
*/
def lettersAndNumbersOnly_?(s: String): Boolean =
s.matches("[a-zA-Z0-9]+")
/**
* Returns true if the given string contains any whitespace
* at all. Assumes that `s` is not null.
*/
def containsWhitespace(s: String): Boolean =
s.matches(".*\\s.*")
end StringUtils
Методы расширения
Есть много ситуаций, когда необходимо добавить функциональность к закрытым классам.
Например, представьте, что у вас есть класс Circle
, но вы не можете изменить его исходный код.
Это может быть определено в сторонней библиотеке так:
case class Circle(x: Double, y: Double, radius: Double)
Если вы хотите добавить методы в этот класс, то можете определить их как методы расширения, например:
implicit class CircleOps(c: Circle) {
def circumference: Double = c.radius * math.Pi * 2
def diameter: Double = c.radius * 2
def area: Double = math.Pi * c.radius * c.radius
}
В Scala 2 используйте implicit class
, подробности здесь.
extension (c: Circle)
def circumference: Double = c.radius * math.Pi * 2
def diameter: Double = c.radius * 2
def area: Double = math.Pi * c.radius * c.radius
В Scala 3 используйте новую конструкцию extension
.
Дополнительные сведения см. в главах этой книги или в справочнике по Scala 3.
Теперь, когда у вас есть экземпляр Circle
с именем aCircle
,
то вы можете вызывать эти методы следующим образом:
aCircle.circumference
aCircle.diameter
aCircle.area
Дальнейшее изучение
Есть много чего, что можно узнать о методах, в том числе:
- Вызов методов в суперклассах
- Определение и использование параметров по имени
- Написание метода, который принимает параметр функции
- Создание встроенных методов
- Обработка исключений
- Использование входных параметров vararg
- Написание методов с несколькими группами параметров (частично применяемые функции).
- Создание методов с параметрами универсального типа.
Дополнительные сведения об этих функциях см. в других главах этой книги.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java