浅谈Kotlin语法篇之如何让函数更好地调用(三)

4,073 阅读10分钟

简述: 今天带来的是Kotlin浅谈系列第三弹,这讲来聊下函数调用和函数重载问题,看到标题就知道Kotlin在函数调用方面有优于Java的地方。先抛出以下几个坑(估计曾经的你踩过...),看我们怎么去一步步填坑,从中你会体验Kotlin这门语言魅力。

  • 1、Java在函数调用方面存在怎样的坑?
  • 2、Kotlin是怎样去解决函数调用的坑?
  • 3、Java在函数重载方面存在怎样的坑?
  • 4、Kotlin是怎样去解决函数重载带来的坑?
  • 5、Java与Kotlin互相调用时重载函数应该注意哪些问题?

一、Java函数调用存在的问题

我们尝试回忆一下在用Java开发程序的过程中,经常会去调用一些方法,有的人设计方法的参数有很多,而且参数命名有时候还不太规范(不能达到见名知意),并且设计的时候几个相同的参数类型还是挨着的。这个实际上给调用者带来了很大的困扰和麻烦,谨慎的程序猿会定位到这个函数定义的地方,大概看了下参数的调用的顺序以及每个参数的函数。特别当这个方法被打包成一个lib的时候,查看就比较麻烦。而且相同类型参数挨着很容易对应错。我们来看下这个例子(现在需求是: 给每个集合中的元素按照前缀、分割符、后缀拼接打印)

    //张三设计的函数接口顺序(调用者必须按照这个顺序传)
    static String joinToString(List<Integer> nums, String prex, String sep, String postfix) {
        StringBuilder builder = new StringBuilder(prex);
        for (int i = 0; i < nums.size(); i++) {
            builder.append(i);
            if (i < nums.size() - 1) {
                builder.append(sep);
            }
        }
        builder.append(postfix);
        return builder.toString();
    }

    //李四设计的函数接口顺序(调用者必须按照这个顺序传)
    static String joinToString(List<Integer> nums, String sep, String prex, String postfix) {
        StringBuilder builder = new StringBuilder(prex);
        for (int i = 0; i < nums.size(); i++) {
            builder.append(i);
            if (i < nums.size() - 1) {
                builder.append(sep);
            }
        }
        builder.append(postfix);
        return builder.toString();
    }

    //王二设计的函数接口顺序(调用者必须按照这个顺序传)
    static String joinToString(List<Integer> nums, String prex, String postfix, String sep) {
        StringBuilder builder = new StringBuilder(prex);
        for (int i = 0; i < nums.size(); i++) {
            builder.append(i);
            if (i < nums.size() - 1) {
                builder.append(sep);
            }
        }
        builder.append(postfix);
        return builder.toString();
    }  
    
    //假如现在叫你修改一下,拼接串前缀或分隔符,仅从外部调用是无法知道哪个参数是前缀、分隔符、后缀
    public static void main(String[] args) {
    //后面传入的三个字符串顺序很容易传错,并且外部调用者如果不看具体函数定义根本很难知道每个字符串参数的含义,特别公司中一些远古代码,可能还得打成库的代码,点进去看实现肯定蛋疼。
       //调用张三接口
        System.out.println(joinToString(Arrays.asList(new Integer[]{1, 3, 5, 7, 9}), "<", ",", ">"));
       //调用李四接口
        System.out.println(joinToString(Arrays.asList(new Integer[]{1, 3, 5, 7, 9}), ",", "<", ">"));
      //调用王二接口
        System.out.println(joinToString(Arrays.asList(new Integer[]{1, 3, 5, 7, 9}), "<", ">", ","));    
    }

然而针对以上问题,细心的程序猿早就发现,我们AndroidStudio3.0的版本给我们做了个很好的优化提示,但是在3.0之前是没有这个提示的。如图

AndroidStudio工具开发商jetBrains实际上把这个提示是直接融入了他们开发的Kotlin语言中,他们尽量让在语法的层面上少犯错误,少走弯路,更加注重于代码本身的实现;让你直接在语法的层面上就更加明确,减少疑惑性。

二、Kotlin是怎样去解决函数调用的坑

针对以上遇到的问题Kotlin可以很好的解决,在Kotlin函数中有这么一种参数叫做命名参数,它能允许在函数调用的地方指定函数名,这样就能很好使得调用地方的参数和函数定义参数一一对应起来,不会存在传递参数错乱问题。

