Scala学习笔记-详细记录学习中遇到的知识点

                                                  官方文档https://www.scala-lang.org/api/current/
                                                  对应版本:https://www.scala-lang.org/api/x.x.x/
                                                  学习链接https://www.bilibili.com/video/BV1jt411r7hU
    
    Scala 是一门以 java 虚拟机(JVM)为运行环境并将面向对象函数式编程的最佳特性结合在一起的多范式(multi-paradigm)的静态类型编程语言
    Scala 源代码(.scala)会被编译成 Java 字节码(.class),然后运行于 JVM 之上,并可以调用现有的Java 类库,实现两种语言的无缝对接。(.scala文件可以直接通过scala命令编译运行得到结果)

package com.test

object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    print("hello world")
  }
}

    由于Scala与Java类似,本博客只记录Scala特有的语法

输入

    输入使用 StdIn 中对应的方法

import scala.io.StdIn

object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    print("请输入一个数:")
    var num: Int = StdIn.readInt()
    print("请输入一个字符串:")
    var string: String = StdIn.readLine()
    print(num + " " + string)
  }
}

print输出

  1. 字符串通过 + 号连接(类似 java)
  2. printf 用法 (类似 C 语言)字符串通过 % 传值
  3. 字符串通过 $ 引用(类似 PHP)
方式一
println("name=" + name + ",age=" + age)
方式二
printf("name=%s,age=%d\n", name, age)
方式三
println(s"name=$name,age= ${age + 1}")

变量

    Scala 要求变量声明时必须初始化

package com.test

