第3章函数的定义和调用

262 阅读5分钟

第3章函数的定义和调用.png

3.1、在Kotlin中创建集合

kotlin没有采用自己的集合类,采用的是标准的java集合类

示例

  //创建集合
  fun setCollection(){
    val set= hashSetOf(1,7,53)
    //创建list
    val list= arrayListOf(1,7,53)
  
    //创建map
    //to不是一个特殊的结构,而是一个普通的函数
    val map= hashMapOf(1 to "one",7 to "seven",53 to "fifty_three")
  
    for(number in set){
        println(number)
    }
    for(  (index,number)   in map){
        println("$index,$number")
    }
    //查找对象所属的类型
    println(set.javaClass)
    println(list.javaClass)
    println(map.javaClass)
    //class java.util.HashSet
    //class java.util.ArrayList
    //class java.util.HashMap
  
    //获取元素的最后一个元素
    println("元素最后一个元素:${list.last()}")
    println("元素最大值:${list.max()}")
  }

3.2、让函数更好调用

命名参数

  • 例子

     //自定义打印toString
     fun <T> joinToString(collection:Collection<T>,separator:String,prefix:String,postfix:String):String{
     val result=StringBuilder(prefix)
     
     for((index,element) in collection.withIndex()){
         if(index>0)
             result.append(separator)
         result.append(element)
     }
     result.append(postfix)
     
     return result.toString()
     }
    
  • 在调用一个Kotlin定义的函数时,可以显式地表明一些参数的名称,如果在调用一个函数时,指明了一个参数的名称,为了避免混淆,它之后所遇参数都需要表明名称

     println(joinToString(list,prefix = "&",postfix = "&",separator ="." ))
    

默认参数值

  • Kotlin中,可以在声明函数的时候,指定参数的默认值

  • 参数的默认值是被编码到被调用的函数中,而不是被调用的地方

  • 例子

     //自定义打印toString,默认参数值
     fun <T> joinToString2(collection:Collection<T>,separator:String=",",prefix:String="",postfix:String=""):String{
     val result=StringBuilder(prefix)
     
     for((index,element) in collection.withIndex()){
         if(index>0)
             result.append(separator)
         result.append(element)
     }
     result.append(postfix)
     
     return result.toString()
     }
    

消除静态工具类:顶层函数和属性

  • 在Kotlin中,不需要静态工具类,可以把函数直接放到代码文件的顶层,不用从属于任何的类

  • 这些放在文件顶层的函数依然是包内的成员

  • 属性也可以放到文件的顶层

  • 顶层函数

    • 声明joinToString()作为顶层函数

      package 三.函数的定义与调用
      
      //顶层函数,声明在类的外面
      fun joinToString(name:String):String{
          return "helloworld"
      }
      class Join {
      }
      
    • 这会怎么运行呢?当编译这个文件的时候,会生成一些类,因为JVM只能执行类中的代码,当你在使用Kotlin的时候,知道这些就够了,如果需要从Java中来调用这些函数,就必须理解它将会怎样被编译,为了方便理解,我们来看一段代码,这里它会编译成相同的类

      package 三.函数的定义与调用;
           
      public class JoinKt {
      public static String joinToString(String name){
         return "helloworld";
         }
      }
      
    • 可以看到Kotlin编译生成的类的名称,对应于包含函数的文件的名称。这个文件中的所有顶层函数编译为这个类的静态函数。因此,当从Java调用这个函数的时候,和调用任何其他静态函数一样非常简单

       public class JavaTest {
       public void test(){
          JoinKt.joinToString("heloworld");
       }
       }
      
    • 所有顶层函数编译为这个类的静态函数

  • 修改文件类名

    • 要改变包含Kotlin顶层函数的生成的类的名称,需要为这个文件添加@JvmName的注解,将其放到这个文件的开头,位于包名的前面

       @file:JvmName("StringFunctions")
       package 三.函数的定义与调用
       
       //顶层函数,声明在类的外面
       fun joinToString(name:String):String{
           return "helloworld"
       }
       class Join {
       }
      
  • 在java中调用

     public class JavaTest {
     public void test(){
          StringFunctions.joinToString("heloworld");
       }   
     }
    
  • 顶层属性

    • 和函数一样,属性也可以放到文件的顶层

      在一个类的外面保存单独的数据片段虽然不常用,但还是有它的价值

    • 代码

       @file:JvmName("StringFunctions")
       package 三.函数的定义与调用
       
       //顶层函数,声明在类的外面
       fun joinToString(name:String):String{
           return "helloworld"
       }
       //顶层属性,声明在类的外面
       //const val  opCount=0
       var opCount=0
       
       class Join {
           fun performOperation(){
               //方法里面引用顶层属性
               opCount++
               println("当前count值:$opCount")
           }
       }
      
    • 默认情况下,顶层属性和其他任意属性一样,是通过访问器暴露给Java使用的(如果是val就只有一个getter,如果是var就对应一对getter和setter)

      • const关键字

        • 为了方便使用,如果你想要把一个常量以public static final的属性暴露给Java,可以用const来修饰(这个适用于所有的基本数据类型的属性,以及String类型)。(我的理解如果不用const修饰,那么会提供一个getter方法,而这种方式和在java中static中的方式就有点不一样了,java中的static final是没有getter方法的,所以用const来修饰,可能是去掉了getter这个方法,然后可以直接访问这个属性)

          //顶层属性,声明在类的外面
          const val  opCount=0
          

          const必须写在val前面,而且不能用var修饰,等同于:public static final int opCount=0;