//kotlin一个函数的接口满足以上三种顺序调用的接口,准确来说是参数列表中任意参数顺序组合的调用
fun joinToString(nums: List<Int>, prex: String, sep: String, postfix: String): String {
    val builder = StringBuilder(prex)
    for (i in nums.indices) {
        builder.append(i)
        if (i < nums.size - 1) {
            builder.append(sep)
        }
    }
    builder.append(postfix)
    return builder.toString()
}
fun main(args: Array<String>) {
    //调用kotlin函数接口,满足张三接口设计需求,且调用更加明确
    println(joinToString(nums = listOf(1, 3, 5, 7, 9), prex = "<", sep = ",", postfix = ">"))
    //调用kotlin函数接口,满足李四接口设计需求,且调用更加明确
    println(joinToString(nums = listOf(1, 3, 5, 7, 9), sep = ",", prex = "<", postfix = ">"))
    //调用kotlin函数接口,满足王二接口设计需求,且调用更加明确
    println(joinToString(nums = listOf(1, 3, 5, 7, 9), prex = "<", postfix = ">", sep = ","))
}

AndroidStudio3.0带命名参数的函数调用高亮提示更加醒目

总结: 通过以上的例子,可以得出Kotlin在函数调用方面确实是比Java明确,也避免我们去踩一些不必要的坑,没有对比就没有伤害,相比Java你是否觉得Kotlin更加适合你呢。

三、Java在函数重载方面存在怎样的坑

无论是在Java或者C++中都有函数重载一说,函数重载目的为了针对不同功能业务需求,然后暴露不同参数的接口,包括参数列表个数,参数类型,参数顺序。也就是说几乎每个不同需求都得一个函数来对应,随着以后的扩展,这个类中的相同名字函数会堆成山,而且每个函数之间又存在层级调用,函数与函数之间的参数列表差别有时候也是细微的,所以在调用方也会感觉很疑惑,代码提示发现有七八相同的方法。举个例子(Android图片加载框架我们都习惯于再次封装一次,以便调用方便)

//注意:这是我早期写出来kotlin代码(很丑陋),虽然这个看起来是kotlin代码,但是并没有脱离Java语言的思想
//束缚,也没有利用起kotlin的新特性,这个封装完全可以看做是直接从java代码翻译过来的kotlin代码。
fun ImageView.loadUrl(url: String) {//ImageView.loadUrl这个属于扩展函数,后期会介绍,暂时可以先忽略
	loadUrl(Glide.with(context), url)
}

fun ImageView.loadUrl(requestManager: RequestManager, url: String) {
	loadUrl(requestManager, url, false)
}

fun ImageView.loadUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) {
	ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).start()
}

fun ImageView.loadUrl(urls: List<String>) {
	loadUrl(Glide.with(context), urls)
}

fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>) {
	loadUrl(requestManager, urls, false)
}

fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) {
	ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).start()
}

fun ImageView.loadRoundUrl(url: String) {
	loadRoundUrl(Glide.with(context), url)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String) {
	loadRoundUrl(requestManager, url, false)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) {
	ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).round().start()
}

fun ImageView.loadRoundUrl(urls: List<String>) {
	loadRoundUrl(Glide.with(context), urls)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>) {
	loadRoundUrl(requestManager, urls, false)
}

fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) {
	ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).round().start()
}
//调用的地方
activity.home_iv_top_banner.loadUrl(bannerUrl)
activity.home_iv_top_portrait.loadUrl(portraitUrls)
activity.home_iv_top_avatar.loadRoundUrl(avatarUrl)
activity.home_iv_top_avatar.loadRoundUrl(avatarUrls)

//以上的代码,相信很多人在Java中看到有很多吧,先不说个人写的,就拿官方库中的Thread类的构造器方法就有
//七八个。说明函数重载往往在符合需求接口扩展的时候,也在渐渐埋下了坑。不说别的就拿这个类来说,即使直
//接看函数定义,你也得花时间去理清里面的调用关系,然后才能放心去使用。而且这样函数以后维护起来特别麻烦。

四、Kotlin是怎样去解决函数重载带来的坑

针对以上例子的那么重载方法,实际上交给kotlin只需要一个方法就能解决实现,并且调用的时候非常方便。实际上在Kotlin中还存在一种函数参数叫做默认值参数。它就可以解决函数重载问题,并且它在调用的地方结合我们上面所讲的命名参数一起使用会非常方便和简单。