object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    val name: String = "Tom"
    var age: Int = 18
    println("name=" + name + "age=" + age)
  }
}

    声明变量时,类型可以省略(编译器自动推导,即类型推导
    类型确定后,就不能修改,因为 Scala 是强数据类型语言

val name = "Tom"   //String
var age = 18    //Int
val salary = 1111.1    //Double
println(age.isInstanceOf[Int])

    在声明/定义一个变量时,可以使用 var 或者 val 来修饰, var 修饰的变量可变,val 修饰的变量不可改(final)。因为 val 没有线程安全问题,因此效率高,Scala 的设计者推荐使用 val。val 和 var 的区别在于 val 只有一个只读方法,var 有读写方法

 class Person{
   
    val name:String = "Tom"
    var age: Int = 18

    def getInfo(): Unit ={
   
      println("name=" + name+",age="+age)
    }
  }
public class Person {
   
  private final String name = "Tom";
  private int age = 18;

  public String name() {
    return this.name; } 
  public int age() {
    return this.age; } 
  public void age_$eq(int x$1) {
    this.age = x$1; }

  public void getInfo() {
   
    Predef..MODULE$.println(10 + "name=" + name() + ",age=" + age());
  }
}

    var 修饰的对象引用(地址)可以改变;val 修饰的则不可改变,但对象的状态(属性的值)却是可以改变的(val array[Int],地址不可以改变,但是数组里面的元素可以改变)。

数据类型

    Scala 与 Java 有着相同的数据类型,在 Scala 中数据类型都是对象(Byte、Int、Long、Double都是对象,对象中有很多方法),也就是说 scala 没有 java 中的原生类型中的原生类型。
在这里插入图片描述

  1. Scala 数据类型分为两大类 AnyVal(值类型,运行时使用Java的基本类型) 和 AnyRef(引用类型), 注意:不管是 AnyVal 还是 AnyRef 都是对象。
  2. 在 Scala 中有一个根类型 Any,它是所有类的父类
  3. Null 类型是 Scala 的特别类型,它只有一个值 null,它是 bottom calss,它是所有 AnyRef 类型的子类。null 可以赋值给任意引用类型(AnyRef),但是不能赋值给值类型(AnyVal:比如 Int, Float, Char, Boolean, Long, Double, Byte, Short)
  4. Nothing 类型也是 bottom class ,它是所有类的子类,在开发中通常可以将 Nothing 类型的值返回给任意变量或者函数, 可以使用其抛出异常
  5. Unit 类似于 Java 里的 void,用作不返回任何结果。Unit 只有一个实例,()
  6. 在 Scala 中仍然遵守,低精度的值向高精度的值自动转换,即隐式转换(implicit conversion)
  7. 多种类型的数据混合运算时,系统首先自动将所有数据转换成精度最大的那种数据类型,然后再进行计算。byte,short,char 在计算时首先转换为 int 类型
var num:Short = 5
num = num - 2   //error, Int -> Short
num = (num - 2).toByte   //correct
  1. 强制类型转换: 2.7.toInt(一切皆对象)强转只针对于最近的操作数有效,往往会使用小括号提升优先级
val num1: Int = 10 * 3.5.toInt + 6 * 1.5.toInt // 36
val num2: Int = (10 * 3.5 + 6 * 1.5).toInt // 44
  1. 整数默认是Int,Long要在后面加 l 或 L。小数默认是Double,Float要在后面加 f 或 F
  2. 字符类型 Char 是2个字节,Char 类型可以保存 Int 的常量值,但不能保存 Int 的变量值,需要强转

标识符与关键字

    标识符规则:

  1. 首字符为字母,后续字符任意字母和数字,美元符号,可后接下划线 _
  2. 数字不可以开头
  3. 首字符为操作符(比如+ - * / ),后续字符也至少一个需要跟操作符。操作符(比如+ - * / )不能在标识符中间和最后。
var ++ = "string"   //编译后为 $plus$plus
var +-*/ = "string"
var `true` = "string"
  1. 用反引号 `` 包括的任意字符串,即使是关键字也可以

    Scala 有 39 个关键字:
    packageimportclassobjecttraitextendswithtypeforSomeprivateprotectedabstractsealedfinalimplicitlazyoverridetrycatchfinallythrowifelsematchcasedowhileforreturnyielddefvalvarthissupernewtruefalsenull

运算符优先级

  1. () []
  2. 单目运算符(!,~)(从右到左)
  3. 算术运算符
  4. 移位运算符
  5. 比较运算符
  6. 位运算符
  7. 逻辑运算符
  8. 赋值运算符(从右到左)
  9. 逗号

if else

    Scala 的 if else 与Java 的类似
    Scala 中任意表达式都是有返回值的,也就意味着 if else 表达式其实是有返回结果的,具体返回结果的值取决于满足条件的代码体的最后一行内容

val num: Int = 500
val result = {
   
  if (num>50) 100 else "fifty"
}
print(result)

    Scala不支持三元运算符,用 if else 代替

for循环

方式一、左闭右闭
	for(i <- 1 to 5)

方式二、左闭右开
	for(i <- 1 until 5)

方式三、使用Range,可设置步长,左闭右开
	for (i <- Range(1,5,2))

方式四、左闭右闭,可设置步长
	for (i <- 1.to(5, 2))

方式五、使用indices,底层与until相同,左闭右开
	for (i <- array.indices)

方式六、forEach
	list.foreach((x: Int) => println(x))
	list.foreach(x => println(x))
	list.foreach(println(_))
	list.foreach(println)

方式七:类似Java的forEach
	for (arg <- args)
	arg是一个val,不能修改

    循环守卫,即循环保护式(也称条件判断式)。保护式为 true 则进入循环体内部,为 false则跳过,类似于 continue。注意条件不需要括号。

for (i <- 1 to 100
	if (i % 3) == 0
	if (i % 10) == 0) {
   
  print(i + " ") // 30 60 90
}

    可以引入变量

for (i <- 1 to 8; j = 9 - i) {
   
  println(i + " " + j)
}

    嵌套循环:多个 <- 之间要用分号隔开(因为圆括号和方括号里面不会分号推断

for (i <- 1 to 9; j <- 1 to 9) {
   
  println(i + " " + j)
}

    下面这个for循环有两个嵌套循环,外部循环遍历文件目录,获得文件名以 .txt 结尾的文件列表,内部循环获取每个文件里长度大于length的行。

  object ScalaTest {
   

    private def readFile(file: java.io.File) =
      scala.io.Source.fromFile(file).getLines().toList

    def grep(path: String, length: Int) = {
   
      val list = new java.io.File(path)
      for (file <- list.listFiles()
           if file.getName.endsWith(".txt"); <---注意分号
           line <- readFile(file)
           if line.length > length)
        println(file.getName + " " + line)
    }

    def main(args: Array[String]): Unit = {
   
      grep("E:\\file", 10)
    }
  }

yield生成器

    感觉类似python的生成器。使用 yield 关键字,将遍历过程中处理的结果返回到一个新 Vector 集合中(scala.collection.immutable.IndexedSeq)

// 获取2,4,6,8...20列表
val res = for(i <- 1 to 10) yield i * 2   // i可以是代码块

//生成满足1<=j<i<5的所有对偶(i, j)
for(i <- List.range(1, 5); j <- List.range(1, i)) yield (i, j)
res0: List[(Int, Int)] = List((2,1), (3,1), (3,2), (4,1), (4,2), (4,3))

List.range(1, 5).flatMap(i => List.range(1, i).map(j => (i, j)))
res1: List[(Int, Int)] = List((2,1), (3,1), (3,2), (4,1), (4,2), (4,3))

    返回一个斐波拉契数列

def getFibonacci(n: Int): IndexedSeq[Int] = {
   
    var one: Int = 1
    var two: Int = 1
    val Fibonacci = for (i <- 1 to n) yield {
   
      if (i == 1) one
      else if (i == 2) two
      else {
   
        two = one + two
        one = two - one
        two
      }
    }
    Fibonacci
}

    for 推导式有一个不成文的约定:当 for 推导式仅包含单一表达式时使用圆括号,当其包含多个表达式时使用大括号。当使用 {} 来换行写表达式时,分号就不用写了

for(i <- 1 to 10;j <- 1 to 10){
   }
可以写成
for{
   i <- 1 to 10
    j <- 1 to 10}{
   }

    while 与 do while 和Java的一样。While 语句本身没有值,即整个 While 语句的结果是 Unit 类型的()

break与continue

    Scala 内置控制结构特地去掉了 break 和 continue,是为了更好的适应函数化编程,推荐使用函数式的风格解决 break 和 contine 的功能,而不是一个关键字。
    breakableutil.control.Breaks._ 下,breakable 是一个高阶函数:可以接收函数的函数就是高阶函数。breakable源码如下:

  1. op: => Unit 表示接收的参数是一个没有输入,也没有返回值的函数(代码块)
  2. breakable 对 break() 抛出的异常做了处理,代码就继续执行
  3. 当传入的是代码块,要将() 改成{}
def breakable(op: => Unit) {
   
    try {
   
      op
    } catch {
   
      case ex: BreakControl =>
        if (ex ne breakException) throw ex
    }
}
import util.control.Breaks.{
   break, breakable}

var num: Int = 1
breakable(
  while (num < 10) {
   
    if (num == 5)
      break()
    num += 1
  }
)
print(num)    //5

    可以使用 if else 或是 循环守卫实现 continue 的效果

for (i <- 1 to 10 if (i != 2 && i != 3)) {
   
	println("i=" + i)
}
-----------------------
for (i <- 1 to 10) {
   
	if (i != 2 && i != 3)
		println("i=" + i)
}

函数

在这里插入图片描述

    在Scala中,函数的创建不用依赖于类或者对象

def 函数名 ([参数名: 参数类型], ...)[[: 返回值类型] =] {
	语句
	return 返回值
}

函数可以有返回值,也可以没有
返回值形式 1: : 返回值类型 =
返回值形式 2: = 表示返回值类型不确定,使用类型推导完成
返回值形式 3: 表示没有返回值,return 不生效


    在声明形参时,可以直接赋初始值(默认值)。调用函数时,如果没有指定实参,则会使用默认值。如果指定了实参,则实参会覆盖默认值。
    如果函数存在多个参数,每一个参数都可以设定默认值,传递参数默认按照声明顺序[从左到右],也可以使用带名参数

def mysqlCon(add:String = "localhost",port : Int = 3306,
		user: String = "root", pwd : String = "root"): Unit = {
   
	println("add=" + add)
	println("port=" + port)
	println("user=" + user)
	println("pwd=" + pwd)
}

从左到右覆盖,顺序不能改变
mysqlCon("127.0.0.1", 7777)
使用带名参数
mysqlCon(user = "tom", pwd = "123")

    Scala 函数支持可变参数,args 是集合,通过 for 循环可以访问到各个值。args的类型是Array。

支持 0 到多个参数
def sum(args :Int*) : Int = {
   }
支持 1 到多个参数
def sum(n1: Int, args: Int*) : Int = {
   }
遍历可变形参
for (item <- args) {
   }

    如果想传入数组,要在参数后面加上 : _*

  def echo(arg: String, args: String*) = args.foreach(println)

  def main(args: Array[String]): Unit = {
   
    val array = Array("1", "2", "3")
    echo("", array: _*)
  }

函数注意事项:

  1. 函数的形参列表可以是多个, 如果函数没有形参,调用时可以不带()
  2. Scala 中的函数可以根据函数体最后一行代码自行推断函数返回值类型,所以return 关键字可以省略
def getSum(n1: Int, n2: Int): Int = {
   
	n1 + n2
}
  1. 因为 Scala 可以自行推断,所以在省略 return 关键字的场合,返回值类型也可以省略
def getSum(n1: Int, n2: Int) = {
   
	n1 + n2
}
  1. 如果代码只有一行,花括号也可以省略
def getSum(n1: Int, n2: Int) = n1 + n2
  1. 如果函数明确使用 return 关键字,那么函数返回就不能使用自行推断了。这时要明确写成 : 返回类型 =如果返回值处什么都不写,即使有 return 返回值为()
  2. 如果函数明确声明无返回值(声明 Unit),那么函数体中即使使用 return 关键字也不会有返回值
  3. 如果明确函数无返回值或不确定返回值类型,那么返回值类型可以省略(或声明为 Any)
  4. Scala 语法中任何的语法结构都可以嵌套其他语法结构(灵活),即:函数中可以再声明/定义函数,类中可以再声明类 ,方法中可以再声明/定义方法
  5. 递归函数未执行之前是无法推断出来结果类型,使用递归时必须有明确的返回值类型
  6. 函数的任何参数都是 val,而不是 var
def f(a: Int) = a += 1 // 不能编译

局部函数

    局部函数定义在函数内部,只对该函数可见,不能从外部访问。局部函数不需要用 private 修饰(这个修饰符只能在成员上使用)

  def processFile(filename: String, width: Int) = {
   
    def processLine(line: String) = {
   
      if (line.length > width)
        println(filename + ": " + line)
    }

    val source = scala.io.Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(line)
  }

    局部函数可以访问包含它们的函数的参数,像 processLine 可以直接访问 processFile 的参数 filename 和 width。

惰性lazy

    惰性计算(尽可能延迟表达式求值)是许多函数式编程语言的特性。惰性集合在需要时提供其元素,无需预先计算它们,这带来了一些好处。首先,您可以将耗时的计算推迟到绝对需要的时候。其次,您可以创造无限个集合,只要它们继续收到请求,就会继续提供元素。函数的惰性使用让您能够得到更高效的代码。Java 并没有为惰性提供原生支持,Scala 提供了。
    当函数返回值被声明为 lazy 时,函数的执行将被推迟,直到我们首次对此取值,该函数才会执行。这种函数我们称之为惰性函数,在 Java 的某些框架代码中称之为懒加载(延迟加载)。

object LazyTest {
   
	def main(args: Array[String]): Unit = {
   
		lazy val res = sum(10, 20)
		println("res=" + res) //在使用res时才执行sum()
	}
	
	def sum(n1: Int, n2: Int): Int = {
   
		return n1 + n2
	}
}
  1. lazy 不能修饰 var 类型的变量
  2. 加了 lazy 会导致函数的执行被推迟,如果声明一个变量时声明了 lazy,那么变量值的分配也会推迟

异常

    Scala 提供 try 和 catch 块来处理异常。try 块用于包含可能出错的代码。catch 块用于处理 try 块中发生的异常。可以根据需要在程序中有任意数量的 try…catch 块。
    在 Scala 中只有一个 catch,在 catch 中有多个 case, 每个 case 可以匹配一种异常,=> 关键符号,表示后面是对该异常的处理代码块。

def main(args: Array[String]): Unit = {
   
    try {
   
      var num = 10 / 0
    } catch {
   
      case ex: ArithmeticException => println("捕获除数为0的算术异常")
      case ex: Exception => println("捕获异常")
    } finally {
   
      println("处理异常成功")
    }
}

运行结果:
捕获除数为0的算术异常
处理异常成功


    跟Scala的大多数控制结构一样,try-catch-finally最终会返回一个值。例如下面这个例子,如果没有抛出异常,整个表达式的结果就是try子句的结果;如果URL的格式有问题并被捕获异常,则返回一个默认的URL;如果没有抛出异常也没有捕获异常,整个表达式就没有结果。

 import java.net.{
   MalformedURLException, URL}

 def getURL(path: String) =
   try {
   
     new URL(path)
   } catch {
   
     case ex: MalformedURLException =>
       new URL("http://www.scala-lang.org")
   }
  1. Scala 的异常的工作机制和 Java 一样,但是 Scala 没有“checked(编译期)”异常,即 Scala 没有编译异常这个概念,异常都是在运行的时候捕获处理。
  2. throw 关键字,抛出一个异常对象。所有异常都是 Throwable 的子类型。throw 表达式是有返回值的,是 Nothing,因为 Nothing 是所有类型的子类型,所以 throw 表达式可以用在需要类型的地方
def test(): Nothing = {
   
	throw new ArithmeticException("算术异常")
}
  1. 如果有异常发生,catch 子句是按次序捕捉的。因此,在 catch子句中,越具体的异常越要靠前,越普遍的异常越靠后,如果把越普遍的异常写在前,把具体的异常写在后,在 scala 中也不会报错,但这样是非常不好的编程风格。
  2. finally 子句用于执行不管是正常处理还是有异常发生时都需要执行的步骤,一般用于对象的清理工作或释放资源,这点和 Java 一样。
  3. 在 Scala 中,可以使用 throws 注释来声明异常
def main(args: Array[String]): Unit = {
   
	func()
}

//等同于 NumberFormatException.class
@throws(classOf[NumberFormatException])
def func() = {
   
	"abc".toInt
}

类与对象

    先写一个Person类

object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    val person = new Person
    person.name = "John"
    person.age = 18
    person.getInfo()
  }
}

