从 Java 过渡到 Kotlin(一):“面向过程”

〇、引言

作为一个接触 Java 几年的人,虽然我很喜欢 Java 清晰的代码结构,但是在写工程的时候还是会有力不从心之感。Java 的语法简单,但是代码会变得冗长,同时作为一个强迫症,在编写一块新功能的时候,总是无法第一时间去编写实现实际功能的代码,而去考虑如何编写抽象类或者接口,来适应今后可能出现的对该功能进行扩充的需求,但是进行这种工作的时候脑子里对于实现具体功能的思路一直萦绕着,感觉十分折磨。

之后我就接触到了 Kotlin,它是开发了可以称作是 Java 最好用的 IDE——IntelliJ IDEA 的 JetBrains 公司开发的一门与 Java 十分贴近的语言,Kotlin 代码能够编译成 Java 字节码并且在 JVM 上运行,也能够调用 Java 代码,且其语法进行了足够良好的设计,用作 Java 的上位替代大约是十分合适的,而且 Google 宣布将 Kotlin 作为 Android 开发的推荐语言。因此我便开始学习 Kotlin,来提高我的开发效率。

我在阅读一些 Kotlin 书籍时,发现这些书的信息密度非常低,假如你是一个对编程一窍不通的人,这些书对你或许很有帮助。但是作为一个很熟悉 Java 的人,阅读这些书时就会感觉好不容易读完一章,几十页,几千个字,所阐述的东西就只有那么一点,但是我却花费了阅读这么多字的精力(可见速读能力是多么重要!),因此,为了我学习 Kotlin 进行记录之用,也为了帮助已经比较熟悉 Java 的人能够快速上手 Kotlin,我开启了这个系列。

我学习 Kotlin 所使用的书是《Kotlin 编程权威指南》

事实上,我觉得如果你即使不熟悉 Java,本系列的内容你大概也能看懂相当一部分。

一、Hello world

Kotlin 并不像 Java 一样,一切元素都是类的成员。Kotlin中的类不再像 Java 中那样具有至高无上的地位,我们的 main 函数就直接写在任何地方,然后 IntelliJ IDEA 就可以以这个 main 函数为入口来执行。

fun main() {
    println("Hello world")
}

可以看到,在 Kotlin 中调用 println 不需要使用冗长的 System.out 前缀,且语句无需以 ; 号结尾,但同时又保留了大括号。我认为 Kotlin 是统合并优化了 C++、Java、Python等语言的优点的一门语言

二、基本数据类型

0x01 基本类型变量的声明

省流:

  • var 声明变量,val 声明常量
  • 可以为变量指定类型,也可以通过赋值让编译器推导出它的类型
  • Kotlin 的基本类型对应 Java 中基本类型的封装,因此 Kotlin 取消了装箱和拆箱的概念

Java 中规定的基本数据类型就是 Kotlin 中规定的基本数据类型,只是在 Kotlin 中使用类型名时相比 Java 会首字母大写,例如 Java 中的 long 类型对应 Kotlin 中的 Long 类型。

在 Kotlin 中声明变量的基本语句如下:

var [变量名]:[类型] = 字面量
val [常量名]:[类型] = 字面量

其中 var 表示声明变量,val表示声明常量,它们对应的 Java 语句为:

[类型] [变量名] = 字面量;
final [类型] [常量名] = 字面量;

此外,Kotlin 有类似于 Python 的对象类型推导功能,也就是说我们在一些时候可以不去写类型名,例如:

var x = 100

就已经是创建了一个 Int 类型的变量,这里我们没有明着指出 x 的类型,但是 Kotlin 可以根据后面的100的类型推导出这个 x 应当是 Int 类型的。

为了方便叙述,以后把 valvar 均称作变量。

事实上,Kotlin 这些所谓的基本数据类型更像是 Java 中基本数据类型的封装(也就是类似于 int 封装成 Integer),Kotlin 就全面采用这种封装的类型,也就取消了拆箱和装箱的概念。

0x02 Kotlin 字符串模板

fun main() {
    val a = 20
    val b = 40
    println("" + a + "+" + b + "=" + (a + b))
    println("$a+$b=${a + b}")
}

输出结果:

20+40=60
20+40=60

