Scala入门系列(7)-Scala之面向对象编程

类与对象

Java是面向对象的编程语言,由于历史原因,Java中还存在着非面向对象的 内容:基本类型 ,null,静态方法等。Scala语言来自于Java,所以天生就是面向对象的语言,而且Scala是纯粹的 面向对象的语言,即在Scala中,一切皆为对象。

在这里插入图片描述
类和对象的区别和联系

  1. 类是抽象的,概念的,代表一类事物,比如人类,猫类…
  2. 对象是具体的,实际的,代表一个具体事物
  3. 类是对象的模板,对象是类的一个个体,对应一个实例
  4. Scala中类和对象的区别和联系 和 Java是一样的。

定义类

基本语法

[修饰符] class 类名 { 
类体 
}

注意事项:

  1. scala语法中,类并不声明为public,所有这些类都具有公有可见性(即默认就 是public)
  2. 一个Scala源文件可以包含多个类
  3. 属性是类的一个组成部分,一般是值数据类型,也可是引用类型。

属性/成员变量

注意事项:

  1. 属性的定义语法同变量,示例:[访问修饰符] var 属性名称 [:类型] = 属性值
  2. 属性的定义类型可以为任意类型,包含值类型或引用类型
  3. Scala中声明一个属性,必须显示的初始化,然后根据初始化数据的类型自动推 断,属性类型可以省略(这点和Java不同)。
  4. 如果赋值为null,则一定要加类型,因为不加类型, 那么该属性的类型就是Null 类型
  5. 如果在定义属性时,暂时不赋值,也可以使用符号_(下划线),让系统分配默 认值. 整数0,浮点0.0,布尔false,引用null。
  6. 不同对象的属性是独立,互不影响,一个对象对属性的更改,不影响另外一个,这点和java完全一样
class Cat {
  var name: String = "cat"
  var age: Int = _
  var color: String = _
  var friend: Dog = new Dog()
}

class Dog {
}

创建对象

基本语法:

val | var 对象名 [:类型] = new 类型()

注意事项

  1. 如果我们不希望改变对象的引用(即:内存地址), 应该声明为val 性质的,否则 声明为var, scala设计者推荐使用val ,因为一般来说,在程序中,我们只是改变 对象属性的值,而不是改变对象的引用。
  2. scala在声明对象变量时,可以根据创建对象的类型自动推断,所以类型声明 可以省略,但当类型和后面new 对象类型有继承关系即多态时,就必须写了

方法

Scala中的方法其实就是函数
基本语法

def 方法名(参数列表) [:返回值类型] = { 方法体 }

调用机制

  1. 当我们scala开始执行时,先在栈区开辟一个main栈。main栈是最后被销毁
  2. 当scala程序在执行到一个方法时,总会开一个新的栈。
  3. 每个栈是独立的空间,变量(基本数据类型)是独立的,相互不影响(引用类型除外)
  4. 当方法执行完毕后,该方法开辟的栈就会被jvm机回收。

构造器

构造器(constructor)又叫构造方法,是类的一种特殊的方法,它的主要作用是完 成对新对象的初始化。和Java一样,Scala构造对象也需要调用构造方法,并且可以有任意多个构造 方法(即scala中构造器也支持重载)。 Scala类的构造器包括: 主构造器 和 辅助构造器

基本语法

class 类名(形参列表) { // 主构造器 
	// 类体 
	def this(形参列表) { 
	// 辅助构造器 }
	def this(形参列表) { 
	//辅助构造器可以有多个... 
	} 
}

注意事项:

  1. Scala构造器作用是完成对新对象的初始化,构造器没有返回值。
  2. 主构造器的声明直接放置于类名之后
  3. 主构造器会执行类定义中的所有语句,这里可以体会到Scala的函数式编程 和面向对象编程融合在一起,即:构造器也是方法(函数),传递参数和 使用方法和前面的函数部分内容没有区别
  4. 如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括 号也可以省略
  5. 辅助构造器名称为this(这个和Java是不一样的),多个辅助构造器通过不 同参数列表进行区分, 在底层就是f构造器重载。
  6. 如果想让主构造器变成私有的,可以在()之前加上private,这样用户只能通 过辅助构造器来构造对象了
  7. 辅助构造器的声明不能和主构造器的声 明一致,会发生错误(即构造器名重复)
object Oop001 {
  def main(args: Array[String]): Unit = {
    val person1: Person = new Person("张三", 18)

  }
}

//private主构造器变成私有
class Person private(name: String, age: Int, sex: Int) { // 主构造器

  def this(name: String, age: Int) { //辅助构造器
    //辅助构造器无论是直接或间接,最终都一定要调用主构造器,执行主构造器的逻辑
    //而且需要放在辅助构造器的第一行[这点和java一样,java中一个构造器要调用同类的其它构造器,也需要放在第
    this(name, age, 1)
    println("辅助构造器")
  }

  println("主构造器")

  def test001(): Unit = {
    println("test001")
  }
}

构造器参数

  1. Scala类的主构造器的形参未用任何修饰符修饰,那么这个参数是局部变量。
  2. 如果参数使用val关键字声明,那么Scala会将参数作为类的私有的只读属性 使用
  3. 如果参数使用var关键字声明,那么那么Scala会将参数作为类的成员属性使 用,并会提供属性对应的xxx()[类似getter]/xxx_$eq()[类似setter]方法,即这时 的成员属性是私有的,但是可读写。

Bean属性

JavaBeans规范定义了Java的属性是像getXxx()和setXxx()的方法。许多 Java工具(框架)都依赖这个命名习惯。为了Java的互操作性。将Scala字段 加@BeanProperty时,这样会自动生成规范的 setXxx/getXxx 方法。这时可 以使用 对象.setXxx() 和 对象.getXxx() 来调用属性。给某个属性加入@BeanPropetry注解后,会生成getXXX和setXXX的方法 并且对原来底层自动生成类似xxx(),xxx_$eq()方法,没有冲突,二者可以共存。

class Animal {
  @BeanProperty
  var name: String = _
  @BeanProperty
  var age: Int = _
}

对象创建的流程分析

  1. 加载类的信息(属性信息,方法信息)
  2. 在内存中(堆)开辟空间
  3. 使用父类的构造器(主和辅助)进行初始
  4. 使用主构造器对属性进行初始化
  5. 使用辅助构造器对属性进行初始化
  6. 将开辟的对象的地址赋给变量引用

和Java一样,Scala中管理项目可以使用包,但Scala中的包的功能更加强大, 使用也相对复杂。

基本语法

package 包名

三大作用

  1. 区分相同名字的类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围

命名规则
只能包含数字、字母、下划线、小圆点.,但不能用数字开头, 也不要使用关键字。

命名规范
一般是小写字母+小圆点,一般是 com.公司名.项目名.业务模块名

注意事项

  1. 包也可以像嵌套类那样嵌套使用(包中有包),可以在同一个文件中, 将类(class / object)、trait 创建在不同的包中,这样就非常灵活了。

在这里插入图片描述

  1. 也可以写成多行
    在这里插入图片描述

  2. 作用域原则:可以直接向上访问。 即: Scala中子包中直接访问父包 中的内容, 大括号体现作用域。 (提示:Java中子包使用父包的类, 需要import)。在子包和父包 类重 名时,默认采用就近原则,如果 希望指定使用某个类,则带上包 名即可。

  3. 父包要访问子包的内容时,需要 import对应的类

  4. 包名可以相对也可以绝对,比如,访问BeanProperty的绝对路径是: root. scala.beans.BeanProperty ,在一般情况下:我们使用相对路径来引 入包,只有当包名冲突时,使用绝对路径来处理。
    在这里插入图片描述

包对象

包可以包含类、对 象和特质trait(接口),但不能包含函 数/方法或变量的定义。这是 Java虚拟机的局限。为了弥补 这一点不足,scala提供了包对象的概念来解决这个问题。

基本语法

package object 包名

注意事项

  1. 包对象中可以定义变量,函数
  2. 创建包对象必须和包的名字一致
  3. 包对象中的义变量,函数可字节在包中使用
package object Oop004{
  var name:String="张三"
  def test(): Unit ={
    println(" package object Oop004 test ")
  }
}

package Oop004 {

  object Test008{
    def main(args: Array[String]): Unit = {
      println(name)
      test()
    }
  }
}

包对象的底层实现机制:

  1. 当创建包对象后,在该包下生成 public final class package 和 public final class package$
  2. 通过 package$ 的一个静态实例完成对包对象中的属性和方法的调用。

包的可见性

Java访问修饰符

在这里插入图片描述

Scala中包的可见性

在Java中,访问权限分为: public,private,protected和默认。在Scala中,你可 以通过类似的修饰符达到同样的效果。但是使用上有区别。

  1. 当属性访问权限为默认时,从底层看属性是private的,但是因为提供了 xxx_$eq()[类似setter]/xxx()[类似getter] 方法,因此从使用效果看是任何地方 都可以访问)
  2. 当方法访问权限为默认时,默认为public访问权限
  3. private为私有权限,只在类的内部和伴生对象中可用
  4. protected为受保护权限,scala中受保护权限比Java中更严格,只能子类访问, 同包无法访问 (编译器)
  5. 在scala中没有public关键字,即不能用public显式的修饰属性和方法。
  6. 包访问权限(表示属性有了限制。同时包也有了限制),这点和Java不一样, 体现出Scala包使用的灵活性。

包的引入

Scala引入包也是使用import, 基本的原理和机制和Java一样,但是Scala中的 import功能更加强大,也更灵活。 因为Scala语言源自于Java,所以java.lang包中的类会自动引入到当前环境中,而 Scala中的scala包和Predef包的类也会自动引入到当前环境中,即起其下面的类 可以直接使用。 如果想要把其他包中的类引入到当前环境中,需要使用import语言。

注意事项

  1. 在Scala中,import语句可以出现在任何地方,并不仅限于文件顶部,import语 句的作用一直延伸到包含该语句的块末尾。这种语法的好处是:在需要时在引 入包,缩小import 包的作用范围,提高效率。
import scala.List
class Oop005 {

  import scala.beans.BeanProperty
  @BeanProperty
  var name:String="张三"
}
  1. Java中如果想要导入包中所有的类,可以通过通配符*,Scala中采用下 _
import scala.collection._
  1. 如果不想要某个包中全部的类,而是其中的几个类,可以采用选取器
import scala.collection.{Searching,GenIterable}
  1. 如果引入的多个包中含有相同的类,那么可以将不需要的类进行重命名进行区 分,这个就是重命名。
  import java.util.{ HashMap=>JavaHashMap, List}
  var map1 = new JavaHashMap();
  1. 如果某个冲突的类根本就不会用到,那么这个类可以直接隐藏掉。
 import java.util.{ HashMap=>_, List}

封装

封装(encapsulation)就是把抽象出的数据和对数据的操作封装在一起,数据被 保护在内部,程序的其它部分只有通过被授权的操作(成员方法),才能对数据进 行操作。
注意事项:

  1. Scala中为了简化代码的开发,当声明属性时,本身就自动提供了对应setter/getter 方法,如果属性声明为private的,那么自动生成的setter/getter方法也是private的, 如果属性省略访问权限修饰符,那么自动生成的setter/getter方法是public的。
  2. 因此我们如果只是对一个属性进行简单的set和get ,只要声明一下该属性(属性使 用默认访问修饰符) 不用写专门的getset,默认会创建,访问时,直接对象.变量。 这样也是为了保持访问一致性
  3. 从形式上看 dog.food 直接访问属性,其实底层仍然是访问的方法, 看一下反编译 的代码就明白
  4. 有了上面的特性,目前很多新的框架,在进行反射时,也支持对属性的直接反射

继承

继承可以解决代码复用,让我们的编程更加靠近人类思维.当多个类存在相同 的属性(变量)和方法时,可以从这些类中抽象出父类(比如Student),在父类中定 义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只 需要通过extends语句来声明继承父类即可。

基本语法

class 子类名 extends 父类名 { 类体 }

注意事项

  1. 使用继承代码的复用性、代码的扩展性和维护性提高了。
  2. 子类继承了所有的属性,只是私有和受保护的属性不能直接访问, 需要通过公共的方法去访问。
  3. 重写一个非抽象方法需要用override修饰符,调用超类的 方法使用super关键字
class Person {
  var name: String = _
  var age: Int = _
  // 私有属性不能访问
  private var test: String = _

  protected var test001: String = _

  def sayHello(): Unit = {
    println("sayHello")
  }

  // 私有方法不能访问
  private def privateSayHello(): Unit = {
    println("privateSayHello")
  }

  // 受保护的方法不能访问
  protected def protectedSayHello(): Unit = {
    println("protectedSayHello")
  }
}

class Student extends Person {
  var className: String = _
  var stuNo: Int = _

  def study(): Unit = {
    println("study")
  }
  // 重写父类方法
  override def sayHello(): Unit ={
    println("重写父类方法")
  }
}

object Oop006 {
  def main(args: Array[String]): Unit = {
    val student = new org.pearl.scala.demo.oop.Student()
    // 父类方法
    student.sayHello()
    // 父类属性
    //println(student.test)
    // student.privateSayHello
    //student.protectedSayHello
    println(student.name)
  }
}

类型检查和转换

要测试某个对象是否属于某个给定的类,可以用 isInstanceOf方法。用asInstanceOf方法将引用转 换为子类的引用。classOf获取对象的类名。可以判断传入对象的类型,然后转成对应的子 类对象,进行相关操作,这里也体现出多态的特点。

注意事项

  1. classOf[String]就如同Java的 String.class 。
  2. obj.isInstanceOf[T]就如同Java的obj instanceof T 判断obj是不是T类型。
  3. obj.asInstanceOf[T]就如同Java的(T)obj 将obj强 转成T类型。
class Oop008 {
  var name: String = _
  var age: Int = _

  def sayHello: Unit = {
    println("sayHello")
  }
}

class SubOop008 extends Oop008 {

}

object Test008 {
  def main(args: Array[String]): Unit = {
    val oop008 = new SubOop008()
    println(oop008.isInstanceOf[Oop008])
    // 子类转为父类
    val test008: Oop008 = oop008.asInstanceOf[Oop008]
    // 打印类
    val subOop008 = classOf[SubOop008]
    println(subOop008)
    println()
  }
}

超类的构造

在Java中,创建子类对象时,子类的构造器总是去调用一个 父类的构造器(显式或者隐式调用)。在Scala中,类有一个主构器和任意数量的辅助构造器,而每个辅助构造器都必须先调用 主构造器(也可以是间接调用)。只有主构造器可以调用父类的构造器。辅助构造器不能直接调用父类的构造器。

覆写字段

在Scala中,子类改写父类的字段,我们称为覆写/重写字段。覆写字段需使用 override修饰。在Java中只有方法的重写,没有属性/字段的重写,准确的讲,是隐藏字 段代替了重写。

抽象类

在Scala中,通过abstract关键字标记不能被实例化的类。方法不用标记abstract, 只要省掉方法体即可。抽象类可以拥有抽象字段,抽象字段/属性就是没有初始 值的字段。抽象类的价值更多是在于设计,是设计者设计好后,让子类继承并 实现抽象类。
基本语法

abstract class Person() { 
// 抽象类 
var name: String // 抽象字段, 没有初始化 
def printName // 抽象方法, 没有方法体 
}

注意事项

  1. 抽象类不能被实例
  2. 抽象类不一定要包含abstract方法。也就是说,抽象类可以没有abstract方法
  3. 一旦类包含了抽象方法或者抽象属性,则这个类必须声明为abstract
  4. 抽象方法不能有主体,不允许使用abstract修饰。
  5. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法和抽象属性, 除非它自己也声明为abstract类。【
  6. 抽象方法和抽象属性不能使用private、final 来修饰,因为这些关键字都 是和重写/实现相违背的。
  7. 抽象类中可以有实现的方法.
  8. 子类重写抽象方法不需要override,写上也不会错

匿名子类

和Java一样,可以通过包含带有定义或重写的代码块的方式创建一个匿名的子类.

继承层级

在这里插入图片描述

  1. 在scala中,所有其他类都是AnyRef的子类,类似Java的Object。
  2. AnyVal和AnyRef都扩展自Any类。Any类是根节点
  3. Any中定义了isInstanceOf、asInstanceOf方法,以及哈希方法等。
  4. Null类型的唯一实例就是null对象。可以将null赋值给任何引用,但不能赋值 给值类型的变量[案例演示]。
  5. Nothing类型没有实例。它对于泛型结构是有用处的,举例:空列表Nil的类 型是List[Nothing],它是List[T]的子类型,T可以是任何类。

静态属性和静态方法

Java中静态方法并不是通过对象调用的,而是通过类对象调用的,所以 静态操作并不是面向对象的。Scala语言是完全面向对象(万物皆对象)的语言,所以并没有静态的操作 (即在Scala中没有静态的概念)。但是为了能够和Java语言交互(因为Java中有静 态概念),就产生了一种特殊的对象来模拟类对象,我们称之为类的伴生对象。这个类的所有静态内容都可以放置在它的伴生对象中声明和调用。

注意事项

  1. Scala中伴生对象采用object关键字声明,伴生对象中声明的全是 “静态” 内容,可以通过伴生对象名称直接调用。
  2. 伴生对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致。
  3. 伴生对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问
  4. 从语法角度来讲,所谓的伴生对象其实就是类的静态方法和成员的集合
  5. 从技术角度来讲,scala还是没有生成静态的内容,只不过是将伴生对 象生成了一个新的类,实现属性和方法的调用。
  6. 从底层原理看,伴生对象实现静态特性是依赖于 public static final MODULE$ 实现的。伴生对象的声明应该和伴生类的声明在同一个源码文件中(如果不在同一 个文件中会运行错误!),但是如果没有伴生类,也就没有所谓的伴生对 象了,所以放在哪里就无所谓了。
  7. 如果 class A 独立存在,那么A就是一个类, 如果 object A 独立存在,那 么A就是一个"静态"性质的对象[即类对象], 在 object A中声明的属性和方 法可以通过 A.属性 和 A.方法 来实现调用当一个文件中,存在伴生类和伴生对象时,文件的图标会发生变化
  8. 伴生对象中定义apply方法 ,可以实现: 类名(参数) 方式 来创建对象实例
// 伴生类 同一个文件中名字相同的class和object,互为伴生类伴生对象
class Demo001 {
  def test001(): Unit = {
    Demo001.test001()
  }
}

// 伴生对象
object Demo001 {
  def main(args: Array[String]): Unit = {

  }

  def apply(): Demo001 = new Demo001()

  def test001(): Unit = {
    println("test001")
  }
}

trait

从面向对象来看,接口并不属于面向对象的范畴,Scala是纯面向对象的语言, 在Scala中,没有接口。 Scala语言中,采用特质trait(特征)来代替接口的概念,也就是说,多个类具 有相同的特征(特征)时,就可以将这个特质(特征)独立出来,采用关键字 trait声明。 理解trait 等价于(interface + abstract class)。Scala的继承是单继承,也就是一个类最多只能有一个父类,这种单继承的机制可保证类 的纯洁性,比c++中的多继承机制简洁。但对子类功能的扩展有一定影响.所以 我们认为: Scala引入trait特征 第一可以替代Java的接口, 第二个也是对单继承机制 的一种补充。

基本语法

trait 特质名 { trait}

注意事项

  1. Scala提供了特质(trait) ,特质可以同时拥有抽 象方法和具体方法,一 个类可以实现/继承多个 特质。
  2. 特质中没有实现的方法就是抽象方法。类通过extends继承特质,通过with 可以继承多个特质
  3. 所有的java接口都可以当做Scala特质使用
  4. 和Java中的接口不太一样的是特质中的方法并不一定是抽象的,也可以 有非抽象方法(即:实现了的方法)。
  5. 除了可以在类声明时继承特质以外,还可以在构建对象时混入特质,扩展目标类的功 能
  6. 动态混入是Scala特有的方式(java没有动态混入),可在不修改类声明/定义的情况 下,扩展类的功能,非常的灵活,耦合性低 。
  7. 动态混入可以在不影响原有的继承关系的基础上,给指定的类扩展功能。
trait TraitTest01 {
  // 抽象方法
  def test001()
}

trait TraitTest02 {
  // 非抽象方法
  def test002(): Unit = {
    println("test002")
  }
}
trait TraitTest03 {
  // 非抽象方法
  def test003(): Unit = {
    println("test003")
  }
}
// 类通过extends继承特质,通过with 可以继承多个特质,
class ClassTest extends Serializable with TraitTest01 with TraitTest02 {
  override def test001(): Unit = {
    println("实现方法test001")
  }
}

object Test003{
  def main(args: Array[String]): Unit = {
    //特质的动态混入功能,可以在new一个对象的时候with N个特质,就有with的每个特质的已实现方法和已定义的属性
    val classTest = new ClassTest() with TraitTest03
    // 直接调用动态混入的方法
    classTest.test003()
  }
}

叠加特质

构建对象的同时如果混入多个特质,称之为叠加特质, 那么特质声明顺序从左到右,方法执行顺序从右到左。

  1. 特质声明顺序从左到右。
  2. Scala在执行叠加对象的方法时,会首先从后面的特质(从右向左)开始执行
  3. Scala中特质中如果调用super,并不是表示调用父特质的方法,而是向前 面(左边)继续查找特质,如果找不到,才会去父特质查找
  4. 如果想要调用具体特质的方法,可以指定:super[特质].xxx(…).其中的泛型 必须是该特质的直接超类类型
object TraitTest002 {
  def main(args: Array[String]): Unit = {
    // 叠加特质 混入多个特质
    // 创建动态混入对象,构造器按照叠加顺序从左到右被构造
    val tt = new TT() with T2 with T3 with T1
    // 方法执行按照叠加顺序从右到左被构造
    tt.say11()
  }
}

// 特质1
trait T1 {
  println("T1 ")

  def say11()
}

// 特质2
trait T2 extends T1 {
  println("T2 ")

  override def say11(): Unit = {
    println("T2 say")
  }
}

// 特质3
trait T3 extends T2 {
  println("T3 ")

  override def say11(): Unit = {
    println("T3 say")
  }
}

class TT {

}

在特质中重写抽象方法特例

调用父特质的抽象方法,那么在实际使用时,没有方法的具体实现, 无法编译通过,为了避免这种情况的发生。可重写抽象方法,这样在使用时, 就必须考虑动态混入的顺序问题。

object TraitTest003 {
  def main(args: Array[String]): Unit = {
    //val ttt = new TTT with TraitTest003Sub // 报错
    val ttt = new TTT with TraitTest003 // 无错
    println("=======")
  }
}

trait TraitTest003 extends TraitTest003Sub {
  override def insert(id: Int) = {
    println("TraitTest003.实现方法。。。。。")
  }
}

trait TraitTest003Base {
  def insert(id: Int)
}

trait TraitTest003Sub extends TraitTest003Base {
  // 当我们给某个方法增加了abstract override 后,就是明确的 告诉编译器,
  // 该方法确实是重写了父特质的抽象方法,但是重写后,
  // 该方法 仍然是一个抽象方法(因为没有完全的实现,需要其它特质继续实现)
  // 重写抽象方法时需要考虑混入特质的顺序问题和完整性问题
  abstract override def insert(id: Int) = {
    println("TraitTest003Sub.insert")
    super.insert(id)
  }
}

class TTT{

}

富接口

即该特质中既有抽象方法,又有非抽象方法, 这种特质叫富接口。

特质中的具体字段

特质中可以定义具体字段,如果初始化了就是具体字段,如果不初始化就是抽 象字段。混入该特质的类就具有了该字段,字段不是继承,而是直接加入类, 成为自己的字段。特质中未被初始化的字段在具体的子类中必须被重写。

class TraitTest004 extends TraitTest004Base{
  override var name: String = _
}

trait TraitTest004Base {
  var name: String 
}

特质构造顺序

特质也是有构造器的,构造器中的内容由“字段的初始化” 和一些其他语句构成。

扩展类的特质

特质可以继承类,以用来拓展该类的一些功能。所有混入该特质的类,会自动成为那个特质所继承的超类的子类。如果混入该特质的类,已经继承了另一个类(A类),则要求A类是特质超类的 子类,否则就会出现了多继承现象,发生错误。

trait TraitTest004Base extends Exception{
  var name: String
  println(getMessage)
}

嵌套类

在Scala中,你几乎可以在任何语法结构中内嵌任何语法结构。如在类中可以 再定义一个类,这样的类是嵌套类,其他语法结构也是一样。 嵌套类类似于Java中的内部类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云烟成雨TD

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值