class Person{
   
  var name:String = _   //默认值,null
  var age: Int = _   //默认值,0

  def getInfo(): Unit ={
   
    println("name=" + name+",age="+age)
  }
}

    反编译如下:

public class Person {
   
  private String name;
  private int age;

  public String name() {
    return this.name; } 
  public void name_$eq(String x$1) {
    this.name = x$1; } 
  public int age() {
    return this.age; } 
  public void age_$eq(int x$1) {
    this.age = x$1; }

  public void getInfo() {
   
    Predef..MODULE$.println(10 + "name=" + name() + ",age=" + age());
  }
}
  1. 类中声明了name和age,底层默认是 private,同时生成对应的 xxx()(类似getter)和 xxx_$eq()(类似setter)的方法
  2. 类中定义的方法底层默认为 public
  3. 一个 Scala 源文件可以包含多个类.,而且默认都是 public
  4. 定义属性时,必须显式的初始化,可以使用符号 _ (下划线),让系统分配默认值
  5. 如果赋值为 null,则一定要加类型。因为不加类型, 那么该属性的类型就是 Null 类型.
  6. 变量声明语法:[访问修饰符] var 属性名称 [:类型] = 属性值
  7. 创建对象语法:val | var 对象名 [:类型] = new 类型()(创建对象的类型可以自动推断,所以类型可以省略,多态时必须加类型)
  8. 如果我们不希望改变对象的引用(即:内存地址),应该声明为 val 性质的,否则声明为 var

    构建一个有理数类,实现有理数加法和乘法。

  class Rational(a: Int, b: Int) {
   
    require(b != 0)

    private val g = gcd(a.abs, b.abs)
    val numer = a / g
    val denom = b / g

    def this(a: Int) = this(a, 1)

    def +(that: Rational): Rational = new Rational(numer * that.denom + that.numer * denom, denom * that.denom)

    def *(that: Rational): Rational = new Rational((numer * that.numer, denom * that.denom))

    override def toString = a + "/" + b

    private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

  }