在字符串中使用美元符号,可以让字符串中的一些字符作为代码执行。

当然,如果想要打印美元符号,可以在美元符号前添加 \ 符号,即可取消其作为模板标识符的作用。

0x03 Kotlin 数组与区间

1. 区间

区间有其对应的类名:CharRange, IntRange, LongRange

区间有两种字面量:a..b 表示区间 [a, b]a until b 表示区间 [a, b),同时可以使用关键字 in 来判断某个数在数值上是否属于某个区间。

fun main() {
    val C1 : CharRange = 'a' until 'z'
    val C2 : CharRange = 'a' .. 'z'
    val N  : IntRange  = 1 .. 50
    println("${'z' in C1}, ${'z' in C2}, ${49 in N}, ${72 in 68 .. 100}")
}

输出结果:

false, true, true, true
2. 数组

Java 中有基本数据类型的数组和对象数组,Kotlin 中对基本数据类型的数组都专门定制了类,类名就是 [类型名]+Array,例如 Int 的数组就是 IntArray,函数arrayOf可以创建一个数组对象,而 intArrayOf可以创建一个 IntArray 对象。此外,也可以直接使用 IntArray 的“构造函数”来创建对象。

var arr1 = intArrayOf(1, 2, 3, 4, 5)
var arr2 = IntArray(5)

分别与 Java 中

int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = new int[5];

功能相同。

而 Kotlin 中 arr1 对象也封装了一些操作,此处不再赘述。当然,Kotlin 也可使用形如 arr1[0] 写法访问数组元素。

0x04 Kotlin 运算符

Kotlin 中有两种比较相等符号,=====

  • 其中 == 相当于 Java 中 Object 类内的 equals函数,按照对象的值进行比较;
  • ===类似于 Java 中的 == 符号,按照变量的地址值进行比较;
fun main() {
    val t1 = "test"
    var t2 = "te"
    t2 += "st"
    println("${t1 == t2}, ${t1 === t2}")
}

输出结果:

true, false

二、函数

0x00 基本

Kotlin 函数定义的格式如下:

fun [函数名]([参数1][类型], [参数2][类型], ...) : [返回值] {
    
}

如果没有返回值,就无需填写返回值。针对函数返回值, Kotlin 并未提供类似于 varval 语句的类型推导功能。

fun plus(a:Int, b:Int) : Int {
    return a + b
}
fun display(x:Int) {
    println("$x")
}

就相当于 Java 中的:

public static int plus(int a, int b) {
    return a + b;
}
public static void display(int x) {
    System.out.println(x);
}

此外,Kotlin 的函数可以定义在函数内部

例如:

fun main() {
    fun plus(a:Int, b:Int) : Int {
        return a + b
    }
    fun display(x:Int) {
        println("$x")
    }

    println("${plus(20, 50)}")
    display(90)
}

输出结果:

70
90
特性:默认参数

Kotlin 的函数与 Python 一样,能够为参数设定默认值。例如:

fun plus(a:Int, b:Int, c:Int = 5) {
    println("${a + b + c}")
}
fun main() {
    plus(1, 2)
}

运行结果:

8

此外,Kotlin 还像 Python 那样能够为指定的参数赋值,例如:

fun plus(a:Int, b:Int = 5, c:Int) {
    println("${a + b + c}")
}
fun main() {
    plus(1, c = 5)
}

运行结果:

11
语法糖:单表达式函数

如果一个函数只含有一个语句,那么可以写成这种形式:

fun single(x:Int) = println("$x is $x")
fun main() {
    single(2)
}

运行结果:

2 is 2

这种语法糖更多应用在有返回值的情况:

fun single(x:Int) = x * x * x
fun main() {
    println(single(5))
}

运行结果:

125

0x01 Lambda 表达式

1. 基本写法
{[参数列表]->[代码]}

代码举例:

fun main() {
    val a = {x:Int, y:Int ->
        println("$x and $y")
        x + y
    }(1, 5)
    println(a)
}

运行结果:

1 and 5
6

这段代码等价于:

fun plus(x:Int, y:Int): Int{
    println("$x and $y")
    return x + y
}
fun main() {
    val a = plus(1, 5)
    println(a)
}

