Advertisement
  1. Code
  2. Kotlin
Code

Kotlin从零开始:高级功能

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: More Fun With Functions
Kotlin From Scratch: Classes and Objects

Chinese (Simplified) (中文(简体)) translation by Zhang Xiang Liang (you can also view the original English article)

Kotlin是一种功能性语言,这意味着功能是前沿和中心。该语言包含了使编码功能变得简单和富有表现力的功能。在本文中,您将了解Kotlin中的扩展函数,高阶函数,闭包和内联函数。 在前面的文章中,您了解了Kotlin中的顶级函数,lambda表达式,匿名函数,本地函数,中缀函数以及最终成员函数。在本教程中,我们将继续深入了解Kotlin中的函数:

前面的文章中,您了解了Kotlin中的顶级函数,lambda表达式,匿名函数,本地函数,中缀函数以及最终成员函数。在本教程中,我们将继续深入了解Kotlin中的函数:

  • 扩展功能 
  • 更高阶的功能
  • 关闭
  • 内联函数

1.扩展函数

那岂不是很好,如果  String在Java类型都必须掌握的String中的第一个字母的方法,就像PHP中的ucfirst()?我们可以称这种方法upperCaseFirstLetter()。 

为了实现这一点,你可以创建一个继承StringStringJava子类。但请记住,StringJava 中的类是final的 - 这意味着你不能继承它。 Kotlin可能的解决方案是创建帮助程序函数或顶级函数,但这可能不太理想,因为我们无法利用IDE自动完成功能查看可用于此String类型的方法列表。真正好的是以某种方式向类中添加一个函数,而不必从该类继承。 真正好的是以某种方式向类中添加一个函数,而不必从该类继承。

那么,Kotlin让我们看到了另一个很棒的功能:  扩展功能。这些使我们能够使用新功能扩展一个类,而不必从该类继承。 这些使我们能够使用新功能扩展一个类,而不必从该类继承。换句话说,我们不需要创建新的子类型或更改原始类型。 

扩展函数在想要扩展的类外部声明。换句话说,它也是一个顶级函数(如果您想要复习Kotlin中的顶级函数,请参阅 本系列中的更多趣味函数教程)。 

除了扩展功能外,Kotlin还支持  扩展属性。在这篇文章中,我们将讨论扩展函数,我们将等到将来的帖子与Kotlin中的类一起讨论扩展属性。 

创建扩展功能

正如你在下面的代码中看到的那样,我们定义了一个顶层函数,因为我们声明了一个扩展函数。这个扩展函数位于一个名为com.chike.kotlin.strings的包中.

要创建扩展函数,必须在函数名称前面扩展您所要扩展类的名称。类名或定义扩展名的类型称为  接收者类型,  接收者对象  是调用扩展函数的类实例或值。

请注意,this函数体内的关键字引用了接收者对象或实例。 

调用扩展函数

创建扩展函数后,首先需要将扩展​​函数导入到其他包或文件中,以便在该文件或包中使用。然后,调用该函数就像调用接收器类型类的其他方法一样。

在上面的例子中,接收器类型是 String,接收器对象是"chike"。如果您使用的是IntelliJ IDEA等具有智能感知功能的IDE,则会在String类型中的其他功能列表中看到您的新扩展功能。 

IntelliJ IDEA intellisense feature

Java互操作性

请注意,在幕后,Kotlin将创建一个静态方法。这个静态方法的第一个参数是接收者对象。所以Java调用者很容易调用这个静态方法,然后将接收者对象作为参数传递。 

例如,如果我们的扩展函数是在一个StringUtils.kt文件中声明的,那么Kotlin编译器将使用静态方法 StringUtilsKtupperCaseFirstLetter() 创建一个Java类。 

这意味着Java调用者可以通过引用其生成的类来简单地调用该方法,就像其他任何静态方法一样。 

请记住,这个Java互操作机制与Kotlin中的顶级函数的工作方式类似,正如我们在更多趣味函数  文章中讨论的那样!

扩展函数与成员函数

请注意,扩展函数不能重写已经在类或接口中声明的函数(称为成员函数)(如果您想要复习Kotlin中的成员函数,请参阅本系列中的上一个教程)。 因此,如果您定义了一个具有完全相同函数签名的扩展函数 - 相同的函数名称和参数的相同数量,类型和顺序,无论返回类型如何,Kotlin编译器都不会调用它。 在编译过程中,当调用函数时,Kotlin编译器将首先在实例类型或其超类中定义的成员函数中查找匹配项。 如果有匹配,那么该成员函数就是被调用或绑定的成员函数。如果没有匹配,那么编译器将调用该类型的任何扩展函数。 

总之:成员函数总是赢。 

我们来看一个实际的例子。

在上面的代码中,我们定义了一个Student使用两个成员函数调用的类型:printResult()和expel()。然后我们定义了两个与成员函数名称相同的扩展函数。 

让我们调用printResult()函数并查看结果。 

正如你所看到的,被调用或绑定的函数是成员函数,而不是具有相同函数签名的扩展函数(虽然IntelliJ IDEA仍然会给你提示)。

但是,调用成员函数expel()和扩展函数expel(reason: String)会产生不同的结果,因为函数签名是不同的。 

成员扩展函数

大多数情况下,您都会将扩展函数声明为顶级函数,但请注意,您也可以将它们声明为成员函数。 

在上面的代码中,我们另一个类中类ClassA中声明了扩展函数exFunction() 的ClassB。所述调度接收器 是其中扩展被声明的类的实例,并且该接收器类型的扩展方法的实例被称为扩展接收器。 当调度接收器和扩展接收器之间存在名称冲突或映射时,请注意编译器选择扩展接收器。 

因此,在上面的代码示例中,扩展接收器是ClassB-so的一个实例  ,这意味着  toString() 方法ClassB 在扩展函数中调用时是类型的exFunction()。对于我们来调用调度接收器的toString() 方法,我们需要使用一个合格的:ClassAthis

2.  高阶函数 

高阶函数只是一个函数,它将另一个函数(或lambda表达式)作为参数,返回一个函数,或者同时执行这两个函数。所述last()收集功能是从标准库中的高阶函数的一个例子。 

在这里,我们将一个lambda传递给该last函数,以作为在元素子集内搜索的谓词。现在我们将开始在Kotlin中创建我们自己的高阶函数。 

创建一个高阶函数

看circleOperation()下面的函数  ,它有两个参数。第一个,  radius接受一个double,第二个,  op是一个接受double作为输入并返回一个double作为输出的函数 - 我们可以更简洁地说第二个参数是“从double到double的函数”。 

注意  op 函数的函数参数类型包含在圆括号中(),输出类型由箭头分隔。该函数  circleOperation() 是接受函数作为参数的高阶函数的典型示例。

调用一个高阶函数

在调用这个circleOperation()函数时,我们将另一个函数  calArea()传给它。(请注意,如果传递函数的方法签名与高阶函数声明的方法签名不匹配,则函数调用将不会编译。) 

要将calArea() 函数作为参数传递给circleOperation()我们,我们需要在前面  加上前缀  :: 并省略()括号。

明智地使用高阶函数可以使我们的代码更易于阅读并且更容易理解。 

Lambdas和高阶函数

我们也可以在调用函数时直接将lambda(或函数文字)传递给高阶函数: 

请记住,为避免显式指定参数,it只有在lambda具有一个参数的情况下,我们才能使用自动生成的参数名称。(如果您想要在Kotlin中重温lambda,请参阅 本系列中的More Fun With Functions教程)。

返回一个函数

请记住,除了接受函数作为参数之外,高阶函数还可以将函数返回给调用者。 

这里该multiplier()函数将返回一个函数,该  函数将给定因子应用于传递给它的任何数字。这个返回的函数是从double到double的lambda(或函数字面量)(意思是返回函数的输入参数是double类型,输出结果也是double类型)。  

为了测试这个,我们传入了两个因子,并将返回的函数赋值给变量doubler。我们可以像普通函数那样调用它,并且我们传入的任何值都会翻倍。

3.闭包

闭包是一个可以访问外部作用域中定义的变量和参数的函数。 

在上面的代码中,传递给filter()收集函数的lambda 使用length外部函数的参数printFilteredNamesByLength()。 请注意,此参数是在lambda范围外定义的,但lambda仍能够访问该参数length。这个机制是函数式编程中闭包的一个例子。

4.内联函数

在文章 More Fun With Functions中 ,我提到Kotlin编译器在创建lambda表达式时在后台创建了Java早期版本的匿名类。 

不幸的是,这种机制引入了开销,因为每次创建lambda时都会在引擎盖下创建一个匿名类。此外,使用外部函数参数或带闭包的局部变量的lambda会增加自己的内存分配开销,因为每次调用时都会将新对象分配给堆。 

将内联函数与正常函数进行比较

为了防止这些开销,Kotlin团队为我们提供了inline函数修饰符。inline代码编译期间,带修饰符的高阶函数将内联。 换句话说,编译器将复制lambda(或函数文字)以及更高阶的函数体并将它们粘贴到调用位置。 

我们来看一个实际的例子。 

在上面的代码中,我们有一个circleOperation()没有inline修饰符的高阶函数。现在让我们看看编译和反编译代码时生成的Kotlin字节码,然后将其与具有inline修饰符的字符编码进行比较。 

在上面生成的Java字节码中,您可以看到编译器circleOperation()在main()方法内调用了该函数。

现在我们来指定高阶函数inline,也可以看到生成的字节码。

为了制作一个内联的高阶函数,我们必须inline在fun关键字之前插入修饰符,就像我们在上面的代码中所做的一样。我们还要检查为这个内联函数生成的字节码。 

查看函数中内联函数的生成字节码main(),可以看到,不是调用circleOperation()函数,而是复制circleOperation()包含lambda体的函数体并将其粘贴到其调用位置。

通过这种机制,我们的代码已经得到显着优化 - 不再创建匿名类或额外的内存分配。 但要非常清楚,我们在后台会有比以前更大的字节码。出于这个原因,强烈建议只内联接受lambda作为参数的较小的高阶函数。 

Kotlin中的许多标准库高级函数都有内联修饰符。例如,如果你采取偷看在收集操作功能filter() 和first(),你会看到他们有inline修改,也是体积小

切记不要将不接受lambda作为参数的普通函数内联!他们会编译,但不会有显着的性能改进(IntelliJ IDEA甚至会提供此暗示)。 

noinline修饰符

如果函数中有两个以上的lambda参数,您可以选择使用noinline参数上的修饰符来决定哪些lambda不内联。这个功能特别适用于需要大量代码的lambda参数。换句话说,Kotlin编译器不会将该lambda复制并粘贴到被调用的地方,而是在场景后面创建一个匿名类。   在这里,我们将noinline修改器插入第二个lambda参数。请注意,此修饰符仅在函数具有inline修饰符时才有效。

在这里,我们将noinline修改器插入第二个lambda参数。请注意,此修饰符仅在函数具有inline修饰符时才有效。

在内联函数中跟踪堆栈

请注意,当在内联函数内引发异常时,堆栈跟踪中的方法调用堆栈与不带inline修饰符的普通函数不同。 这是因为编译器使用了内联函数的复制和粘贴机制。很酷的是,IntelliJ IDEA可以帮助我们轻松导航堆栈跟踪中的方法调用堆栈以实现内联函数。我们来看一个例子。

在上面的代码中,故意在内联函数中引发异常myFunc()。现在让我们看看代码运行时IntelliJ IDEA中的堆栈跟踪。 看下面的截图,你可以看到我们有两个导航选项可供选择:内联函数体或内联函数调用站点。选择前者会使我们注意到函数体中抛出的异常,而后者将使我们接近调用方法的地步。 选择前者会使我们注意到函数体中抛出的异常,而后者将使我们接近调用方法的地步。

IntelliJ IDEA stack trace for inline function

如果函数不是内联函数,我们的堆栈跟踪就像您可能已经熟悉的一样:

IntelliJ IDEA stack trace for normal function

结论

在本教程中,您了解了Kotlin的更多功能。我们涵盖:

  • 扩展函数
  • 更高阶的函数
  • 闭包
  • 内联函数

在Kotlin From Scratch系列的下一篇教程中,我们将深入研究面向对象的编程,并开始学习Kotlin中的类如何工作。再见!

要了解有关Kotlin语言的更多信息,我建议访问  Kotlin文档。或者在Envato Tuts +上查看我们的其他一些Android应用程序开发帖子!

Envato qr branded
关注我们的公众号
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.