Kotlin语法手册(三)

Kotlin语法手册(三)

在使用kotlin时,由于掌握的不够牢靠,好多时候也还是Java编程的习惯,浪费了kotlin提供的语言特性,方便性,间接性,在阅读一些Android开源库的时候,由于好多都是kotlin语法编写的,看的比较费劲,还得去查阅kotlin的语法,比较不方便,故把kotlin的语法记录下来,方便查阅温故,巩固自己的基础知识。

Kotlin 中使用关键字 class 声明类,类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成,类头与类体都是可选的, 如果一个类没有类体,可以省略花括号;kotlin 中类默认是 publish(公有的) 且 final (不可继承)的。如下所示:

//无类头、类体
class Empty

构造函数

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数

主构造函数

主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

class Person constructor(firstName: String) { }

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字

class Person(firstName: String) { }

//如果没有函数体,则可以取消{}
class Person(firstName: String)

如果构造函数有注解或可见性修饰符,则 constructor 关键字是必需的,并且这些修饰符在它前面

class Customer public @Inject constructor(name: String) { /*……*/ }

主构造函数不能包含任何的代码,初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中,初始化块包含了在类被创建时执行的代码,主构造函数的参数可以在初始化块中使用。

如果需要的话,也可以在一个类中声明多个初始化语句块。需要注意的是,构造函数的参数如果用 val/var 进行修饰,则相当于在类内部声明了一个同名的全局属性。如果不加 val/var 进行修饰,则构造函数的参数只能在 init 函数块和全局属性初始化时进行引用。

class Person(val firstName: String, val lastName: String, var age: Int) { 
    init { 
    println("initializer blocks , firstName is: $firstName , lastName is: $lastName,age is:$age,") 
    }
}
次构造函数

类也可以声明前缀有 constructor的次构造函数。

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字:

class Point(val x: Int, val y: Int) {

    init {
        println("initializer blocks , x value is: $x , y value is: $y")
        
    }
    //通过this直接委托主构造函数
    constructor(base: Int) : this(base + 1, base + 1) {
        println("constructor(base: Int)")
    }

    //通过constructor(base: Int)次构造函数间接委托
    constructor(base: Long) : this(base.toInt()) {
        println("constructor(base: Long)")
    }

}

请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行

属性与字段

在 kotlin 中,在类中声明一个属性和声明一个变量一样是使用 val 和 var 关键字。val 变量只有一个 getter ,var 变量既有 getter 也有 setter

class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
    var state: String? = null
    var zip: String = "123456"
}

fun copyAddress(address: Address): Address {
    val result = Address() // Kotlin 中没有“new”关键字
    result.name = address.name // 将调用访问器
    result.street = address.street
    // ……
    return result
}

声明一个属性的完整语法是:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

初始化器(property_initializer)、getter、setter都是可选的。属性类型(PropertyType)如果可以从property_initializer或getter中推断出来也是可省略的。

//有默认的getter、setter,可以省略
var name = "Holmes, Sherlock"
自定义访问器

除了默认的getter和setter外,我们可以为属性定义自定义的访问器。如果我们定义了一个自定义的 getter,那么每次访问该属性时都会调用它;如果我们定义了一个自定义的 setter,那么每次给属性赋值时都会调用它。

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // 解析字符串并赋值给其他属性
    }

如果你需要改变一个访问器的可见性或者对其注解,但是不需要改变默认的实现, 你可以定义访问器而不定义其实现:

var setterVisibility: String = "abc"
    private set // 此 setter 是私有的并且有默认实现

var setterWithAnnotation: Any? = null
    @Inject set // 用 Inject 注解此 setter
延迟初始化属性与变量

一般地,属性声明为非空类型必须在构造函数中初始化,然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 为了应对这种情况,可以用 lateinit 修饰符来标记该属性,用于告诉编译器该属性会在稍后的时间被初始化。

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // 直接解引用
    }
}

该修饰符只能用于在类体中的属性(不是在主构造函数中声明的 var 属性,并且仅当该属性没有自定义 getter 或 setter 时),而自 Kotlin 1.2 起,也用于顶层属性与局部变量。该属性或变量必须为非空类型,并且不能是原生类型。
在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。

扩展函数

扩展函数类似Java 中实现的静态工具方法,用于为一个类增加一种新的行为。

//为 String 类声明一个扩展函数 lastChar() ,用于返回字符串的最后一个字符
//get方法是 String 类的内部方法,length 是 String 类的内部成员变量,在此处可以直接调用
fun String.lastChar() = get(length - 1)

//为 Int 类声明一个扩展函数 doubleValue() ,用于返回其两倍值
//this 关键字代表了 Int 值本身
fun Int.doubleValue() = this * 2

之后,我们就可以像调用类本身内部声明的方法一样,直接调用扩展函数

fun main() {
    val name = "leavesC"
    println("$name lastChar is: " + name.lastChar())

    val age = 24
    println("$age doubleValue is: " + age.doubleValue())
}

如果需要声明一个静态的扩展函数,则必须将其定义在伴生对象上,这样就可以在没有 Namer 实例的情况下调用其扩展函数,就如同在调用 Java 的静态函数一样

class Namer {

    companion object {

        val defaultName = "mike"

    }

}

fun Namer.Companion.getName(): String {
    return defaultName
}

fun main() {
    Namer.getName()
}

需要注意的是,如果扩展函数声明于 class 内部,则该扩展函数只能该类和其子类内部调用,此时相当于声明了一个非静态函数,外部无法引用到。所以一般都是将扩展函数声明为全局函数。

对于扩展函数来说,如果基类和子类都分别定义了一个同名的扩展函数,此时要调用哪个扩展函数是由变量的静态类型来决定的,而非这个变量的运行时类型。

fun main() {
    val view: View = Button()
    
    //运行时的类型
    view.click()//Button clicked
    
    //静态类型
    view.longClick() //View longClicked
}

open class View {
    open fun click() = println("View clicked")
}

class Button : View() {
    override fun click() = println("Button clicked")
}

fun View.longClick() = println("View longClicked")

fun Button.longClick() = println("Button longClicked")

如果一个类的成员函数和扩展函数有相同的签名,成员函数会被优先使用

扩展函数并不是真正地修改了原来的类,其底层其实是以静态导入的方式来实现的。扩展函数可以被声明在任何一个文件中,因此有个通用的实践是把一系列有关的函数放在一个新建的文件里

需要注意的是,扩展函数不会自动地在整个项目范围内生效,如果需要使用到扩展函数,需要进行导入

扩展属性

扩展函数也可以用于属性

//扩展函数也可以用于属性
//为 String 类新增一个属性值 customLen
var String.customLen: Int
    get() = length
    set(value) {
        println("set")
    }

fun main() {
    val name = "leavesC"
    println(name.customLen)//会使用get方法
    name.customLen = 10//会使用set方法
    println(name.customLen)
    //7
    //set
    //7
}

修饰符

在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public

1、public

public 修饰符是限制级最低的修饰符,对所有都是可用的,是默认的修饰符

2、protected

protected 修饰符只能被用在类或者接口中的成员上,protected 成员只在该类和它的子类中可见。

3、internal

一个定义为 internal 的包成员,对其所在的整个 module 可见,但对于其它 module 而言就是不可见的了。例如,假设我们想要发布一个开源库,库中包含某个类,我们希望这个类对于库本身是全局可见的,但对于外部使用者来说它不能被引用到,此时就可以选择将其声明为 internal 的来实现这个目的。

4、private

意味着只在这个类内部(包含其所有成员)可见

5、final和open

kotlin 中的类和方法默认都是 final 的,即不可继承的,如果想允许创建一个类的子类,需要使用 open 修饰符来标识这个类,此外,也需要为每一个希望被重写的属性和方法添加 open 修饰符

open class View {
    open fun click() {

    }
	//不能在子类中被重写
    fun longClick() {

    }
}

class Button : View() {
    override fun click() {
        super.click()
    }
}

如果重写了一个基类或者接口的成员,重写了的成员同样默认是 open 的。比如上面代码中,如果 Button 类是 open 的,则其子类也可以重写其 click() 方法,如果你想禁止再次覆盖,使用 final 关闭。

open class Button : View() {
    final override fun click() {
        super.click()
    }
}

类的分类

抽象类

类以及其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数,因为这是默认的。

abstract class BaseClass {
    abstract fun fun1()
}

数据类

我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data
定义一个新的数据类非常简单:

data class Point(val x: Int, val y: Int)

数据类默认地为主构造函数中声明的所有属性生成了如下几个方法:

  • getter、setter(需要是 var)
  • componentN()。按主构造函数的属性声明顺序进行对应
  • copy()
  • toString()
  • hashCode()
  • equals()

为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:

  • 主构造函数需要包含一个参数
  • 主构造函数的所有参数需要标记为 val 或 var
  • 数据类不能是抽象、开放、密封或者内部的

User反编译成为Java类如下:

public final class Point {
   private final int x;
   private final int y;

   public final int getX() {
      return this.x;
   }

   public final int getY() {
      return this.y;
   }

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }

   public final int component1() {
      return this.x;
   }

   public final int component2() {
      return this.y;
   }

   @NotNull
   public final Point copy(int x, int y) {
      return new Point(x, y);
   }

   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static Point copy$default(Point var0, int var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.x;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.y;
      }

      return var0.copy(var1, var2);
   }

   public String toString() {
      return "Point(x=" + this.x + ", y=" + this.y + ")";
   }

   public int hashCode() {
      return this.x * 31 + this.y;
   }

   public boolean equals(Object var1) {
      if (this != var1) {
         if (var1 instanceof Point) {
            Point var2 = (Point)var1;
            if (this.x == var2.x && this.y == var2.y) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

其实它就是java的POJO 的模版代码。

通过数据类可以简化很多的通用操作,可以很方便地进行:格式化输出变量值、映射对象到变量、对比变量之间的相等性、复制变量等操作。

fun main() {
    val point1 = Point(10, 20)
    val point2 = Point(10, 20)
    println("point1 toString() : $point1") //point1 toString() : Point(x=10, y=20)
    println("point2 toString() : $point2") //point2 toString() : Point(x=10, y=20)

    val (x, y) = point1
    println("point1 x is $x,point1 y is $y") //point1 x is 10,point1 y is 20

    //在 kotlin 中,“ == ” 相当于 Java 的 equals 方法
    //而 “ === ” 相当于 Java 的 “ == ” 方法
    println("point1 == point2 : ${point1 == point2}") //point1 == point2 : true
    println("point1 === point2 : ${point1 === point2}") //point1 === point2 : false

    val point3 = point1.copy(y = 30)
    println("point3 toString() : $point3") //point3 toString() : Point(x=10, y=30)
}

需要注意的是,数据类的 toString()、equals()、hashCode()、copy() 等方法只考虑主构造函数中声明的属性。

密封类

Sealed 类(密封类)用于对类可能创建的子类进行限制,用 Sealed 修饰的类的直接子类只允许被定义在 Sealed 类所在的文件中(密封类的间接继承者可以定义在其他文件中)。这有助于帮助开发者掌握父类与子类之间的变动关系,避免由于代码更迭导致的潜在 bug,且密封类的构造函数只能是 private 的。Sealed 修饰符修饰的类也隐含表示该类为 open 类,因此无需再显式地添加 open 修饰符。

sealed class View {

    fun click() {

    }

}
//对于 View 类,其子类(Button、TextView)只能定义在与之同一个文件里
class Button : View() {

}

class TextView : View() {

}

因为 Sealed 类的子类对于编译器来说是可控的,所以如果在 when 表达式中处理了所有 Sealed 类的子类,那就不需要再提供 else 默认分支。即使以后由于业务变动又新增了 View 子类,编译器也会检测到 check 方法缺少分支检查后报错,所以说 check 方法是类型安全的

fun check(view: View): Boolean {
    when (view) {
        is Button -> {
            println("is Button")
            return true
        }
        is TextView -> {
            println("is TextView")
            return true
        }
    }
}

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

enum class Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

枚举可以声明一些参数

enum class Day(val index: Int) {
    SUNDAY(0), MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6)
}

枚举也可以实现接口

interface OnChangedListener {

    fun onChanged()

}

enum class Day(val index: Int) : OnChangedListener {
    SUNDAY(0) {
        override fun onChanged() {

        }
    },
    MONDAY(1) {
        override fun onChanged() {
            
        }
    }
}

枚举也包含有一些共有函数

fun main() {
    val day = Day.FRIDAY
    //获取值
    val value = day.index  //5
    //通过 String 获取相应的枚举值
    val value1 = Day.valueOf("SUNDAY") //SUNDAY
    //获取包含所有枚举值的数组
    val value2 = Day.values()
    //获取枚举名
    val value3 = Day.SUNDAY.name //SUNDAY
    //获取枚举声明的位置
    val value4 = Day.TUESDAY.ordinal //2
}

嵌套类

在 kotlin 中在类里面再定义的类默认是嵌套类,此时嵌套类不会包含对外部类的隐式引用。

class Outer {

    private val bar = 1

    class Nested {
        fun foo1() = 2
    }
}

嵌套类翻译成java语言就是内部静态类,如下所示:

public final class Outer {
   private final int bar = 1;

   public static final class Nested {
      public final int foo1() {
         return 2;
      }
   }
}

可以看到 Nested 其实就是一个静态类,因此不能访问外部类的非静态成员,比如在foo1()方法中就不能访问bar变量

内部类

如果需要去访问外部类的成员,需要用 inner 修饰符来标注被嵌套的类,这称为内部类。内部类会隐式持有对外部类的引用。

class Outer {

    private val bar = 1

    inner class Nested {
        fun foo1() = 2
        fun foo2() = bar//这时是可以访问外部bar变量的
    }
}

翻译成java语言,如下所示:

public final class Outer {
   private final int bar = 1;

   public final class Nested {
      public final int foo1() {
         return 2;
      }

      public final int foo2() {
         return Outer.this.bar;
      }
   }
}

综上所述,在kotlin中,嵌套类就是java中就相当于static class A;内部类在java中就相当于class A,在kotlin中,需要在class关键字前加上inner,就是内部类。

匿名内部类

可以使用对象表达式来创建匿名内部类实例

window.addMouseListener(object : MouseAdapter() {

    override fun mouseClicked(e: MouseEvent) { …… }

    override fun mouseEntered(e: MouseEvent) { …… }
})

内联类

先看如下代码:

fun sendEmail(delay: Long) {
    println(delay)
}

对于 sendEmail 方法的入参参数而言,我们无法严格限制入参参数的含义类型,有的开发者可能会将 delay 理解为以毫秒为单位,有的开发者可能又会理解为以分钟为单位。
为了提升程序的健壮性,我们可以通过声明一个包装类来作为参数类型:

fun sendEmail(delay: Time) {
    println(delay.second)
}

class Time(val second: Long)

class Minute(private val count: Int) {

    fun toTime(): Time {
        return Time(count * 60L)
    }

}

fun main() {
    sendEmail(Minute(10).toTime())
}

这样,在代码源头上就限制了开发者能够传入的参数类型,开发者通过类名就能直接表达出自己希望的时间大小。然而这种方式由于额外的堆内存分配问题,就增加了运行时的性能开销,新的包装类相对原生类型所需要的性能消耗要大得多。
内联类(InlineClass)就可以解决这个问题。使用内敛类,上述代码可以重写为如下:

fun sendEmail(delay: Time) {
    println(delay.second)
}

inline class Time(val second: Long)

inline class Minute(private val count: Int) {

    fun toTime(): Time {
        return Time(count * 60L)
    }

}

fun main() {
    sendEmail(Minute(10).toTime())
}

使用 inline 修饰的类就称为内联类,内联类必须含有唯一的一个属性在主构造函数中初始化,在运行时将使用这个唯一属性来表示内联类的实例,从而避免了包装类在运行时的额外开销。使用内联类后,sendEmail方法会被解释为一个以 long 类型作为入参类型的函数,并不包含任何对象,不会有性能上的消耗。

继承

在 Kotlin 中所有类都有一个共同的超类 Any,类java所有类都有个超类object;any有三个方法:equals()、 hashCode() 与 toString()。因此,所有的kotlin类都定义了这些方法。前面介绍关键字的时候已经提到,默认情况下,kotlin类是final类行的,不能被继承,要使一个类可继承,需用open关键字标记它。

open class Base(p: Int)

class Derived(p: Int) : Base(p)

如果派生类有一个主构造函数,其基类型必须直接或间接调用基类的主构造函数

open class Base(val str: String)

class SubClass(val strValue: String) : Base(strValue)

class SubClass2 : Base {

    constructor(strValue: String) : super(strValue)

    constructor(intValue: Int) : super(intValue.toString())

    constructor(doubValue: Double) : this(doubValue.toString())

}

覆盖方法

kotlin 需要显式标注可覆盖的成员和覆盖后的成员:

open class Base() {
    open fun fun1() {

    }

    fun fun2() {
        
    }
}

class SubClass() : Base() {
    override fun fun1() {
        super.fun1()
    }
}

用 open 标注的函数才可以被子类重载,子类用 override 表示该函数是要对父类的同签名函数进行覆盖。标记为 override 的成员本身也是开放的,可以被子类覆盖。如果想禁止再次覆盖,可以使用 final 关键字标记。如果父类没有使用 open 对函数进行标注,则子类不允许定义相同签名的函数。对于一个 final 类(没有用 open 标注的类)来说,使用 open 标记属性和方法是无意义的。

覆盖属性

属性覆盖与方法覆盖类似;在父类中声明,然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。 每个声明的属性可以由具有初始化器的属性或者具有 get 方法的属性覆盖。

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。因为一个 val 属性本质上声明了一个 get 方法, 而将其覆盖为 var 只是在子类中额外声明一个 set 方法。
请注意,你可以在主构造函数中使用 override 关键字作为属性声明的一部分。

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点

class Polygon : Shape {
    override var vertexCount: Int = 0  // 以后可以设置为任何数
}

调用超类实现

派生类可以通过 super 关键字调用其超类的函数与属性访问器的实现

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

class SubClass : BaseClass() {

    override fun fun1() {
        super.fun1()
    }

}

对于内部类来说,其本身就可以直接调用调用外部类的函数

open class BaseClass2 {
    private fun fun1() {
        println("BaseClass fun1")
    }

    inner class InnerClass {
        fun fun2() {
            fun1()
        }
    }

}

但如果想要在一个内部类中访问外部类的父类,则需要通过由外部类名限定的 super 关键字来实现

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

class SubClass : BaseClass() {

    override fun fun1() {
        println("SubClass fun1")
    }

    inner class InnerClass {

        fun fun2() {
            super@SubClass.fun1()
        }

    }

}

fun main() {
    val subClass = SubClass()
    val innerClass = subClass.InnerClass()
    //BaseClass fun1
    innerClass.fun2()
}

如果一个类从它的直接超类和实现的接口中继承了相同成员的多个实现, 则必须覆盖这个成员并提供其自己的实现来消除歧义

为了表示采用从哪个超类型继承的实现,使用由尖括号中超类型名限定的 super 来指定,如 super< BaseClass >

open class BaseClass {
    open fun fun1() {
        println("BaseClass fun1")
    }
}

interface BaseInterface {
    //接口成员默认就是 open 的
    fun fun1() {
        println("BaseInterface fun1")
    }
}

class SubClass() : BaseClass(), BaseInterface {
    override fun fun1() {
        //调用 SubClass 的 fun1() 函数
        super<BaseClass>.fun1()
        //调用 BaseInterface 的 fun1() 函数
        super<BaseInterface>.fun1()
    }
}

接口

kotlin 中的接口与 Java 8 中的类似,可以包含抽象方法的定义以及非抽象方法的实现。

class View : Clickable {
    
    override fun click() {
        println("clicked")
    }

}

interface Clickable {
    fun click()
    fun longClick() = println("longClicked")
}

如果一个类实现了多个接口,而接口包含带有默认实现且名字相同的方法,此时编译器就会要求开发者必须显式地实现该方法,可以选择在该方法中调用不同接口的相应实现。

class View : Clickable, Clickable2 {

    override fun click() {
        println("clicked")
    }

    override fun longClick() {
        super<Clickable>.longClick()
        super<Clickable2>.longClick()
    }
}

interface Clickable {
    fun click()
    fun longClick() = println("longClicked")
}

interface Clickable2 {
    fun click()
    fun longClick() = println("longClicked2")
}

接口中的属性

可以在接口中定义属性。在接口中声明的属性要么是抽象的,要么提供访问器的实现。

interface MyInterface {
    val prop: Int // 抽象的

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

SAM接口

只有一个抽象方法的接口称为函数式接口或 SAM(单一抽象方法,Single Abstract Method) 接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。

可以用 fun 修饰符在 Kotlin 中声明一个函数式接口。

fun interface KRunnable {
   fun invoke()
}
SAM转换

对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性。

例如,有这样一个 Kotlin 函数式接口:

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

如果不使用 SAM 转换,那么你需要像这样编写代码:

// 创建一个类的实例
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:

// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值