可见,Lambda 表达式可以省略 return 符号,而且其能够推断返回值类型。(事实上,如果在这里加上 return 符号,则这个 Lambda 表达式就会被作为一个 Unit 类型(相当于 Java 中的 void 类型)的函数处理),在我们这个示例程序中就会报错。

2. Lambda 表达式变量

能够用一个变量来存储 Lambda 表达式。当然,既然是用变量存储,那么变量也具有某种类型。存储 Lambda 表达式的变量的类型为:

([参数列表]) -> [返回值]

如果没有返回值,则返回值为 Unit

代码举例:

fun main() {
    val a : (Int, Int) -> String = {x: Int, y: Int ->
        println("You called me?")
        "$x + $y = ${x + y}"
    }
    println(a(1, 5))
}

运行结果:

You called me?
1 + 5 = 6

而且,由于 Lambda 表达式能够使用变量来存储,这也意味着一个函数的参数可以是 Lambda 表达式类型

代码举例

fun printTwice(lmb : (x:Int, y:Int) -> String) {
    val str = lmb(2, 5)
    println(str)
    println(str)
}
fun main() {
    val a : (x:Int, y:Int) -> String = {x: Int, y: Int ->
        println("You called me?")
        "$x + $y = ${x + y}"
    }
    printTwice(a)
}

运行结果:

You called me?
2 + 5 = 7
2 + 5 = 7

语法糖a:单参数的 Lambda 表达式

可以注意到,我们之前创建一个 Lambda 表达式类型的变量的时候,虽然在类型上声明了参数列表,但是在具体编写函数体的时候还会写一次参数列表。而对于单参数的 Lambda 表达式,则无需在函数体那里再写一次参数列表,可以使用 it 关键字来访问这个参数。

代码举例:

fun main() {
    val a : (Int)->String = {
        "$it * $it = ${it * it}"
    }
    println(a(5))
}

运行结果:

5 * 5 = 25

语法糖b:最后一个参数是 Lambda 表达式类型参数的函数
fun func(t:()->Unit, x:()->Unit) {
    t()
    x()
}
fun main() {
    func({println("ttttttttttttttt")}) {
        println("xxxxxxxxxxxxxxx")
    }
}

输出结果:

ttttttttttttttt
xxxxxxxxxxxxxxx

可见,如果一个函数的最后一个参数是 Lambda 表达式,可以不把这个 Lambda 表达式写在括号内部,而可以直接使用大括号编写这个 Lambda 函数的内容,这个大括号里面的内容就是传入函数的 Lambda 表达式变量的内容。


语法糖c:仿佛对象成员方法的 Lambda 表达式

代码举例:

fun main() {
    val arrPrint:(IntArray)->Unit = {
        for(x in it) {
            print("$x, ")
        }
        println()
    }
    val arrInit:IntArray.()->Unit = {
        for(x in withIndex()) {
            set(x.index, x.index * 10 + 1)
        }
    }
    val arr = IntArray(10)
    arr.arrInit()
    arrPrint(arr)
}

输出结果:

1, 11, 21, 31, 41, 51, 61, 71, 81, 91, 

注意这里的 arrInit ,它在声明 Lambda 表达式的类型时,在前面加上了一个IntArray.,这样一来在 Lambda 表达式内部编写代码时,就可以使用 this 或者直接来调用 IntArray 类中的成员。参观其调用方式,它仿佛真的就是为 IntArray 类添加了一个 arrInit 方法。

注:这里涉及到的 for 循环会在后文进行讲解,此处其作用就是输出一个整数数组的内容。

0x03 Kotlin 通用函数

也就是说,apply, run, let, with, also 五个函数。
它们与 Lambda 表达式均有紧密关系,或者说,辅助 Lambda 表达式的作用。
日后再深入探究五个看似没用的函数之间的异同点。

0x04 内联函数

省流:内联函数能够用来优化程序资源开销,与C++中的 #define A(x) 类似,正因如此,内联函数不能递归。

实际上就是辅助编译器优化的功能,也就是使用一个 inline 关键字来声明函数,这样一来凡是调用这个函数的地方,在编译的时候都会把这段代码给直接复制粘贴到对应位置(有点类似于 C 中的宏函数)此外,内联函数不能递归