构造器

    Scala 类的构造器包括:主构造器辅助构造器。主构造器只有一个,辅构造器可以有多个,且辅构造器名字为 this

object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    val person = new Person("John", 18)
    person.getInfo()
  }
}

class Person() {
   
  var name: String = _
  var age: Int = _
  age += 10

  def this(name: String) {
   
    this()
    this.name = name
  }

  def this(age: Int) {
   
    this("Tom")  //间接调用主构造器,this(name: String)第一行调用了主构造器
    this.age = age
  }

  def this(name: String, age: Int) {
   
    this
    this.name = name
    this.age = age
  }

  def getInfo(): Unit = {
   
    println("name=" + name + ",age=" + age)
  }

  println(age)
}

    反编译如下:

public class Person{
   
  private String name;
  private int age;

  public String name() {
    return this.name; } 
  public void name_$eq(String x$1) {
    this.name = x$1; } 
  public int age() {
    return this.age; } 
  public void age_$eq(int x$1) {
    this.age = x$1; }

  public void getInfo() {
   
    Predef..MODULE$.println(10 + "name=" + name() + ",age=" + age());
  }

  public Person() {
   
    age_$eq(age() + 10);
    Predef..MODULE$.println(BoxesRunTime.boxToInteger(age()));
  }

  public Person(String name) {
   
    this();
    name_$eq(name);
  }