3.3、给别人的类添加方法:扩展函数和属性

导言

  • 这个标题很明显了,是给已写好的类添加一些扩展的函数,在已有的基础上添加一些自己的函数或者属性

使用步骤

  • 扩展函数定义在类的外面
  • 函数声明格式:
    1. 在函数名称前面加要扩展的类,比如这里是扩展java的String类,在函数前面加String.,这个叫做接收者类型;
    2. this为接收者对象,用来调用扩展函数的对象

代码

  • package 三.函数的定义与调用
    
    /**
     * 注意:扩展的函数,要定义在类的外面
     */
    fun String.lastChar():Char=this.get(this.length-1)
    
    /**
     * 扩展的类
     */
    class ExtendUtils {
     fun test(){
           println("Kotlin".lastChar())
       }
     }
    

    接收者类型

    String为接收者类型,“Kotlin”为接收者对象

  • 自己写的扩展函数可以写在任何一个.kt文件中:

    1. 函数要声明在类的外面;
    2. 函数名称前面加了扩展类的类型String.
    3. 接收者对象this

    这就相当于给String类增加了一个额外的函数,这个函数其他类可以直接使用,java类也可以直接使用

注意

  • 在扩展函数中,可以直接访问被扩展的类的其他方法和属性
  • 扩展函数并不允许你打破它的封装性,不能访问私有的或者是受保护的成员

导入和扩展函数

  • 对于你定义的一个扩展函数,它不会自动地在整个项目范围内生效。相反,如果你要使用它,需要进行导入,就像其他任何的类或者函数一样

  • 为了避免偶然性的命名冲突,Kotlin允许用和导入类一样的语法来导入单个的函数

     import 三.函数的定义与调用.lastChar
     val c="Kotlin".lastChar()
    
  • 使用关键字as来修改导入的类或者函数名称

     import 三.函数的定义与调用.lastChar as last
     val c="Kotlin".last()
    

    当你在不同的包中,有一些重名的函数时,在导入时给它重命名就显得很有必要了,这样可以在同一个文件中去使用它们。在这种情况下,对于一般的类和函数,还有一个选择,可以选择用全名来指出这个类或者函数

从java中调用扩展函数

  • java调用的时候,kotlin文件ExtendUtils变成了ExtendUtilsKt,多了一个Kt的后缀,代码如下:

     ExtendUtilsKt.lastChar("哈哈");
    

作为扩展函数的工具函数

  • 扩展函数无非就是静态函数的一个高效的语法糖,可以使用更具体的类型来作为接收者类型,而不是一个类

不可重写的扩展函数

  • 扩展函数的静态性质决定了扩展函数不能被子类重写
  • Kotlin会把扩展函数作为静态函数对待