做个简单比较:

inline fun plus(a:Int, b:Int) : String{
    return "$a + $b = ${a + b}"
}
fun main() {
    val t = plus(1, 5)
    println(t)
}

其编译成 Java 字节码然后反编译回的 Java 代码如下:

public final class HelloKt {
   @NotNull
   public static final String plus(int a, int b) {
      int $i$f$plus = 0;
      return a + " + " + b + " = " + (a + b);
   }

   public static final void main() {
      byte a$iv = 1;
      int b$iv = 5;
      int $i$f$plus = false;
      String t = a$iv + " + " + b$iv + " = " + (a$iv + b$iv);
      System.out.println(t);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

去除 inline 关键字后:

fun plus(a:Int, b:Int) : String{
    return "$a + $b = ${a + b}"
}
fun main() {
    val t = plus(1, 5)
    println(t)
}

反编译的 Java 代码如下:

public final class HelloKt {
   @NotNull
   public static final String plus(int a, int b) {
      return a + " + " + b + " = " + (a + b);
   }

   public static final void main() {
      String t = plus(1, 5);
      System.out.println(t);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

三、流程控制语句

0x00 Kotlin 选择结构

1. if-else 表达式

这与 Java 中的 if-else 用法基本相同,能够使用 if-else if-else的结构来控制程序流程。

kotlinifelse 关键字可以用来写三目表达式,例如:

val a1 = 10
val a2 = 20
val a3 = if(a1 > a2) a1 else a2

这就相当于 Java 中的:

final int a1 = 10;
final int a2 = 20;
final int a3 = (a1 > a2) ? a1 : a2;

kotlin 中甚至写:var a = if(2 > 3) 3 else if(5 > 9) 2 else 9,可以将其理解为多个选择分支,也可以理解成嵌套三目,按前者理解,其逻辑更加一目了然。

2. when 表达式

省流:相当于一大串 if-else if-else 语句

when 表达式是 switch 表达式的强化版,它既可以作为传统的 switch-case 结构使用,也可以作为一大组 if-else if-else语句使用,亦可以二者混合使用,本质上还是相当于一大组 if-else if-else

fun main() {
    var a = Integer.parseInt(Scanner(System.`in`).nextLine())
    when(a) {
        10 -> {
            println("is 10!")
        }
        8 -> {
            println("is 8!")
        }
        in 1..20 -> {
            println("in [1, 20]!")
        }
        else -> {
            println("else")
        }
    }
}

我这里习惯性地使用了 Java 中从标准输入流中读入数字的函数,可见 Kotlin 是能够直接调用 Java 代码的。当然,这里的代码也是靠 IntelliJ IDEA 辅助写出来的。

当输入 10 时,程序输出 is 10!,当输入 21 时,程序输出 in [1, 20]!,当输入 100 时,程序输出 else

从中我们得到三点信息:

  • when 结构中无需使用 break 关键字,它只会执行满足条件的其中一个分支包含的代码
  • when 结构会从上到下查找,并且只会执行第一个满足条件的分支包含的代码。因为在测试用例中,输入 10 时,它满足第一个与第三个分支的条件,但是却仅执行了第一个分支包含的代码。

这个 when 语句与以下代码等价:

if(a == 10) {
    println("is 10!")
}else if(a == 8) {
    println("is 8!")
}else if(a in 1..20) {
    println("in [1, 20]!")
}else {
    println("else")
}

此外,既然 when 本质上可以看作是一组 if-else if-else,它也可以作为类似于三目运算符使用。

当然,when后面可以不用带括号,因为它相当于一组 if-else if-else 结构,此前 when 括号内添加变量导致的衍生语法(直接写10就代表了 a == 10)更像是个语法糖。

0x01 Kotlin 循环结构

1. while 循环

Kotlin 中有 whiledo-while 循环,其用法与 Java 中的完全一致,此处不再赘述。

2. for 循环
a. 老 for 和新 for

Kotlin 中彻底取消了能够追溯到 C语言的 for([初始化语句]; 条件判断语句; [每次循环后执行的语句])的格式。

for(A; B; C) {
xxxxxx...
}

A;
while(B) {
xxxxxx...
C;
}

功能完全一致,for 循环原本在 C 语言中的结构没必要保留了。

在 Java 和 Python 中,for 循环可以用来遍历集合对象(例如数组等)中的所有元素,在 Java 中又称作所谓“增强 for”,而 Python 和 Kotlin已经取消了 C语言式 for循环,它们的 for 循环都相当于 Java 的“增强for”

我们在 Java 中可以使用这样的语句:for(int i = 0; i < array.length; i++) {array[i]......}或者 for(int ele : array) {ele.....} 的方式来处理数组,前者不仅能够读取数组元素,还能定位数组元素并对数组元素进行修改,后者就只能遍历一遍数组,除非额外添加外部变量辅助,python 的 for 循环就只有类似于后者的功能所以某些场景下使用起来未必方便,而 Kotlin则提供了一个更好的解决方案

b. Kotlin 的 for 循环

Kotlin 本质上只有一种 for 循环结构,也就是 Java 中所谓的增强 for

fun main() {
    val arr = intArrayOf(10, 20, 30, 40, 50, 60, 70, 80)
    for(x in arr) {
        print("$x, ")
    }
}

输出结果:

10, 20, 30, 40, 50, 60, 70, 80, 

但是别忘了 Kotlin 中的数组对象进行了封装并集成了一些其他有用的功能,假设我们的处理中需要使用各元素的下标号:

fun main() {
    val arr = intArrayOf(10, 20, 30, 40, 50, 60, 70, 80)
    for((i, x) in arr.withIndex()) {
        println("arr[$i] = $x")
    }
}

输出结果:

arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50
arr[5] = 60
arr[6] = 70
arr[7] = 80

上述代码也可以写成:

fun main() {
    val arr = intArrayOf(10, 20, 30, 40, 50, 60, 70, 80)
    for(t in arr.withIndex()) {
        println("arr[${t.index}] = ${t.value}")
    }
}

这里实质就是在遍历数组的下标及其内容组成的对子。

此外,for 也可以利用区间:

fun main() {
    for (i in 1..4) {
        print(i)
    }
}

输出结果:

1234

四、异常处理

0x00 Kotlin 对空指针异常的特殊处理

省流:

  • 一般类型的变量无法赋 null
  • 声明变量时类型名后面加 ? 号就可以给这个变量赋 null 值,称之为可空变量
  • 可控变量无法直接调用对象方法,需要用其他手段调用
    • ?. 调用,安全,不强制
    • !!. 调用,不安全,强制
    • Java 式先判定是否为空的方法。

Kotlin 对变量能否赋值为 null 做了限制,具体参见上面的省流部分。示例代码:

var x : Int? = null

(如果这里把 ? 号去掉,会报错)

? 号表明这个变量可能为空,因此不允许这个变量直接调用它的方法。下面讲解如何让可空变量调用方法,一共有三种方法。

  • 参考以下代码:

    fun main() {
        var x = readLine()
        if(x == "NULL") {
            x = null
        }
        println("${x == null}, ${x?.capitalize()}")
    }
    

    其中capitlize()函数的作用是根据 x 生成一个字符串,相比 x 会让 x 首字母大写。

    readLine() 就是从标准输入流中读入一行。readLine 函数返回的是一个 String? 类型,因此变量 x 就是一个可空字符。

    运行此程序,输入 NULL ,输出结果为:

    true, null
    

    这表明 x 的确赋值为了 null,后面让 x 调用 capitalize() 方法,但是却输出为 null,且程序没有报错,这就是 kotlin 对空指针异常的防范,也就是允许可空变量使用 ?. 调用方法。

  • 此外,也可以改成如下的调用形式:

    x!!.capitalize()
    

    这就是不管 x 是否为 null,均强制调用这个方法。这样的话可能会抛出 NullPointerException 异常。

  • 也可以使用类似于 Java 中对于可能为空的变量的处理方法,也就是用 if(x == null) 来判断 x 是否为空。

空合并运算符

使用 ?: 符号来为空对象提供一个默认的解决方案。

fun main() {
    var x = readLine()
    if(x == "NULL") {
        x = null
    }
    println("${x == null}, ${x?.capitalize()?:"this is null"}")
}

运行结果 (输入NULL):

true, this is null

0x01 Kotlin 异常处理

省流:和 Java 类似的 try-catch 以及 throw,但是 Kotlin 不需要像 Java 那样在函数声明时使用 throws 关键字标记会抛出的异常的类型。

fun except(){
    throw RuntimeException("Just a test")
}
fun main() {
    try {
        except()
    }catch (e:Exception){
        e.printStackTrace()
    }
}

输出结果:

java.lang.RuntimeException: Just a test
	at HelloKt.except(Hello.kt:2)
	at HelloKt.main(Hello.kt:6)
	at HelloKt.main(Hello.kt)

五、杂项

0x00 String类成员方法

省流:看 IDEA 的内置提示基本上能看出来这个函数是干啥的

1. substring

用于截取字符串。

fun main() {
    val a = "123456789"
    val b = a.substring(2)
    val c = a.substring(2, 4)
    val d = a.substring(2..4)
    println(b)
    println(c)
    println(d)
}

输出结果:

3456789
34
345
2. split

用于分割字符串

fun main() {
    val a = "111 222 333 444"
    val b = a.split(" ")
    println(b)
}

输出结果:

[111, 222, 333, 444]
3. replace

这里涉及到正则表达式的相关内容,在之后专题篇章中会专门讲解。

0x01 集合类


1. List

Kotlin 可以使用 listOf 函数创建一个 List 对象。

fun main() {
    val t:List<String> = listOf("a", "b", "c")
    println(t)
}

输出结果:

[a, b, c]

(示例代码中的 List<String> 可省略)

List 对象只能读取其中的内容,而无法修改其中的内容。
可以使用 t[0] 这样的方法来访问其中的内容,但是无法修改其中的内容,也就是说,类似于 Python 中的元组。

下面的代码演示遍历一个 List

fun main() {
    val t = listOf("a", "b", "c")
    for(str in t) {
        println(str)
    }
}

输出结果:

a
b
c
2. Set
a. 基本

Kotlin 可以使用 setOf 函数创建一个集合。一个集合中的元素是不可重复的,创建集合时如果出现重复元素则会自动去重。示例代码:

fun main() {
    val set = setOf("I", "Love", "You", "And", "You")
    println(set)
}

输出结果:

[I, Love, You, And]
b. 判定集合中是否含有某个元素

使用 containscontainAll 函数来判定集合中是否含有某个或某些元素:

fun main() {
    val set = setOf("I", "Love", "You", "And", "You")
    println(set.contains("I"))
    println(set.containsAll(setOf("I", "Love")))
}

输出结果:

true
true
c. 按索引查找集合中的元素

使用 elementAt 函数能够按照索引查找集合中的元素,但是由于 Kotlin 的集合的内部实现为链表,因此按照这种方式查找集合中的元素耗时较长,如果有按照索引查找元素的需求,应当采用 List

d. 添加新的元素
fun main() {
    val set = mutableSetOf("I", "Love", "You", "And", "You")
    set.add("kotlin")
    println(set)
}

输出结果:

[I, Love, You, And, kotlin]
3. Map
a. 基本

Kotlin 可以使用 mapOf 函数创建一个集合。

fun main() {
    val map = mapOf("Tim" to 24, Pair("Alice", 14), Pair("Jack", 26))
    println(map["Tim"])
}

输出结果:

24

to 是一个中缀函数,我们在之后讲解 Kotlin 的面向对象体系时会介绍这个内容。这里 Tim to 24 的作用与 Pair("Tim", 24) 一致。

b. 遍历
fun main() {
    val map = mapOf("Tim" to 24, Pair("Alice", 14), Pair("Jack", 26))
    for(i in map) {
        println("${i.key}, ${i.value}")
    }
}

输出结果:

Tim, 24
Alice, 14
Jack, 26
c. 修改
fun main() {
    val map = mutableMapOf("Tim" to 24, Pair("Alice", 14), Pair("Jack", 26))
    map["Tim"] = 20
    map["Jim"] = 50
    println("Tim = ${map["Tim"]}. Jim = ${map["Jim"]}")
}

需要使用 mutableMapOf 创建一个可以修改的 map

用起来与 Python 十分相似。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值