kotlin学习(二)泛型、函数、lambda、扩展、运算符重载

news2024/9/21 10:51:27

文章目录

  • 泛型:in、out、where
    • 型变(variance)
    • 不变(Invariant)
    • 协变(Covariant)
      • Java上界通配符<? extends T>
      • Kotlin的关键词 out
      • @UnsafeVariance
    • 逆变(Contravariant)
      • Java 下界通配符<? super T>
      • kotlin的关键字 in
      • 协变与逆变对比
        • <? extends T>与 <? super T>对比
        • PECS原则
        • in与out
    • 类型投影与星投影
      • 类型投影
      • 星投影
    • 泛型擦除
    • 下划线 _ 参数类型
      • 可类型推导的参数略写
      • 作为lambda函数是参数名称
      • 作为解构声明的参数
  • 函数
    • 参数
      • 可变数量的参数 varargs
      • lambda作为参数
    • 中缀函数
      • 中缀函数的优先级
    • 局部函数
    • 泛型函数
    • 尾递归函数tailrec
    • 内联函数
      • inline
      • noinline
      • crossinline
    • 静态方法
      • 伴生类
      • 真正的静态方法:@JvmStatic注释
      • 顶层方法
    • 标准函数的使用
      • let
      • with
      • run
      • apply
  • lambda表达式
    • lambda表达式语法
    • 简化lambda
    • 从lambda中返回
    • lambda表达式的类型
  • 扩展
    • 扩展函数
    • 扩展属性 get()
  • 运算符重载 operator

泛型:in、out、where

Kotlin 中的类可以有类型参数,与 Java 类似:

class Box<T>(t: T) {
    var value = t
}

创建这样类的实例只需要提供类型参数即可:

val box: Box<Int> = Box<Int>(1)

如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,就可以省略类型参数:

val box = Box(1);

型变(variance)

Object a = new String("字符串")

String作为Object的子类,就可以直接将子类对象赋值给父类,这个操作即达到了型变

但是Java中在使用泛型时,是无法型变的,这意味着 List<String> 并不是 List<Object> 的子类型。

 List<String> strs = new ArrayList<String>();
 List<Object> objs = strs;// !!!此处的编译器错误让我们避免了之后的运行时异常
//假设我们忽略这个错误继续操作,假设objs = strs 也支持型变
 objs.add(1);// 这里我们把一个整数放入一个字符串列表
 String s = objs.get(0);// !!! ClassCastException:无法将整数转换为字符串

实际应用中,开发者需要语言对泛型类型的型变支持,所以引出了协变、逆变、不可变的实现思想(以此支持泛型的型变)

不变(Invariant)

默认情况下,Kotlin中的泛型类型是不变的。这意味着无法将一个类型的泛型实例赋值给另一个类型的泛型实例,即使它们之间有继承关系。

协变(Covariant)

如果A是B的子类型,并且Generic<A>也是Generic<B>的子类型,那么Generic<T>可以称之为一个协变类

Java上界通配符<? extends T>

Java的协变是通过上界通配符实现的,? extends 表示泛型参数必须是T类型或它的子类(继承自T,extends T)

如果Dog是Animal的子类,但 List< Dog> 并不是 List< Animal> 的子类。
下面的代码会在编译时报错:

List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs; // incompatible types

而使用上界通配符之后,List< Dog> 变成了 List<? extends Animal> 的子类型。即 animals 变成了可以放入任何 Animal 及其子类的 List。

List<? extends Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;

Kotlin的关键词 out

把上述代码改成 Kotlin 的代码:

fun main() {
    var animals: List<Animal> = ArrayList()
    val dogs = ArrayList<Dog>()
    animals = dogs
}

居然没有编译报错?其实,Kotlin 的 List 跟 Java 的 List 并不一样。

Kotlin 的 List 源码中使用了outout相当于 Java 上界通配符。

public interface List<out E> : Collection<E> {

    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    public operator fun get(index: Int): E

    public fun indexOf(element: @UnsafeVariance E): Int

    public fun lastIndexOf(element: @UnsafeVariance E): Int

    public fun listIterator(): ListIterator<E>