扩展属性

  • 扩展属性提供了一种方法,用来扩展类的API,可以用来访问属性,用的是属性语法而不是函数的语法

  • 例子

     val String.lastChar:Char get()=get(length-1)
    
  • 可以看到,和扩展函数一样,扩展属性也像接收者的一个普通的成员属性一样。

  • 这里,必须定义getter函数,因为没有支持字段,因此没有默认的getter的实现,同理,初始化也不可以,因为没有地方存储初始值

  • 如果在StringBuilder上定义一个相同的属性,可以置为var,因为StringBuilder的内容是可变的

     //声明一个可变的扩展属性
     var StringBuilder.lastChar:Char 
     get()=get(length-1)
     set(value:Char){
         this.setCharAt(length-1,value)
     }
    
     //可以像访问使用成员属性一样访问它
     println("Kotlin".lastChar())
     val sb=StringBuilder("Kotlin?")
     sb.lastChar='!'
     println(sb)
     
     >>
     n
     Kotlin!
    
  • 注意

    当从Java中访问扩展属性的时候,应该显式的调用它的getter函数:StringUtilKt.getLastChar("Java")

3.4、处理集合:可变参数、中缀调用和库的支持

扩展java集合的api

  • 本章前提,是基于Kotlin中的集合与Java的类相同,但对API做了扩展,可以看一个示例,用来获取列表中最后一个元素并找到数字集合中的最大值:

     val strings:List<String> = listOf("first","second","fourteenth")
     println(strings.last())
     
     val numbers:Collection<Int> = setOf(1,14,2)
     println(numbers.max())
     
     fourteenth
     14
    
  • 为什么在Kotlin中能对集合有这么多丰富的操作,答案很明显了:因为被声明为了扩展函数。许多扩展函数都在Kotlin标准库中都有声明

可变参数:让函数支持任意数量的参数

  • 当你在调用一个函数来创建列表的时候,可以传递任意个数的参数给它:

    var list=listOf(2,3,5,7,11)
    
  • 查看这个函数在库当中的声明,可以发现

    fun listOf<T>(vararg values:T):List<T>{...}
    
  • Kotlin可变参数与java类似,但语法略有不同:Kotlin在该类型之后不再使用三个点,而是在参数上使用vararg修饰符

  • 展开运算符

    • 在java中,可以按原样传递数组,而Kotlin则要求你显式的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。从技术的角度来讲,这个功能被称为展开运算符,使用的时候,在对应的参数前面加一个*

    展开运算符

       这个示例展示了,通过展开运算符,可以在单个调用中组合来自数组的值和某些固定值,这在Java中并不支持

键值对的处理:中缀调用和解构声明

  • 可以使用mapOf函数创建map

    val map=mapOf(1 to "one",7 to "seven",53 to "fifty-three")
    
  • 单词to不是内置的结构,而是一种特殊的函数调用,被称为中缀调用

  • 在中缀调用中,没有添加额外的分割符,函数名称是直接放在目标对象名称和参数之间的,以下两种调用方式是等价的

    1.to("one")
    

    一般to函数的调用

     1 to “one”
    

    使用中缀符号调用to函数

  • 中缀调用可以与只有一个参数的函数一起使用,无论是普通的函数还是扩展函数。要允许使用中缀符号调用函数,需要使用infix修饰符来标记

    ​ 下面是一个简单的to函数的声明

     infix fun Any.to(other:Any)=Pair(this,other)
    

    to函数会返回一个Pair类型的对象,Pair是Kotlin标准库中的类,它用来表示一对元素,Pair和to的声明都用到了泛型,简单起见,这里省略了泛型

    可以直接用Pair的内容来初始化两个变量

    val (number,name) = 1 to "one"
    

    Kotlin允许一次声明多个变量 ,此技术成为解构声明, 如图展示了它如何与Pair一起使用:

    解构声明

    • 也就是1 to “one”创建了一个Pair对象,然后用解构声明来展开了,分别赋值给了number和name变量

    • 解构声明特征不止用于Pair,还可以使用map的key和value内容来初始化两个变量

       for((index,element) in collection.withIndex()){
       if(index>0)
           result.append(separator)
       result.append(element)
       }
      
    • to函数是一个扩展函数,可以创建一对任何元素,这意味着它是泛型接收者的扩展:可以使用 1 to “one”、"one" to 1、list to list.size()等写法

    • 看看mapOf函数的声明

      fun  <K,V>  mapOf(vararg values:Pair<K,V>):Map<K,V>
      

        像listOf一样,mapOf接收可变数量的参数,只是参数为键值对

3.5、字符串和正则表达式的处理

Kotlin字符串和Java字符串完全相同

可以将在Kotlin代码中创建的字符串传递给任何Java函数,也可以把任何Kotlin标准库函数应用到从Java代码接收的字符串上,不用转换,不用创建附加的包装对象

Kotlin通过提供一系列有用的扩展函数,使标准Java字符串使用起来更加方便

它还隐藏了一些令人费解的函数,添加了一些更清晰易用的扩展

分割字符串

  • Kotlin提供了一些名为split的具有不同参数的重载的扩展函数,用来承载正则表达式的值需要一个Regex类型,而不是String,这样确保当有一个字符串传递给这些函数的时候,不会被当作正则表达式

  • 这里用一个点号或者破折号来分割字符串

    //显式地创建一个正则表达式
    println("12.345-6.A".split("\\.|-".toRegex()))
    [12, 345, 6, A]
    
    • Kotlin使用与Java中完全相同的正则表达式语法

    • 这里的模式匹配一个点(我们对它进行转义来表示我们指的是字面量,而不是通配符)或破折号

    • 在Kotlin中,使用扩展函数toRegex将字符串转为正则表达式

  • 对于一些简单的情况,就不需要使用正则表达式了,Kotlin中的split扩展函数的其他重载支持任意数量的纯文本字符串分隔符:

      println("12.345-6.A".split(".","-"))
    

正则表达式和三重引号的字符串

  • 例子:解析文件的完整路径名称到对应的组件:目录、文件名和扩展名。有两种方式实现

  • 1、使用扩展函数处理字符串。Kotlin标准库中包含了一些可以用来获取在给定分隔符第一次(或最后一次)出现之前(或之后)的子字符串的函数

    分割函数

    /**
     * 使用String的扩展函数来解析文件路径
     */
    fun parsePath(path: String) {
    //最后一个斜线之前的字符串
    val directory = path.substringBeforeLast("/")
    //最后一个斜线之后的字符串
    val fullName = path.substringAfterLast("/")
    
    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")
    
    println("Dir:$directory,name:$fileName,ext:$extension")
    }
    

    path字符串中的最后一个斜线之前的部分,是目录的路径;点号之后的部分,是文件的扩展名;而文件名称,介于两者之间

    解析字符串在Kotlin中变得更加容易,而且不需要使用正则表达式,这个功能非常强大

  • 2、使用正则表达式

    • 也可以使用Kotlin标准库中的正则表达式来实现

        /**
        	* 使用正则表达式来解析文件路径
          */
          fun parsePath2(path:String){
         
          val regex="""(.+)/(.+)\.(.+)""".toRegex()
          val matchResult=regex.matchEntire(path)
          if(matchResult!=null){
              //这里用到了解构声明
              val (directory,filename,extension)=matchResult.destructured
              println("Dir:$directory,name:$filename,ext:$extension")
          }
          }
      
      //使用正则表达式来解析文件路径
      parsePath2("/User/yole/kotlin-book/chapter.adoc")
      
      - Dir:/User/yole/kotlin-book,name:chapter,ext:adoc
      
    • 正则表达式写在一个三重引号的字符串中,在这样的字符串中,不需要对任何字符进行转义,包括反斜线,所以可以用.而不是\.来表示点,正如写一个普通的字符串字面值一样

    • 这个正则表达式将一个路径分为三个由斜线和点分割的组。这个.模式从字符串的一开始就进行匹配,所以第一组(.+)包含最后一个斜线之前的子串。这个子串包括所有前面的斜线,因为他们匹配“任何字符”的模式,同理,第二组包含最后一个点之前的子串,第三组包含剩余部分

      image-20210824202408510

