scalabook
Методы расширения - детали
Методы расширения (extension methods) позволяют добавлять методы к типу после его определения,
т.е. позволяют добавлять новые методы в закрытые классы.
Например, предположим, что кто-то другой создал класс Circle
:
case class Circle(x: Double, y: Double, radius: Double)
Теперь представим, что необходим метод circumference
, но нет возможности изменить исходный код Circle
.
До того как концепция вывода терминов была введена в языки программирования,
единственное, что можно было сделать, это написать метод в отдельном классе или объекте, подобном этому:
object CircleHelpers: def circumference(c: Circle): Double = c.radius * math.Pi * 2
Затем этот метод можно было использовать следующим образом:
val aCircle = Circle(2, 3, 5)// aCircle: Circle = Circle(x = 2.0, y = 3.0, radius = 5.0)CircleHelpers.circumference(aCircle)// res0: Double = 31.41592653589793
Методы расширения позволяют создать метод circumference
для работы с экземплярами Circle
:
extension (c: Circle) def circumference: Double = c.radius * math.Pi * 2
В этом коде:
Circle
— это тип, к которому будет добавлен метод расширенияcircumference
- Синтаксис
c: Circle
позволяет ссылаться на переменнуюc
в методах расширения
Затем в коде метод circumference
можно использовать так же, как если бы он был изначально определен в классе Circle
:
aCircle.circumference// res1: Double = 31.41592653589793
Импорт методов расширения
Представим, что circumference
определен в пакете lib
- его можно импортировать с помощью
import lib.circumferenceaCircle.circumference
Если импорт отсутствует, то компилятор выводит подробное сообщение об ошибке, подсказывая возможный импорт, например так:
value circumference is not a member of Circle, but could be made available as an extension method.
The following import might fix the problem:
import lib.circumference
Операторы
Синтаксис метода расширения также можно использовать для определения операторов. Примеры:
extension (x: String) def < (y: String): Boolean = ...extension (x: Elem) def +: (xs: Seq[Elem]): Seq[Elem] = ...extension (x: Number) infix def min (y: Number): Number = ...
"ab" < "c"1 +: List(2, 3)x min 3
Generic расширения
Также возможно расширить generic типы, добавив параметры типа в расширение. Например:
extension [T](xs: List[T]) def second = xs.tail.head
extension [T: Numeric](x: T) def + (y: T): T = summon[Numeric[T]].plus(x, y)
Параметры типа в расширениях также можно комбинировать с параметрами типа в самих методах:
extension [T](xs: List[T]) def sumBy[U: Numeric](f: T => U): U = ...
Аргументы типа, соответствующие параметрам типа метода, передаются стандартно:
List("a", "bb", "ccc").sumBy[Int](_.length)
Напротив, аргументы типа, соответствующие параметрам типа extension
, могут быть переданы,
только если на метод ссылаются как на метод без расширения:
sumBy[String](List("a", "bb", "ccc"))(_.length)
Или при передаче обоих аргументов типа:
sumBy[String](List("a", "bb", "ccc"))[Int](_.length)
Расширения также могут принимать предложения using
.
Например, приведенное выше расширение +
можно было бы записать с предложения using
:
extension [T](x: T)(using n: Numeric[T]) def + (y: T): T = n.plus(x, y)
Коллективные расширения
Ключевое слово extension
объявляет о намерении определить один или несколько методов расширения для типа,
заключенного в круглые скобки.
Чтобы определить для типа несколько методов расширения, используется следующий синтаксис:
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
Коллективные расширения, подобные этим, являются сокращением для индивидуальных расширений, где каждый метод определяется отдельно. Например, расширение выше - это:
extension (c: Circle) def circumference: Double = c.radius * math.Pi * 2
extension (c: Circle) def diameter: Double = c.radius * 2
extension (c: Circle) def area: Double = math.Pi * c.radius * c.radius
Коллективные расширения также могут принимать параметры типа и содержать предложения using
.
Пример:
extension [T](xs: List[T])(using Ordering[T]) def smallest(n: Int): List[T] = xs.sorted.take(n) def smallestIndices(n: Int): List[Int] = val limit = smallest(n).max xs.zipWithIndex.collect { case (x, i) if x <= limit => i }
Вызов методов расширения
Чтобы преобразовать ссылку в метод расширения, компилятор должен знать о методе расширения. В этом случае говорится, что метод расширения применим в точке отсчета.
Существует четыре возможных способа применения метода расширения:
- метод расширения виден под простым именем, будучи определенным, унаследованным или импортированным в области, охватывающей ссылку.
- метод расширения является членом некоторого экземпляра
given
, видимого в точке ссылки. - ссылка имеет форму
r.m
, а метод расширения определен в неявной области видимости типаr
. - ссылка имеет форму
r.m
, а метод расширения определен в некотором экземпляреgiven
в неявной области видимости типаr
.
Вот пример первого правила:
trait IntOps: extension (i: Int) def isZero: Boolean = i == 0
extension (i: Int) def safeMod(x: Int): Option[Int] = // метод расширения, определенный в той же области видимости - IntOps if x.isZero then None else Some(i % x)
object IntOpsEx extends IntOps: extension (i: Int) def safeDiv(x: Int): Option[Int] = // метод расширения, введенный в область видимости через наследование от IntOps if x.isZero then None else Some(i / x)
trait SafeDiv: import IntOpsEx.*
extension (i: Int) def divide(d: Int): Option[(Int, Int)] = // методы расширения импортированы и, следовательно, находятся в области видимости (i.safeDiv(d), i.safeMod(d)) match case (Some(d), Some(r)) => Some((d, r)) case _ => None
Согласно второму правилу, метод расширения можно сделать доступным,
определив экземпляр given
, содержащий его, например так:
given ops1: IntOps() // приносит safeMod в область видимости1.safeMod(2)
По третьему и четвертому правилу метод расширения доступен,
если он находится в неявной области действия типа получателя или в экземпляре given
в этой области.
Пример:
class List[T]: ...object List: ... extension [T](xs: List[List[T]]) def flatten: List[T] = xs.foldLeft(List.empty[T])(_ ++ _)
given [T: Ordering]: Ordering[List[T]] with extension (xs: List[T]) def < (ys: List[T]): Boolean = ...end List
// метод расширения доступен, поскольку он находится в неявной области видимости List[List[Int]]List(List(1, 2), List(3, 4)).flatten
// метод расширения доступен, поскольку он находится в given Ordering[List[T]], // который сам находится в неявной области видимости List[Int]List(1, 2) < List(3)
Ссылки: