读书笔记《functional-kotlin》有关函数的更多信息
在前面的章节中,我们介绍了 Kotlin 函数的许多特性。但是现在我们将扩展这些许多特性,其中大部分是从其他语言中借来的,但有一个新的转折,以完全适应 Kotlin 的总体目标和风格——类型安全和实用简洁。
一些功能,例如 Domain Specific Languages (DSLs),让开发人员将语言扩展到最初设计Kotlin时未考虑的领域。
在本章结束时,您将全面了解所有功能特性,包括:
- Extension functions
- Operator overloading
- Type-safe builders
- Inline functions
- Recursion and corecursion
function 可以有零个或多个参数。我们的函数basicFunction
有两个参数,如下代码所示:
每个参数定义为parameterName: ParameterType
,在我们的例子中,name: String
和大小:整数
。这里没有什么新鲜事。
当 parameters 有两种我们已经 涵盖——vararg
和 lambdas:
带有修饰符标记的参数的函数, vararg
可以用零个或多个值调用:
一个函数不能有多个 vararg
参数,即使是不同的类型也不行。
我们已经讨论过,如果函数的最后一个 parameter 是 lambda,那么它不能在 括号外传递 和花括号内,好像 lambda 本身就是一个控制结构体。
我们在 unless 函数">第 2 章,函数式编程入门,在第一节-类和高阶函数。让我们看一下下面的代码:
现在,如果我们结合 vararg
和 lambda 会发生什么?让我们在下面的代码片段中检查它:
Lambda 可以位于带有 vararg
参数的函数的末尾:
让我们来点冒险,一个 vararg
lambdas 参数:
我们不能在括号外传递一个 lambda,但我们可以在里面传递许多 lambda:
理想情况下,我们的函数不应该有太多参数,但并非总是如此。一些函数往往很大,例如数据类
构造函数(构造函数在技术上是一个返回新实例的函数)。
- They are hard to use. This can be alleviated or fixed with default parameters that we will cover in the next section, Default parameters.
- They are hard to read—named parameters to the rescue.
- They are probably doing too much. Are you sure that your function isn't too big? Try to refactor it and clean up. Look for possible side effects and other harmful practices. A special case is
data class
constructors, as they are just autogenerated assignments.
使用命名参数,您可以为任何函数调用添加可读性。
让我们以 data class
构造函数为例:
正常的调用将如下所示:
但包括命名参数将增加可供读者/维护者使用的信息并减少脑力劳动。我们还可以按对实际上下文更方便或更有意义的任何顺序传递参数:
命名参数与 vararg
参数结合使用时非常有用:
毫无疑问,Kotlin 的最佳特性之一是 extension 函数。扩展 functions 让您可以使用新函数修改现有类型:
要将扩展函数添加到现有类型,您必须在类型名称旁边写下函数名称,并用点 (.
) 连接。
在我们的示例中,我们将扩展函数 (sendToConsole()
) 添加到 String
类型。在函数体内,this
引用了 String
类型的实例(在这个扩展函数中, string
是接收者类型)。
除了点 (.
) 和 this
之外,扩展函数具有与普通函数相同的语法规则和特性。实际上,在幕后,扩展函数是一个普通函数,其第一个参数是接收器类型的值。所以,我们的 sendToConsole()
扩展函数等价于下面的代码:
所以,实际上,我们并没有用新函数修改类型。扩展函数是编写实用函数的一种非常优雅的方式,易于编写,使用起来非常有趣,而且易于阅读——双赢。这也意味着扩展函数有一个限制——它们不能访问 this
的私有成员,而适当的成员函数可以访问实例内的所有内容:
调用扩展函数与普通函数相同——使用接收器类型的实例(将在扩展中引用为 this
),按名称调用函数。 ;
当我们谈论继承时,成员函数和 extension 函数有很大的不同。
开放类 Canine
有一个子类Dog
。一个独立的函数, printSpeak
,接收 Canine
类型的参数并打印函数 speak(): 字符串
:
我们已经在 第 1 章,Kotlin – 数据类型、对象和类,在 继承 部分。带有 open
方法(成员函数)的开放类可以扩展并改变它们的行为。调用 speak
函数 会根据您的实例类型而有所不同。
printSpeak
函数可以被任何is-a< code class="literal">Canine,Canine
本身或任何子类:
如果我们执行这段代码,我们可以在控制台上看到:
虽然两者都是 Canine
,但 speak
的行为在这两种情况下是不同的,因为子类覆盖了父实现。
与前面的示例一样, Feline
是一个由 Cat
类扩展的开放类。但是 speak
现在是一个扩展函数:
扩展函数不需要标记为覆盖,因为我们没有覆盖任何东西:
如果我们执行这段代码,我们可以在控制台上看到:
在这种情况下,两个调用都会产生相同的结果。虽然一开始看起来很混乱,但一旦你分析了正在发生的事情,它就会变得清晰。我们调用了 Feline.speak()
函数 两次;这是 因为我们传递的每个参数都是 Feline
到 printSpeak(Feline)
函数:
如果我们执行这段代码,我们可以在控制台上看到:
在这种情况下,它的行为仍然与前面的示例相同,但为 name
使用了正确的值。说到这里,我们可以用 name
和 this.name
来引用 name
;两者都是有效的。
扩展函数可以声明为类的成员。声明了 extension 函数的类的实例称为 调度接收器.
Caregiver
内部开放类 定义了 extension 两个不同的函数类,Feline
和 Primate
:
这两个扩展函数都打算在 Caregiver
的实例中使用。实际上,如果成员扩展函数未打开,则将其标记为私有是一种很好的做法。
对于 Primate.react()
,我们使用 Primate 的
和 name
值Caregiver
中的 name
值。要访问具有名称冲突的成员,扩展接收器 (this
) 具有优先权,并且要访问调度程序接收器的成员,合格的 this
语法。没有名称冲突的调度程序接收器的其他成员可以在没有限定的
this
的情况下使用。
不要对我们已经介绍过的 this
的各种方式感到困惑:
- Inside a class,
this
means the instance of that class - Inside an extension function,
this
means the instance of the receiver type like the first parameter in our utility function with nice syntax:
回到我们的 Zoo 示例, 我们实例化一个 Caregiver
,一个 Cat
和一个 Primate
,我们用两个动物实例调用函数 Caregiver.takeCare
:
如果我们执行这段代码,我们可以在控制台上看到:
任何动物园都需要兽医。 Vet
类扩展了 Caregiver
:
我们用 different 实现覆盖了 Feline.react()
函数。我们也直接使用 Vet
类的名称,因为 Feline
类没有属性名称:
之后,我们得到以下输出:
Worker
类有一个函数 work(): String
和一个私有函数 rest() :字符串
。我们还有两个具有相同签名的扩展函数,work
和 rest
:
具有相同签名的扩展函数不是编译错误,而是一个警告:Extension is shadowed by a member: public final fun work(): String
声明与成员函数具有相同签名的函数是合法的,但成员函数始终具有优先权,因此永远不会调用扩展函数。当成员函数为私有时,此行为会发生变化,在这种情况下,扩展函数优先。
也可以使用扩展函数重载现有的成员函数:
在执行时, work()
调用成员函数和 work(String)
和 rest()
是扩展函数:
只有一个参数的函数(普通或扩展)可以被标记 作为 infix
并与 infix
表示法一起使用。 infix
表示法对于自然地表达某些领域的代码很有用,例如数学和代数运算。
让我们为 Int
类型添加一个infix
扩展函数,superOperation
(这只是一个带有花哨名称的常规总和):
我们可以将 superOperation
函数与中缀
表示法或普通表示法一起使用。
infix
符号常用的另一个领域是 assertion 库,例如 HamKrest (https://github.com/npryce/hamkrest) 或 Kluent (https://github.com/MarkusAmshove/Kluent)。用自然、易于理解的语言编写规范代码是一个巨大的优势。
Kluent 断言看起来像自然的英语表达:
Kluent 还附带了一个反引号版本,以提高可读性:
反引号 (`
) 让您可以编写任意标识符,包括在 Kotlin 中保留的单词。现在,您可以编写自己的颜文字功能了:
您可以链接许多 infix
函数来生成内部 DSL,或重新创建经典模因:
your
function, receives Pair<Base, Us>
作为参数(一种Kotlin 标准库中广泛使用的元组)和 infix
扩展函数
返回一个 Pair
使用接收者作为第一个成员,参数作为第二个参数(to
可以使用任何类型的组合来调用)。
运算符重载是多态的一种形式。一些 operators 会改变不同类型的行为。经典的例子是运算符加号 (+
)。在数值上,加号是求和运算,在 String
上是串联。运算符重载是一个有用的工具,可以为您的 API 提供一个自然的表面。假设我们正在编写一个 Time
和 Date
库;在时间单位上定义加号和减号运算符是很自然的。
Kotlin 允许您在自己的或现有类型上定义操作符的行为,使用函数、普通或扩展,用 operator
修饰符标记:
运算符函数 plus 返回一个 Pack
值。要调用它,可以使用 infix
运算符方式(Wolf + Wolf
)或普通方式(Wolf.plus(Wolf)
)。
关于 Kotlin 中的运算符重载需要注意的一点——您可以在 Kotlin 中覆盖的运算符是有限的;您不能创建任意运算符。
二元运算符receive一个参数(这个规则有例外——invoke
和索引访问)。
Pack.plus
扩展函数接收 Wolf
参数并返回 一个新的 Pack
。请注意,MutableMap
也有一个加号 (+
) 运算符:
下表将向您展示所有可以重载的可能二元运算符:
操作员 |
等效 |
备注 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
从 Kotlin 1.1 开始,以前是 |
|
|
|
|
|
|
|
|
|
|
|
必须返回 |
|
|
必须返回 |
|
|
必须返回 |
|
|
必须返回 |
|
|
从 Kotlin 1.1 开始,以前是 |
|
|
检查 |
|
|
检查 |
|
|
必须返回 |
|
|
必须返回 |
|
|
必须返回 |
|
|
必须返回 |
回到 第 2 章,函数式编程入门,在 一等函数和高阶函数部分, 当我们介绍 lambda 函数时,我们展示了 Function1
的定义:
invoke
函数是一个运算符,一个 curious 之一。 invoke
运算符可以在没有 调用 >名称
。
Wolf
类有一个 invoke
运算符:
这就是为什么我们可以用括号直接调用 lambda 函数;实际上,我们正在调用 invoke
运算符。
下表将向您展示具有许多不同参数的 invoke
的不同声明:
运算符 |
等效 |
备注 |
|
|
|
|
|
|
|
|
|
|
|
indexed 访问操作符是数组读写operations 带方括号 ([]
),用于具有类 C 语法的语言。在 Kotlin 中,我们使用 get
运算符 进行读取,使用 set
进行写入。
使用 Pack.get
运算符,我们可以将 Pack
用作数组:
大多数 Kotlin 数据结构都定义了 get
运算符,在这种情况下,Map<K, V>
返回一个V?
。
下表将向您展示具有不同数量参数的 get
的不同声明:
操作员 |
等效 |
备注 |
|
|
|
|
|
|
|
|
set
运算符 有类似的语法:
Note
运算符 get
和 set
可以有任意代码,但索引访问是一个非常著名且古老的约定用于阅读和写作。当您编写这些运算符时(顺便说一下,所有其他运算符也是如此),请使用 最少意外 的原则。将运算符限制在特定域上的自然含义,从长远来看使它们更易于使用和阅读。
下表将向您展示具有不同数量参数的 set
的不同声明:
操作员 |
等效 |
备注 |
|
|
返回值被忽略 |
|
|
返回值被忽略 |
|
|
返回值被忽略 |
我们可以在 Wolf
类中添加 not
运算符:
下表将向您展示所有可能的一元 operators 可以重载:
操作员 |
等效 |
备注 |
|
|
|
|
|
|
|
|
|
|
|
后缀,它必须是对 |
|
|
后缀,它必须是对 |
|
|
前缀,它必须是对 |
|
|
前缀,它必须是对 |
后缀(增量和减量)返回原始值,然后使用运算符返回值更改变量。 Prefix 返回运算符的返回值,然后使用该值更改变量。
通过前面两节(infix
函数和运算符重载),我们为构建出色的 DSL 奠定了良好的基础。 DSL 是一种专业化 到特定的 域,与通用语言
(GPL)。 DSL 的经典示例(即使人们没有意识到)是 HTML(标记)和 SQL(关系数据库查询)。
Kotlin 提供了许多功能来创建内部 DSL(一种在主机 GPL 内部运行的 DSL),但是我们仍然需要介绍一个功能,即类型安全构建器。类型安全构建器让我们以(半)声明方式定义数据,并且对于定义 GUI 非常有用, HTML 标记、XML 等。
TornadoFX 是一个漂亮的 Kotlin DSL 示例。 TornadoFX (https:// tornadofx.io/) 是用于创建 JavaFX 应用程序的 DSL。
我们编写一个 FxApp
类 它扩展了 tornadofx.App
并接收一个tornadofx。查看
类(类引用,而不是实例):
在不到 20 行代码中,包括导入和主函数,我们可以创建一个 GUI 应用程序:
当然,现在它什么也没做,但是如果你将它与 Java 进行比较,使用 TornadoFX 创建一个 JavaFX 应用程序很简单。有 JavaFX 经验的人可能会说,您可以使用 FXML(一种旨在构建 JavaFX 布局的声明性 XML 语言)来实现类似的东西,但与任何其他 XML 文件一样,编写和维护都很困难,而且 TornadoFX 的 DSL 更简单、灵活,并且使用 Kotlin 的类型安全编译。
但是类型安全的构建器是如何工作的呢?
让我们从 Kotlin 标准库中的一个示例开始:
我们可以在其他语言中找到 with
块,例如 JavaScript 和 Visual Basic(包括 .Net)。 with
块是一种语言结构,它允许我们对作为参数传递的值使用任何属性或方法。但是在 Kotlin 中,with
不是保留关键字,而是带有特殊类型参数的普通函数。
让我们看一下 with
声明:
第一个参数是 T
类型的任何值,一个接收器(在扩展函数中?),第二个参数是 block
,是 T.() -> 类型的函数R
。在 Kotlin 的文档中,这种函数被命名为 function type with receiver and with any instance of
T,< /code> 我们可以调用
block
函数。不用担心 inline
修饰符,我们将在下一节中介绍它。
另一个例子呢?让我们看一下:
buildString
函数接收 StringBuilder.() -> Unit
参数并返回一个 String
;声明非常简单:
apply
函数是类似于 with
的扩展函数,但不是返回 R
,返回接收者实例。通常,apply
用于 initializing 和 实例:
如您所见,所有这些函数都非常易于理解,但它们大大提高了 Kotlin 的实用性和可读性。
我最大的爱好之一是骑自行车。 emotion 运动、努力、健康益处和享受风景是其中的一些好处(我可以保持持续不断)。
我想创建一种方法来注册我的自行车及其组件。对于原型阶段,我将使用 XML,但稍后我们可以更改为不同的实现:
这是在 Kotlin 中创建类型安全构建器的完美场景。
最后,我的 bicycle
DSL 应该是这样的:
我的 DSL 是常规的 Kotlin 代码,编译速度很快,我的 IDE 会帮助我自动完成,当我犯错时会抱怨——这是一个双赢的局面。
让我们从程序开始:
我的 DSL 中的 bicycle
的所有部分都将扩展/实现 Element
接口:
Part
是我所有部分的基类;它具有 children
和 attributes
属性;它还继承了带有 XML 实现的 Element
接口。更改为不同的格式(JSON、YAML 等)应该不会太困难。
initElement
函数接收两个参数,一个元素 T
和一个 init
函数与接收者 T.() ->单位
。在内部,执行
init
函数并将元素添加为子元素。
Part
使用 @ElementMarker
注释进行注释,该注释本身也使用 @DslMarker
。它防止内部元素到达外部元素。
在这个例子中,我们可以使用 frame
:
仍然可以使用 this
qualified 明确地做到这一点:
现在,列举几个描述材料、杆类型和制动器的例子:
其中一些部分具有 material
属性:
我们使用 Material
枚举类型的 material
属性,并将其存储在 attributes< /code> 映射,来回转换值:
Bicycle
定义了一个 description
函数和 frame
的函数,fork
和 bar
。每个函数接收一个 init
函数,我们直接将其传递给 initElement
。
Frame
后轮有一个功能:
Wheel
有一个属性 brake
使用Brake
枚举:
Bar
有一个 property 用于它的类型,使用 < code class="literal">BarType 枚举:
Fork
为前轮定义一个函数:
我们已经接近尾声了,我们现在唯一需要的是 DSL 的入口函数:
就这样。 Kotlin 中的 DSL 具有 infix
函数、运算符重载和类型安全的构建器非常强大,而且 Kotlin 社区每天都在创建新的和令人兴奋的库。
高阶函数是非常 有用且花哨,但它们带有一个警告——性能损失。请记住,来自 第 2 章,函数式编程入门,在一等函数和高阶函数,在编译时,一个 lambda 被翻译成一个分配的对象,我们调用它的 invoke
操作符;这些操作会消耗 CPU 功率和内存,无论它们有多小。
像这样的函数:
编译后,它将如下所示:
如果性能是您的首要任务(关键任务应用程序、游戏、视频流),您可以将高阶函数标记为 inline
:
编译后,它将如下所示:
整个函数执行被高阶函数的主体和 lambda 的主体所取代。 inline
函数更快,尽管生成更多字节码:
每次执行 2.3 毫秒看起来并不多,但从长远来看,通过更多优化,可以产生明显的复合效果。
内联 lambda 函数有一个 important 限制——它们不能以任何方式(存储、复制等)进行操作。
UserService
存储了一个监听器列表 (User) ->单位
:
将 addListener
改为 inline
函数会产生编译错误:
如果你仔细想想,这是有道理的。当我们内联一个 lambda 时,我们将其替换为它的主体,这不是我们可以存储在 Map
上的东西。
我们可以使用 noinline
修饰符来解决这个问题:
在 inline
函数上使用 noinline
只会内联高阶函数体,而不是 noinline
lambda 参数(inline
高阶函数可以同时具有:inline
和 noinline
lambdas)。生成的字节码不如完全内联函数快,编译器会显示警告。
内联 lambda 函数不能在另一个执行上下文(本地对象、嵌套 lambda)中使用。
在这个例子中,我们不能在 buildUser
lambda 中使用 transform
:
为了解决这个问题,我们需要一个 crossinline
修饰符(或者,我们可以使用 noinline
但会损失相关的性能):
- A class that extends
(String) -> User
to representbuildUser
and internally createsUser
usingString::toLowerCase
to transform the name - A normal inline code to execute
List<User>.map()
using an instance of the class that representsbuildUser
List<T>.map()
isinline
, so that code gets generated too
一旦你意识到它的限制,内联高阶函数是提高代码执行速度的好方法。事实上,Kotlin 标准库中的许多高阶函数都是内联
。
在 第 2 章中,开始使用函数式编程,在递归部分,我们广泛地介绍了递归(尽管有是本书范围之外的递归主题)。
我们使用 recursion 来编写斐波那契等经典算法 (我们正在重用 tailrecFib< /code> 来自 第 2 章,函数式编程入门):
和阶乘(这里相同,重用 tailrecFactorial ="ch02">第 2 章,函数式编程入门):
在这两种情况下,我们都从一个数字开始,然后我们将其减少以达到基本条件。
我们查看的另一个示例是 FunList
:
forEach
和 fold
函数是递归的。从完整列表开始,我们减少它直到我们到达最后(用 Nil
表示),基本情况。其他函数——reverse
、foldRight
和 map
只是使用 < code class="literal">fold 有不同的变体。
因此,一方面,递归采用复杂值并将其简化为所需的答案,另一方面,corecursion 采用一个值并在其之上构建以产生一个复合值(包括可能无限的数据结构,例如 Sequence<T>
)。
当我们使用 fold
函数进行递归操作时,我们可以使用 unfold
函数:
unfold
函数有两个参数,一个初始 S
值表示开始或基本步骤,以及一个 f
lambda 采用 S
步骤并生成 Pair<T, S>?
(a nullable Pair
) T
值 添加到序列和下一个 S
步骤。
如果 f(s)
的结果为空,我们返回一个空序列,否则我们创建一个单值序列并添加 unfold 的结果
与新步骤。
使用 unfold,
我们可以创建一个重复单个元素多次的函数:
elements
函数采用元素重复任意数量的值。在内部,它使用 unfold
,传递 1
作为初始步骤和一个 lambda,它采用当前步骤并将其与 numOfValues
,返回 Pair<T, Int>
与相同元素和当前步+ 1
或 null。
没关系,但不是很有趣。返回阶乘序列怎么样?我们为您服务:
同样的原则,唯一的区别是我们的初始步骤是 Pair
Pair<Long, Pair<Long, Int>>
。
斐波那契看起来类似:
除了在这种情况下,我们使用 Triple<Long, Long, Int>
。
生成 Factorial 和 Fibonacci 序列的核心递归实现是分别计算 Factorial 或 Fibonacci 数的递归实现的镜像——有些人认为这更容易理解。