//学完命名参数和默认值参数函数,立即重构后的样子
fun ImageView.loadUrl(requestManager: RequestManager = Glide.with(context)
					  , url: String = ""
					  , urls: List<String> = listOf(url)
					  , isRound: Boolean = false
					  , isCrossFade: Boolean = false) {
	if (isRound) {
		ImageLoader.newTask(requestManager).view(this).url(urls).round().crossFade(isCrossFade).start()
	} else {
		ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).start()
	}
}
//调用的地方
activity.home_iv_top_banner.loadUrl(url = bannerUrl)
activity.home_iv_top_portrait.loadUrl(urls = portraitUrls)
activity.home_iv_top_avatar.loadUrl(url = avatarUrl, isRound = true)
activity.home_iv_top_avatar.loadUrl(urls = avatarUrls, isRound = true)

总结: 在Kotlin中,当调用一个Kotlin定义的函数时,可以显示地标明一些参数的名称,而且可以打乱顺序参数调用顺序,因为可以通过参数名称就能唯一定位具体对应参数。通过以上代码发现kotlin的默认值函数完美解决函数重载问题,而命名函数解决了函数调用问题,并且实现任意顺序指定参数名调用函数的参数。

五、Java与Kotlin互相调用时重载函数应该注意哪些问题

5.1 使用@JvmOverloads注解解决Java调用Kotlin重载函数问题

由于在Java中是没有默认值参数的概念,当我们需要从Java中调用Kotlin中的默认值重载函数的时候,必须显示的指定所有参数值。但是这个绝对不是我们想要,否则Kotlin就失去了重载的意义了不能和Java完全互操作。所以在Kotlin给出了另一个方案就是使用@JvmOverloads注解这样就会自动生成多个重载方法供Java调用。可以通过Kotlin代码来看下以下例子

@JvmOverloads
fun <T> joinString(
        collection: Collection<T> = listOf(),
        separator: String = ",",
        prefix: String = "",
        postfix: String = ""
): String {
    return collection.joinToString(separator, prefix, postfix)
}
//调用的地方
fun main(args: Array<String>) {
    //函数使用命名参数可以提高代码可读性
    println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">"))
    println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">"))
    println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<", postfix = ">"))
    println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", prefix = "<"))
    println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", postfix = ">"))
    println(joinString(collection = listOf(1, 2, 3, 4), separator = "!"))
    println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<"))
    println(joinString(collection = listOf(1, 2, 3, 4), postfix = ">"))
    println(joinString(collection = listOf(1, 2, 3, 4)))
}

在Kotlin中参数的默认值是被编译到被调用的函数中的,而不是调用的地方,所以改变了默认值后需要重新编译这个函数。我们可以从如下反编译代码可以看出Kotlin是把默认值编译进入了函数内部的。

  // $FF: synthetic method
  // $FF: bridge method
  @JvmOverloads
  @NotNull
  public static String joinString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
     if((var4 & 1) != 0) {
        var0 = (Collection)CollectionsKt.emptyList();//默认值空集合
     }

     if((var4 & 2) != 0) {
        var1 = ",";//默认值分隔符“,”
     }

     if((var4 & 4) != 0) {
        var2 = "";//默认前缀
     }

     if((var4 & 8) != 0) {
        var3 = "";//默认后缀
     }

     return joinString(var0, var1, var2, var3);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) {
     return joinString$default(collection, separator, prefix, (String)null, 8, (Object)null);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString(@NotNull Collection collection, @NotNull String separator) {
     return joinString$default(collection, separator, (String)null, (String)null, 12, (Object)null);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString(@NotNull Collection collection) {
     return joinString$default(collection, (String)null, (String)null, (String)null, 14, (Object)null);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString() {
     return joinString$default((Collection)null, (String)null, (String)null, (String)null, 15, (Object)null);
  }

5.2 Kotlin调用Java能使用命名参数和默认值参数吗?

注意: 不能 在Kotlin中函数使用命名参数即使在Java重载了很多构造器方法或者普通方法,在Kotlin中调用Java中的方法是不能使用命名参数的,不管你是JDK中的函数或者是Android框架中的函数都是不允许使用命名参数的。

到这里我们是不是发现在Kotlin确实让我们的函数调用变得更加简单,明确呢,赶快试试吧

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~