  public Person(int age) {
   
    this("Tom");
    age_$eq(age);
  }

  public Person(String name, int age) {
   
    this();
    name_$eq(name);
    age_$eq(age);
  }
}
  1. 主构造器内的语句,如 age += 10,println(age) 最终会放到主构造器内,并最终执行主构造器中所有语句,
  2. 辅构造器第一行必须显式调用主构造器(直接或间接),原因是保持与父类的关系,主构造器会有super。即辅构造器最终都一定要调用主构造器,执行主构造器的逻辑。主构造器是类的单一入口。
  3. 构造器作用是完成对新对象的初始化,构造器没有返回值
  4. 主构造器的声明直接放置于类名之后,如 class Person(name: String) {}
  5. 如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略
  6. 辅助构造器名称为 this,多个辅助构造器通过不同参数列表进行区分
  7. 辅助构造器有先后顺序,先定义的可以被后面的辅助构造器调用,先定义的不能调用后定义的辅助构造器
  8. 如果想让主构造器变成私有的,可以在()之前加上 private,这样用户只能通过辅助构造器来构造对象了
class A private {
   }
  1. 对象创建的流程:
    1. 加载类的信息(属性信息,方法信息)
    2. 在内存中(堆)开辟空间
    3. 使用父类的构造器(主和辅助)进行初始化
    4. 使用主构造器对属性进行初始化
    5. 使用辅助构造器对属性进行初始化
    6. 将开辟的对象的地址赋给对象引用