    public fun listIterator(index: Int): ListIterator<E>

    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

@UnsafeVariance

Kotlin 中 List 的 contains、containsAll、indexOf 和 lastIndexOf 方法中,入参均出现了范型 E。并且使用 @UnsafeVariance 修饰。

由于类型安全性的考虑,泛型类型参数默认是不可协变或逆变的。但是,在某些情况下,你可能希望强制允许协变或逆变,这就是 @UnsafeVariance 注解的作用。

正是由于 @UnsafeVariance 的修饰,打破了刚才的限制,否则会编译报错。

@UnsafeVariance 注解应该谨慎使用,因为它会绕过编译器的类型检查,可能引入类型错误。只有在你确定代码是类型安全的情况下,才应该使用 @UnsafeVariance 注解。

逆变(Contravariant)

如果 A 是 B 的子类型,并且 Generic< B> 是 Generic< A> 的子类型,那么 Generic< T> 可以称之为一个逆变类。

Java 下界通配符<? super T>

Java 的逆变通过下界通配符实现。? super表示泛型表示泛型参数必须是T类型或它的父类(T的超类,super T)

通过 ? super T告诉我们泛型参数是T类型或者T类型的父类,因此我们可以向该List中添加T类型或者T类型的子类元素

List<? super Animal> animals = new ArrayList<>();
animals.add(new Dog());

kotlin的关键字 in

in相当于Java的下界通配符<? super T>

类的参数类型使用了in之后,该参数只能出现在方法的入参。

abstract class Printer<in E> {

    abstract fun print(value: E): Unit
}

class AnimalPrinter: Printer<Animal>() {

    override fun print(animal: Animal) {
        println("this is animal")
    }
}

class DogPrinter : Printer<Dog>() {

    override fun print(dog: Dog) {
        println("this is dog")
    }
}

fun main() {

    val animalPrinter = AnimalPrinter()
    animalPrinter.print(Animal())

    val dogPrinter = DogPrinter()
    dogPrinter.print(Dog())
}

协变与逆变对比

<? extends T>与 <? super T>对比

  • 对于 List<? super Integer> l1:

    • 正确的理解: ? super Integer 限定的是泛型参数. 令 l1 的泛型参数是 T, 则 T 是 Integer 或 Integer 的父类, 因此 Integer 或 Integer 的子类的对象就可以添加到 l1 中.
    • 错误的理解: ? super Integer限定的是插入的元素的类型, 因此只要是 Integer 或 Integer 的父类的对象都可以插入 l1 中
  • 对于 List<? extends Integer> l2:

    • 正确的理解: ? extends Integer 限定的是泛型参数. 令 l2 的泛型参数是 T, 则 T 是 Integer 或 Integer 的子类, 进而我们就不能找到一个类 X, 使得 X 是泛型参数 T 的子类, 因此我们就不可以向 l2 中添加元素. 不过由于我们知道了泛型参数 T 是 Integer 或 Integer 的子类这一点, 因此我们就可以从 l2 中读取到元素(取到的元素类型是 Integer 或 Integer 的子类), 并可以存放到 Integer 中.
    • 错误的理解: ? extends Integer 限定的是插入元素的类型, 因此只要是 Integer 或 Integer 的子类的对象都可以插入 l2 中

PECS原则

PECE原则:product extend,consumer super。

  • Producer extends: 如果我们需要一个 List 提供类型为 T 的数据(即希望从 List 中读取 T 类型的数据), 那么我们需要使用 ? extends T, 例如 List<? extends Integer>. 但是我们不能向这个 List 添加数据。
  • Consumer Super: 如果我们需要一个 List 来消费 T 类型的数据(即希望将 T 类型的数据写入 List 中), 那么我们需要使用 ? super T, 例如 List<? super Integer>. 但是这个 List 不能保证从它读取的数据的类型。
  • 如果我们既希望读取, 也希望写入, 那么我们就必须明确地声明泛型参数的类型, 例如 List< Integer>.

in与out

按照上边提到的PECS原则,生产T类型的元素(从list取出),使用 ? extends 取出,对应 out

消费T类型的元素,(把元素放入到list中),使用? super,装入,对应in

PECS:Producer Extend Consumer Super

消费者 in, 生产者 out

类型投影与星投影

在 Kotlin 中,类型投影和星投影都是用于处理泛型类型中的通配符或未知类型参数的概念。

类型投影

上边介绍的使用关键字outin限制泛型类型参数,就属于类型投影

  • out 投影:使用 out 关键字可以使泛型类型参数在所在类型中变为协变。这意味着可以安全地将泛型类型参数的子类型赋值给泛型类型的父类型。使用 out 关键字的泛型类型参数只能用于输出位置(即,作为返回类型),不能用于输入位置(即,作为函数参数)。
  • in 投影:使用 in 关键字可以使泛型类型参数在所在类型中变为逆变。这意味着可以安全地将泛型类型参数的父类型赋值给泛型类型的子类型。使用 in 关键字的泛型类型参数只能用于输入位置(即,作为函数参数),不能用于输出位置(即,作为返回类型)。

星投影

星投影是一种特殊的类型投影,用于在使用泛型类型时,不关心具体的类型参数。星投影使用星号 (*) 来表示未知类型参数。

星投影有三种形式:

  • *:表示一个未知类型参数,可以被任意类型替代。
  • out *:表示一个未知类型参数,可以被任意类型替代,并且可以在输出位置(作为返回类型)使用。
  • in *:表示一个未知类型参数,可以被任意类型替代,并且可以在输入位置(作为函数参数)使用。

星投影通常在以下情况下使用:

当你对泛型类型中的具体类型参数不感兴趣时。
当你需要将泛型类型作为函数参数,但不需要对类型参数进行操作时。

class Processor<T> {
    fun process(value: T) {
        // 处理 value
    }
}

fun processAny(processor: Processor<*>){
    val value: Any = getValue()
    processor.process(value) // 可以传

泛型擦除

与Java一样,Kotlin 为泛型声明用法执行的类型安全检测在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如,Foo 与 Foo<Baz?> 的实例都会被擦除为 Foo<*>。

下划线 _ 参数类型

下划线运算符 _ 可以用于类型参数。

可类型推导的参数略写

当显式指定其他类型时,使用它可以自动推断参数的类型。

abstract class SomeClass<T> {
    abstract fun execute() : T
}

class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}

object Runner {
    inline fun <reified S: SomeClass<T>, T> run() : T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}

fun main() {
    // T被推断为字符串,因为SomeImplementation派生自SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // T被推断为Int,因为SomeImplementation派生自SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

作为lambda函数是参数名称

fun main(args: Array<String>) {

    val aa = mapOf(1 to "a",2 to "B")

    aa.forEach { key, value -> println("value:$value") 
}

在上述示例中,只是用到了value值,key并没有用到。这样,我们就想不在声明key,那么就需要使用下划线字符(_)作为key替代,即:

fun main(args: Array<String>) {

    val aa = mapOf(1 to "a",2 to "B")

    aa.forEach { _, value -> println("value:$value") 
}

作为解构声明的参数

解构声明就是将一个对象解构(destructure)为多个变量,也就是意味着一个解构声明会一次性创建多个变量.简单的来说,一个解构声明有两个动作:

  • 声明多个变量
  • 将对象的属性赋值给相应的变量

例如,有个数据类Person,其有name和age两个属性

data class Person(var name: String, var age: Int) {
}

当我们对Person的实例使用解构声明时,可以这样做:

var person: Person = Person("Jone", 20)
var (name, age) = person
println("name: $name, age: $age")// 打印:name: Jone, age: 20

其中,var (name, age) = person就是解构声明,其实际意义是创建了两个变量name和age,然后将person的属性值”Jone”和20分别赋值给name和age。

通过解构声明创建多个变量时,我们这么做:

fun main() {
    val book = Book(1, "英语")
    val (id, name) = book
}
data class Book(var id: Int, var name: String)

上面的示例中,解构book声明了 id,name两个变量。如果只需要id这一个变量时,可以这么做:

val book = Book(1, "英语")
val (id, _) = book

函数

Kotlin 函数使用 fun 关键字声明:

fun double(x: Int): Int {
    return 2 * x
}

参数

函数参数使用 Pascal 表示法定义——name: type。参数用逗号隔开, 每个参数必须有显式类型:

fun powerOf(number: Int, exponent: Int): Int { /*……*/ }

声明函数参数时可以使用尾随逗号:

fun powerOf(
    number: Int,
    exponent: Int, // 尾随逗号
) {

}

可变数量的参数 varargs

函数的参数(通常是最后一个),可以用vararg修饰符标记:

fun <T> asList(var)

lambda作为参数

如果在默认参数之后的最后一个参数是 lambda 表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入:

fun foo(
    bar: Int = 0,
    baz: Int = 1,
    qux: () -> Unit,
) { /*……*/ }

foo(1) { println("hello") }     // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") }        // 使用两个默认值 bar = 0 与 baz = 1

中缀函数

标有infix关键字的函数可以使用中缀表示法(忽略该调用的点和圆括号)调用。中缀函数必须满足以下要求:

  • 它们必须是成员函数或拓展函数
  • 它们必须只有一个参数
  • 参数不得接受可变数量的参数且不能有默认值
infix fun Int.shl(x : Int) : Int {}

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

中缀函数的优先级

中缀表示法的函数调用类似于算术运算符,但是中缀函数的调用的优先级低于算术操作符、类型转换以及rangeTo操作符,以下表达式是等价的:

  • 1 shl 2 + 3 等价于 1 shl (2 + 3)
  • xs union ys as Set < * > 等价于 xs union (ys as Set< * >)
  • 0 until n * 2 等价于 0 until (n × 2)

另一方面,中缀函数调用的优点级高于布尔操作符&&||isin检测以及其他一些操作符。

  • a && b xor c 等价于 a && (b xor c)
  • a xor b in c 等价于(a xor b)in c

请注意,中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用this,这是确保非模糊解析所必需的。

class MyStringCollection {
	infix fun add(s : String) { }

	fun build() {
		this add "abc"
		add("abc")
		// add "abc"  错误,必须指定接收者
	}
 }

局部函数

kotlin支持局部函数,即一个函数在另一个函数内部:

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

局部函数可以访问外部函数(闭包)的局部变量。在上例中,visited 可以是局部变量:

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }
    dfs(graph.vertices[0])
}

泛型函数

函数可以有泛型参数,通过在函数名前使用尖括号指定:

fun <T> singletonList(item: T): List<T> {}

尾递归函数tailrec

Kotlin 支持一种称为尾递归的函数式编程风格:

val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double =
    if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

上面这个函数的特点是,函数将其自身调用作为它执行的最后的一个操作。代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos, 直到结果不再改变,对于这里指定的 eps 精度会产生 0.7390851332151611 的结果。最终代码相当于这种更传统风格的代码:

val eps = 1E-10 // "good enough", could be 10^-15

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps) return x
        x = Math.cos(x)
    }
}

内联函数

inline

内联函数起初是在 C++ 里面的。
简单来讲,当一个函数被inline标注后,在调用它的地方,会把这个函数方法体中的所有代码移动到调用的地方,而不是通过方法间压栈进栈的方式。

inline在一般的方法是标注是不会起到很大作用的,inline能带来性能提升,往往是在lambda函数上。这是因为在kotlin中,出现了大量的高级函数(高阶函数是将函数作为参数或者返回值的函数),使得越来越多的地方出现函数参数不断传递的现象,每一个函数参数都会被编译为一个对象,使得调用时会增加运行时间开销。

noinline

假设一个函数有多个函数型参数,若加上inline会使全部的参数参与内联,若某些不想参与内联,则需要在参数前加上noinline关键字。
使用如下:

inline fun test(lambda1:() -> Unit, noinline lambda2: () -> Unit) {
	lambda1()
	lambda2()
}

内联的函数将被代码替换,并没有真正的参数含义,只能传递给另一个内联函数,而非内联的函数是一个真正的参数,使用noline关掉局部优化,可以摆脱不能把函数类型的参数当做对象使用的限制。

crossinline

内联函数可以使用return,内联时的return会跑到调用者的函数中,return结束的是调用者的函数,非内联的return结束的是内部类的方法。即使用inline优化会影响调用方的return流程控制。

考虑一种情况,我们即想lambda被inline优化,但是又不想让lambda对调用者的控制流程产生影响,可以使用crossinline关键字,保留了inline的特性,但是如果想在传入的lambda里边return,就会报错。

crossinline就像一个契约,保证传入的lambda一定不使用return

静态方法

静态方法在Java中使用static关键字声明,在调用时无需创建实例,通过类名.方法名的方式调用。

在kotlin中定义Object类,其内部的方法调用类似static方法的调用:

object Utils {
    fun test() {

    }
}

调用如下:

Utils.test()

但其实这并非是真正的静态方法,而是单例对象的方法调用。
其对象的java文件如下:

public final class Utils {
   @NotNull
   public static final Utils INSTANCE;

   public final void test() {
   }

   private Utils() {
   }

   static {
      Utils var0 = new Utils();
      INSTANCE = var0;
   }
}

伴生类

如果我们想使一个普通类中的某些方法通过类名.方法名的方式调用,而别的方法还是正常的调用方式,可以借助伴生类,即关键字companion object

class Util2{
    fun function1(){
        println("这是一个普通的方法")
    }
    
    companion object{
        fun function2(){
            println("这是伴生类中的方法")
        }
    }
}

这里的function1必须通过对象名.方法名的方式调用,而function2被定义在companion object中,可以通过类名.方法名的方式调用,它的本质是在类中创建了一个Compain的静态内部类(伴生类),调用function2(),就是调用此对象的test2()。

上述方法只是实现了类似于静态方法的特性,而非真正的静态方法,因为在java文件中以静态方法的形式调用时,发现这些方法都是不存在的。

真正的静态方法:@JvmStatic注释

如果给单例类(object)和伴生类中(companion object)的方法加上@JvmStatic注解,就会成为真正的静态方法,在kotlin和java文件中都可以调用。
注意:@JvmStatic只能加在单例类和伴生类中的对象上。如果加在一个普通方法上,就会报错。

class Util3 {
    companion object{
        
        @JvmStatic
        fun function(){
            println("这是一个真正的静态方法")
        }
    }
}

顶层方法

顶层方法是指不在类中定义的方法,编译器会自动把所有的顶层方法全部编译为静态方法,如果在kotlin中调用顶层方法,直接使用函数名即可。

标准函数的使用

let

let函数所作的事情就是把一个调用它的对象变成lambda表达式的参数,结合安全调用语法,能有效地把调用let函数的可空对象转变为非空类型,换言之,仅在对象非空时,执行lambda。

s?.let { 
    print(s.length)
}

with

with接收两个参数,一个任意类型的对象,一个Lambda表达式,第一个参数会传给Lambda使用,其Lambda内部执行的方法都是传入的对象所执行的,Lambda的最后一行代码会当成返回值返回,调用with则Lambda会立即执行。

val result = with(obj) {
    //this则代表obj
    //test() 等价于 this.test() 等价于 obj.test()
    //返回值,result最终为value
    "value"
}

比如存在一个水果列表,现在想吃完所有水果,并打印结果,不借助with如下:

val list = listOf("apple, banana, orange")
val builder = StringBuffer()
builder.append("Start eating fruits.\n")
for (fruit in list) {
    builder.append(fruit).append("\n")

}
builder.append("Ate all fruits")
val result = builder.toString()
println(result)

with实现如下:

val list = listOf("apple, banana, orange")
val result = with(StringBuilder()) {
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits")
    toString()
}
println(result)

可以看出我们可以省略对象去调用方法,使得代码更加简洁。

run

run与with的使用场景类似,不同的是run在对象上调用,且只需要一个lambda参数,其他地方一样,
上述吃水果用run实现如下:

val list = listOf("apple, banana, orange")
val result = StringBuilder().run {  
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits")
    toString()
}
println(result)

apply

apply与run相似,也需要在对象上调用,不同的是他的返回值是调用对象本身。
上述吃水果用apply实现如下:

val list = listOf("apple, banana, orange")
val result = StringBuilder().apply {
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits")
}
println(result.toString())

lambda表达式

lambda表达式语法

lambda表达式的完整语法形式如下:
val sum : (Int, Int) -> Int = {x: Int, y: Int -> x + y}

  • lambda表达式总在花括号中
  • 完整语法形式的参数声明放在花括号内,并有可选的类型标注
  • 如果推断出lambda的返回类型不是Unit,那么该lambda主体中的最后一个表达式会视为返回值。

简化lambda

下面这个示例就是简化lambda的写法:

fun main(args: Array<String>) {
	args.forEach {
		if (it == "q") return
		println(it)
	}
	println("The End")
}

首先forEash本质上是一个内联函数:

public inline fun<T> forEach(action: (T) -> Unit): Unit {
	for (element in this) action(elment)
}

inline的作用是代码替换,因此上边的代码相当于:

args.forEach({element -> println(element)})

1、kotlin 允许我们把函数的最后一个lambda表达式参数,移动到小括号外

args.forEach(){
	element -> println(element)
}

2、如果函数只有一个lambda,小括号可以省略掉:

args.forEach{
	element -> println(element)
}

3、只有一个参数可以默认为it:

args.forEach{
	println(it)
}

4、入参,返回值与形参一致的函数可以用函数的引用作为实参传入:

args.forEach(::println)

总结:

  1. 最后一个lambda可以移出去
  2. 只有一个lambda,小括号可以省略
  3. lambda只有一个参数可以默认为it
  4. 入参,返回值与形参一致的函数可以用函数的引用作为实参传入

这下再回头看这种写法就明白了:

fun main(args: Array<String>) {
	args.forEach {
		if (it == "q") return
		println(it)
	}
	println("The End")
}

从lambda中返回

下面的代码,我们预期的效果是打印非q元素的字符,最后输出“The End”。

fun main() {
    var args: String = "opqrst"
    args.forEach {
        if (it == 'q') return
        println(it)
    }
    println("The End")
}

然而实际上,由于forEach本质是inline,lambda中的return实际的调用者在main函数中,因此输出为:

o
p

解决方式是定义一个标签@forEachBlock

fun main() {
    var args: String = "opqrst"
    args.forEach forEachBlock@{
        if (it == 'q') return@forEachBlock
        println(it)
    }
    println("The End")
}

在这里插入图片描述

lambda表达式的类型

public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

注意到,action 这个形参的类型是 (T) -> Unit,这个是 Lambda 表达式的类型,或者说函数的类型,它
表示这个函数接受一个 T 类型的参数,返回一个 Unit 类型的结果。

() -> Int //无参,返回 Int
(Int, Int) -> String //两个整型参数,返回字符串类型
(()->Unit, Int) -> Unit //传入了一个 Lambda 表达式和一个整型,返回 Unit

扩展

扩展函数

扩展函数:对类的方法进行补充,动态给类添加方法。

Java没法对系统类进行拓展,而kotlin可以对其进行拓展,比如实现一个统计string中的字母个数的函数:
若不借助扩展函数,定义StringUtil并实现lettersCount:

object StringUtil {
    fun lettersCount(string: String): Int {
        var count = 0;
        for (char in string) {
            if (char.isLetter()) count++
        }
        return count
    }
}

调用如下:

StringUtil.lettersCount("ab2")

若借助扩展函数,可直接将lettersCount()方法添加到String类中,不必再创建StringUtil:
创建String.kt,其职责就是对String进行拓展,创建新的文件可以使得拓展函数拥有全局访问域,不定义新文件也是可以的,郭霖大佬的建议是定义新文件。

fun String.lettersCount(): Int {
	var count = 0
	for (char in this) {
		if (char.isLetter()) count++
	}
	return count
}

Kotlin访问可以直接使用:

var count = "111asd".lettersCount()

Java中调用需要如下方式:

StringKt.lettersCount("aaa");

扩展属性 get()

kotlin还可以扩展属性,在String.kt加入以下代码,相当于给String添加了一个值为10intget()是固定语法

val String.value : Int get() = 10

Kotlin中访问如下:

var value = "".value

Java中访问如下:

int value = StringKt.getValue("aaa");

运算符重载 operator

Kotlin的每个运算符都有其对应的方法:
在这里插入图片描述
Kotlin的编译器可以把+转换成相对应的plus方法调用。

构建一个Money类:

class Money(val value: Int) {
	operator fun plus(money: Money): Money {
		val sum = value + money.value
		return Money(sum)
	}
}

上边的Money类,在构造时声明了Int类型的value属性,使用operator重载了plus方法.

fun main() {
    val money = Money(10) + Money(30)
    println(money.value)
    //输出 40
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/659831.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Portraiture4.1智能磨皮滤镜插件下载安装使用教程

ps磨皮插件portraiture是一款用于修饰人像照片的插件&#xff0c;可以在Photoshop中使用。它可以通过智能算法来自动识别照片中的肤色区域&#xff0c;然后对其进行磨皮处理&#xff0c;使得肌肤更加光滑细腻。不需要像曲线磨皮、中性灰磨皮那样需要复杂的操作&#xff0c;轻轻…

JavaScript之函数 (七):认识JavaScript函数、函数的声明和调用、函数的递归调用、局部和全局变量、函数表达式的写法、立即执行函数使用

1. 认识JavaScript函数 1.1 程序中的foo、bar、baz 在国外的一个问答网站stackover flow中&#xff0c;常常会使用这几个次进行变量&#xff0c;函数&#xff0c;对象等等声明&#xff0c;地位如同张三&#xff0c;李四&#xff0c;王五。foo、bar这些名词最早从什么时候、地…

【MySQL入门】-- 认识MySQL存储引擎

目录 1.MySQL存储引擎有什么用&#xff1f; 2.MySQL的存储引擎有哪些&#xff1f;分别有什么特点&#xff1f; 3.存储引擎的优缺点 4.关于存储引擎的操作 5. 存储引擎的选择&#xff1f; 6.InnoDB和MyISAM区别&#xff1f; 7.官方文档 1.MySQL存储引擎有什么用&#xff…

2022 年第十二届 MathorCup 高校数学建模挑战赛D题思路

目录 一、前言 二、问题背景 三、问题 四、解题思路 &#xff08;1&#xff09;针对问题1&#xff1a; &#xff08;2&#xff09;针对问题2&#xff1a; &#xff08;3&#xff09;针对问题3&#xff1a; 五、附上几个典型代码 &#xff08;1&#xff09;K-means算法…

文献阅读:Foundation Transformers

文献阅读&#xff1a;Foundation Transformers 1. 文章简介2. 模型结构 1. Sub-LN2. Initialization 3. 实验效果 1. NLP任务 1. 语言模型上效果2. MLM模型上效果3. 翻译模型上效果 2. Vision任务上效果3. Speech任务上效果4. 图文任务上效果 4. 结论 & 思考 文献链接&…

卡尔曼滤波器使用原理以及代码编写

注&#xff1a;要视频学习可以去B站搜索“DR_CAN”讲解的卡尔曼滤波器&#xff0c;深有体会&#xff01; 一、为啥需要卡尔曼滤波器 卡尔曼滤波器在生活中应用广泛&#xff0c;因为在我们生活中存在着不确定性&#xff0c;当我去描述一个系统&#xff0c;这个不确定性就包涵一…

源码编译LAMP与论坛安装

目录 Apache网站服务&#xff08;著名的开源Web服务软件&#xff09; Apache的主要特点 软件版本 如何创建论坛 安装相关服务Apache 安装MySQL数据库 安装PHP框架 然后进行论坛安装 第一步 先进入到MySQL内 第二步 授权bbs数据库 第三步 刷新数据库 第四步 解压指定…

【Windows】虚拟串口工具VSPD7.2安装

【Windows】虚拟串口工具VSPD7.2安装 1、背景2、VSPD7.2安装3、创建虚拟串口 1、背景 ​Virtual Serial Ports Driver​是由著名的软件公司Eltima制作的一款非常好用的​虚拟串口工具​&#xff0c;简称&#xff1a;VSPD。 VSPD其功能如同 Windows机器上COM 串行端口的仿真器…

Go-unsafe详解

Go语言unsafe包 Go语言的unsafe包提供了一些底层操作的函数&#xff0c;这些函数可以绕过Go语言的类型系统&#xff0c;直接操作内存。虽然这些函数很强大&#xff0c;但是使用不当可能会导致程序崩溃或者产生不可预料的行为。因此&#xff0c;使用unsafe包时必须小心谨慎。 …

小白必看!渗透测试的8个步骤

渗透测试与入侵的区别 渗透测试&#xff1a;以安全为基本原则&#xff0c;通过攻击者以及防御者的角度去分析目标所存在的安全隐患以及脆弱性&#xff0c;以保护系统安全为最终目标。 入侵&#xff1a;通过各种方法&#xff0c;甚至破坏性的操作&#xff0c;来获取系统权限以…

C++ 教程(15)——数组(包含实例)

C 支持数组数据结构&#xff0c;它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据&#xff0c;但它往往被认为是一系列相同类型的变量。 数组的声明并不是声明一个个单独的变量&#xff0c;比如 number0、number1、...、number99&#xff0c;而是声…

[架构之路-215]- 系统分析-领域建模基本概念

目录 1. 什么是领域或问题域 2. 什么面向对象的“类” 》 设计类 3. 什么是概念类 4. 什么是领域建模 5. 领域建模与DDD&#xff08;领域驱动架构设计&#xff09;的关系 6. 领域建模的UML方法 7. 领域建模的案例 其他参考&#xff1a; 1. 什么是领域或问题域 领域&a…

Spring AOP之MethodInterceptor原理

文章目录 引言Spring AOP组成先看一下Advice 示例提问 原理 引言 之前我们讨论过了HandlerInterceptor&#xff0c;现在我们来看一下MethodInterceptor。 MethodInterceptor是Spring AOP中的一个重要接口,用来拦截方法调用&#xff0c;它只有一个invoke方法。 Spring AOP组成…

Laya3.0游戏框架搭建流程(随时更新)

近两年AI绘图技术有了长足发展&#xff0c;准备把以前玩过的游戏类型重制下&#xff0c;也算是圆了一个情怀梦。 鉴于unity商用水印和启动时间的原因&#xff0c;我决定使用Laya来开发。目前laya已经更新到了3.0以上版本&#xff0c;就用目前比较新的版本。 之后关于开发中遇到…

HashMap学习:1.7 迁移死循环分析(通俗易懂)

前言 JDK1.7由于采用的头插法&#xff0c;所以多线程情况下可能会产生死循环问题。 正文 头插法 就是每次从旧容器中的hash桶中取出数据后&#xff0c;放到新容器的头节点问题&#xff0c;如果此时头结点位置为空&#xff0c;直接放置即可&#xff0c;如果不为空将头节点的数…

C语言strncpy的使用缺陷和实现,strncat的使用缺陷和实现,strncmp的使用和实现。

1.strncpy 函数原型&#xff1a; char *strncpy( char *strDest, const char *strSource, size_t count );char *strDest 目标字符串首元素地址const char *strSource 源字符串(需要拷贝过去的字符串)size_t count 拷贝字符的个数char *strncpy 拷贝结束后&#xff0c;返回目…

Micormeter实战

Micrometer 为基于 JVM 的应用程序的性能监测数据收集提供了一个通用的 API&#xff0c;支持多种度量指标类型&#xff0c;这些指标可以用于观察、警报以及对应用程序当前状态做出响应。 前言 可接入监控系统 监控系统的三个重要特征&#xff1a; 维度&#xff08;Dimensio…

[保姆教程] Windows平台OpenCV以及它的Golang实现gocv安装与测试(亲测通过)

一、MinGW & CMake 预备步骤 首先打开cmd: c: md mingw-w64 md cmake下载安装MinGW-W64 访问&#xff1a; https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/7.3.0/ 下载&#xff1a; MinGW-W64 GCC-8…

一文详解Softmax的性质与Self-Attention初步解析

概述 最近研究超平面排列(Hyperplane Arrangement)问题的时候&#xff0c;发现ReLU有其缺陷&#xff0c;即举例来说&#xff0c;ReLU 无法使用单一的超平面将分离的所有数据&#xff0c;完整的输出&#xff0c;即只会输出半个空间映射的数据&#xff0c;而另一半空间的数据被置…

面试---简历

项目 1.1、商品管理 新增商品 同时插入商品对应的使用时间数据&#xff0c;需要操作两张表&#xff1a;product&#xff0c;product_usetime。在productService接口中定义save方法&#xff0c;该方法接受一张Dto对象&#xff0c;dto对象继承自product类&#xff0c;并将prod…