多行三重引号的字符串

  • 三重引号字符串的目的,不仅在于避免转义字符,而且使它可以包含任何字符,包含换行符,另外,它提供了一种更简单的方法,从而可以简单的把包含换行符的文本嵌入到程序中

  • 例如,可以用ASCII码画点东西

     //多行三重引号字符串
     val kotlinLogo="""| //
                      .|//
                      .|/ \"""
     println(kotlinLogo.trimMargin("."))
    
  • 一个三重引号的字符串可以包含换行,而不用专门的字符,比如\n,也不必转义字符\

  • 因为多行字符串不支持转义序列,如果需要在字符串的内容中使用美元符号的字面量,则必须使用嵌入式表达式,像这样:

    val price="""$ {'$'} 99.9"""
    
  • 为了更好的格式化,使用trimMargin函数

3.6、让你的代码更整洁:局部函数和扩展

局部函数

  • 为了让代码更整洁,可以在函数中嵌套这些提取的函数,这样,既可以获得所需的结构,也无需额外的语法开销

  • 例子

    • saveUser函数用于将user的信息保存到数据库,并且确保user对象包含有效数据

    • 代码

        /**
        * 带重复代码的函数
          */
          fun saveUser(user:User){
          if(user.name.isEmpty()){
              throw IllegalArgumentException("Can't save user ${user.id}:empty Name")
          }
          if(user.address.isEmpty()){
              throw IllegalArgumentException("Can't save user ${user.id}:empty Address")
          }
          }
      
        //带重复代码的函数
        saveUser(User(1,"",""))
      

      这里的重复代码很少,你可能不想要再类中的一个面面俱到的方法中,去验证用户字段的每一种特殊情况

    • 但是,如果将验证代码放到局部函数中,可以摆脱重复,并保持清晰的代码结构,可以这样做

        /**
         * 提取局部函数来避免重复
         */
         fun saveUser2(user:User){
      
         //声明一个局部函数来验证所有字段
         fun validate(user:User,value:String,fieldName:String){
             if(value.isEmpty()){
                 throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
             }
         }
         //调用局部函数来验证特定字段
         validate(user,user.name,"Name")
         validate(user,user.address,"Address")
      
         //保存user到数据库
         }
      
    • 局部函数,可以访问所在函数中的所有参数和变量,我们可以去掉User参数

      /**
       * 在局部函数中访问外层函数的参数
        */
         fun saveUser3(user: User) {
      
         //现在不用在saveUser3函数中重复user参数了
         fun validate(value: String, fieldName: String) {
             if (value.isEmpty()) {
                 //可以直接访问外部函数的参数
                 throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
             }
         }
         validate(user.name, "Name")
         validate(user.address, "Address")
      
         //保存user到数据库
         }
      
    • 继续改进,把验证逻辑放到User类的扩展函数中

      package 三.函数的定义与调用
      
      import java.lang.IllegalArgumentException
      
      /**
      
       * 提取逻辑到扩展函数
         */
      fun User.validateBeforeSave(){
      fun validate(value:String,fieldName:String){
         if (value.isEmpty()) {
             //可以直接访问User的属性
             throw IllegalArgumentException("Can't save user $id:empty $fieldName")
         }
      }
      validate(name, "Name")
      validate(address, "Address")
      }
      
      class User(val id:Int,val name:String,val address:String) {
      }
      
      /**
       * 保存用户信息
       */
      fun saveUser4(user:User){
      //调用扩展函数
      user.validateBeforeSave()
      //保存user到数据库
      }
      
      saveUser4(User(1,"",""))
      
    • 扩展函数也可以被声明为局部函数,但是深层嵌套的局部函数往往让人费解,因此,一般不建议使用多层嵌套

3.7、小结

Kotlin没有定义自己的集合类,而是在java集合类的基础上提供了更丰富的API

Kotlin可以给函数参数定义默认值,这样大大降低了重载函数的必要性,而且命名参数让多参数函数的调用更加易读

Kotlin允许更灵活的代码结构:函数和属性都可以直接在文件中声明,而不仅仅在类中作为成员

Kotlin可以用扩展函数和属性来扩展任何类的API,包括在外部库中定义的类,而不需要修改其源代码,也没有运行时开销

中缀调用提供了处理单个参数的,类似调用运算符方法的简明语法

Kotlin为普通字符串和正则表达式都提供了大量的方便字符串处理的函数

三重引号的字符串提供了一种简洁的方式,解决了原本在Java中需要进行大量啰嗦的转义和字符串连接的问题

局部函数帮助你保持代码整洁的同时,避免重复

附件:

第3章函数的定义和调用.svg

Kotlin学习之旅开始啦

第1章Kotlin:定义与目的

第2章Kotlin基础