构造器参数

  1. Scala 类的主构造器的形参未用任何修饰符修饰,那么这个参数是局部变量
  2. 如果参数使用 val 关键字声明,那么 Scala 会将参数作为类的私有的只读属性使用
  3. 如果参数使用 var 关键字声明,那么那么 Scala 会将参数作为类的私有的,但是可读写属性使用,并会提供属性对应的 xxx()[类似 getter]/xxx_$eq()[类似 setter]方法
//形参没有任何修饰符,newName是一个局部变量
class Person(newName: String) {
   
  var name = newName
}
//使用val,newName是一个私有的只读属性
class Person1(val newName: String) {
   
  var name = newName
}
//使用var,newName是一个私有的可读写属性
class Person2(var newName: String) {
   
  var name = newName
}
object ScalaTest {
   
  def main(args: Array[String]): Unit = {
   
    val person = new Person("John")
    println(person.newName)  --->  Error
    val person1 = new Person1("John")
    println(person1.newName)
    person1.newName = "Tom"  --->  Error
    val person2 = new Person2("John")
    println(person2.newName)
    person2.newName = "Tom"
    println(person2.newName)
  }
}

    Person的反编译结果

public class Person {
   
  private String name;

  public String name() {
    return this.name; } 
  public void name_$eq(String x$1) {
    this.name = x$1; } 
  public Person(String newName) {
    this.name = newName; }
}

    Person1的反编译结果,newName是一个final修饰的常量

public class Person1 {
   
  private final String newName;
  private String name
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值