Scala系列-4、scala中特质、柯里化、闭包等

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

传送门:大数据系列文章目录
在这里插入图片描述

scala中特质

scala中没有Java中的接口(interface),替代的概念是——特质

语法

trait 名称 {
    // 抽象字段
    // 抽象方法
}

继承特质:
classextends 特质1 with 特质2  {
    // 字段实现
    // 方法实现
}

说明:
- 特质是scala中代码复用的基础单元
- 它可以将方法和字段定义封装起来,然后添加到类中
- 与类继承不一样的是,类继承要求每个类都只能继承一个超类,而一个类可以添加任意数量的特质。
- 特质的定义和抽象类的定义很像,但它是使用trait关键字
- 使用extends来继承trait(scala不论是类还是特质,都是使用extends关键字)
- 如果要继承多个trait,则使用with关键字

特质作为接口使用

创建特质

package com.lee

trait Person {
  //抽象成员变量
  val name:String
  val age: Int
  var address:String
  var birthday:String


  // 抽象的方法
  def work()

  def eat()
}

继承特质

package com.lee

class Teacher  extends Person {
  override val name: String = "小胖"
  override val age: Int = 45
  override var address: String = "北海"
  override var birthday: String = "1980-10-05"

  override def work(): Unit = {
    println("开始工作....")
  }

  override def eat(): Unit = {
    println("小胖开始吃饭....")
  }
}

相关的操作

package com.lee

object MainTest01 {

  def main(args: Array[String]): Unit = {

    val teacher = new Teacher

    teacher.work()
    println(teacher.age)

  }

}

特质中放置非抽象的成员

创建特质

package com.lee

trait Person {
  //抽象成员变量
  val name:String
  val age: Int
  var address:String
  var birthday:String

  // 定义一个非抽象的成员变量
  val sex:String = "男"



  // 抽象的方法
  def work()

  def eat()

  // 定义非抽象的方法

  def run(): Unit ={
    println("人人都需要跑步 健身....")
  }

}

特质的模板操作

示例: 模拟一个日志输出到不同地方的操作

创建特质: 指定输出的内容 , 具体输出位置 并不清楚, 采用抽象

package com.lee

trait Logger {

  def msg():String

  def info() = println("输出info日志:"+msg())
  def waring() = println("输出警告日志:"+msg())
  def error() = println("输出错误日志:"+msg())
}

继承特质: 指定输出的地方

package com.lee

class ConsoleLogger extends Logger {
  override def msg() =  "控制台"

}

使用操作

package com.lee

object MainTest01 {

  def main(args: Array[String]): Unit = {

    val consoleLogger = new ConsoleLogger

    consoleLogger.info()

  }

}

特质的混入对象操作

作用: 可以增强对象的功能

格式:

newwith 特质

操作

创建特质

package com.lee

trait Person {

  val name:String = "李哥"

  def study() = {
    println(name + ":爱学习....")
  }
}

使用特质混入

package com.lee

object MainTest01 {


  def main(args: Array[String]): Unit = {
    // 对象混入特质:  提升当前这个对象的功能
    val tearcher = new Tearcher with Person

    tearcher.study()

  }
}

特质的执行链条

说明: 当一个类继承了多个特质, 而多个特质又有相同的方法, 并每个方法都调用父类的方法, 此时执行顺序为从右往左依次执行

演示操作

在这里插入图片描述

代码演示

trait HandlerTrait {
    def handle(data:String) = println("处理数据...")
}

trait DataValidHanlderTrait extends HandlerTrait {
    override def handle(data:String): Unit = {
        println("验证数据...")
        super.handle(data)
    }
}

trait SignatureValidHandlerTrait extends HandlerTrait {
    override def handle(data: String): Unit = {
        println("校验签名...")
        super.handle(data)
    }
}

class PayService extends DataValidHanlderTrait with SignatureValidHandlerTrait {
    override def handle(data: String): Unit = {
        println("准备支付...")
        super.handle(data)
    }
}

def main(args: Array[String]): Unit = {
    val service = new PayService
    service.handle("支付参数")
}

// 程序运行输出如下:
// 准备支付...
// 检查签名...
// 验证数据...
// 处理数据...

特质的构造执行流程

说明: 当类继承了多个特质后, 而多个特质中都有构造方法, 此时执行的顺序为:

  • trait也有构造代码,但和类不一样,特质不能有构造器参数
  • 每个特质只有一个无参数的构造器。
  • 一个类继承另一个类、以及多个trait,当创建该类的实例时,它的构造顺序如下
  1. 执行父类的构造器
  2. 从左到右依次执行trait的构造器
  3. 如果trait有父trait,先构造父trait,如果多个trait有同样的父trait,则只初始化一次
  4. 执行子类构造器

相关的操作:

创建多个特质

package com.lee

trait T1 {

  println("这是T1的特质 构造方法.....")
}


package com.lee

trait T2 extends T1{

  println("这是T2的特质 构造方法.....")
}


package com.lee

trait T3 extends T1{

  println("这是T3的特质 构造方法.....")
}


package com.lee

trait T4  extends T3{

  println("这是T4的特质 构造方法.....")
}

创建一个普通类 继承特质

package com.lee

class C5  extends T4 with T3 with  T2 with  T1{
    println("这是 C5的构造方法")
}

调用使用

package com.lee

object MainTest01 {
  def main(args: Array[String]): Unit = {

    new C5

  }
}

特质继承class

trait也可以继承class的。特质会将class中的成员都继承下来。

相关的操作:

创建一个类

package com.lee

class C1 {
    val name:String = "张三"

     def eat() = {

       println(name+":吃东西")
     }
}

创建一个特质

package com.lee

trait T1 extends C1{

}

创建测试:

package com.lee

object MainTest01 {

  def main(args: Array[String]): Unit = {

    val c = new C1
    c.eat()

  }

}

样例类

什么是样例类? 样例类主要的作用是为了进行封装数据的操作, 类似于java中实体类(POJO JavaBean)

如何实现一个样例类:

case class 样例类名([var/val] 成员变量名1:类型1, 成员变量名2:类型2, 成员变量名3:类型3)

使用说明:
	1) 在定义样例类的时候,成员变量是不需要进行初始化的操作
    2) 成员变量可使用val 或者 var修饰, 默认采用 val, 所以在定义的时候, 是可以省略的
    3) 如果使用val来定义变量, 此变量在创建样例类对象后, 其变量只能被赋值一次

需求

  • 定义一个Person样例类,包含姓名和年龄成员变量
  • 创建样例类的对象实例(“张三”、20),并打印它
object Demo01 {

  /*
  需求

    - 定义一个Person样例类,包含姓名和年龄成员变量
    - 创建样例类的对象实例("张三"、20),并打印它


    说明:
      1) 在样例类中, 默认重写toString方法
      2) 为成员变量自动实现 get 和 set  方法
      3) 在样例类自动实现 apply方法, 构建样例类对象的时候, 可以不写new的
   */


  case class Person(name:String , age:Int)

  def main(args: Array[String]): Unit = {


    val person:Person = new Person("张三",20)

    println(person) //  Person(张三,20)

    // 创建person对象:
    val person1:Person = Person("张三",20)
    println(person1)
  }

}

当我们定义一个样例类,编译器自动帮助我们实现了以下几个有用的方法:

  • apply方法
  • toString方法
  • equals方法
  • hashCode方法
  • copy方法

样例对象

是单例的, 只有一个示例对象, 一般将样例对象, 看做是java中一种用于定义常量的操作(枚举)

格式:

case object 样例对象名

需求说明

  • 定义一个性别Sex枚举,它只有两个实例(男性——Male、女性——Female)
  • 创建一个Person类,它有两个成员(姓名、性别)
  • 创建两个Person对象(“张三”、男性)、(“李四”、“女”)
package com.lee

object Demo02 {

  /*

    需求说明

      - 定义一个性别Sex枚举,它只有两个实例(男性——Male、女性——Female)
      - 创建一个Person类,它有两个成员(姓名、性别)
      - 创建两个Person对象("张三"、男性)、("李四"、"女")


   */
  trait Sex
  
  case  object  Male extends Sex
  case  object  Female extends  Sex

  case  class Person(name:String ,sex:Sex)


  def main(args: Array[String]): Unit = {

    val person1 = Person("张三",Male)

    val person2 = Person("李四",Female)

    println(person1)
  }
}


scala中模式匹配操作

scala中模式匹配的操作, 类似于java中switch语句, 但是要比java的switch语句要强大的多, 在java的switch语句仅支持一些基本数据类型

格式:

变量 match {
    case "常量1" => 表达式1
    case "常量2" => 表达式2
    case "常量3" => 表达式3
    case _ => 表达式4		// 默认配
}

相关的操作

package com.lee


class MatchDemo01 {}

import scala.io.StdIn
import scala.util.Random

object CaseDemo {
  def main(args: Array[String]): Unit = {
    //1.匹配字符串/数字....
    println("---1.匹配字符串---")
    val arr1 = Array("hadoop","spark")
    var name = arr1(Random.nextInt(arr1.length)) //随机生成一个[0 ~ arr1.length)范围的数字
    //name = "xxx"
    println(name)

    /*if( name == "hadoop"){
        println("hadoop是大数据中离线存储和计算的框架....")
    }else if (name == "spark") {
       println( "spark是大数据中用于计算的框架, 既可以进行实时计算, 也可以离线计算操作")
    }else {

      println("既不是hadoop 也不是 spark....")
    }*/
    name match {
      case  "hadoop" => println("hadoop是大数据中离线存储和计算的框架....")
      case "spark" => println( "spark是大数据中用于计算的框架, 既可以进行实时计算, 也可以离线计算操作")
      case _ => println("既不是hadoop 也不是 spark....")
    }



    //注意:上面功能和Java中的功能一样!
    //下面的才是Scala中的模式匹配特有的功能

    //2.匹配类型
    println("---2.匹配类型---")
    //这样做的意义在于不用使用isInstanceOf和asInstanceOf方法
    val arr2 = Array("hello", 1, CaseDemo)
    val value = arr2(Random.nextInt(arr2.length))
    println(value)

    value match {

      case a:String => println(s"${a} : 是一个 字符串类型的数据")
      case a:Int => println(s"${a} : 是一个 int类型的数据")
      case a:CaseDemo.type =>  println(s"${a} : 是一个 CaseDemo 类型的数据")

    }
    // 注意: 在判断的时候, 如果后面的表达式没有使用的前面的变量, 那么变量可以使用 _ 替代
    value match {

      case _:String => println("是一个 字符串类型的数据")
      case _:Int => println("是一个 int类型的数据")
      case _:CaseDemo.type =>  println("是一个 CaseDemo 类型的数据")

    }



    //3.匹配集合
    println("---3.匹配集合---")
    val arr3 = Array(1,2,3)
    arr3 match {
      case Array(1, x, y) => println(x + " " + y) //3 5  //自带break
      case Array(0) => println("only 0")
      case Array(1, _*) => println("1 ...")
      case _ => println("没有匹配上")
    }

    val li = List(0,1)
    li match {
      case 0 :: Nil => println("only 0") //List(0)
      case x :: y :: Nil => println(s"x: $x y: $y") //x: 3 y: -1  //List(x,y)
      //case 0 :: xx => println("0 "+ xx)
      case 0 :: tail => println("0 "+ tail)
      case _ => println("没有匹配上")
    }

    val tup = (2, 3, 5)
    tup match {
      case (1, x, y) => println(s"1, $x , $y") //1, 3 , 7
      case (_, z, 5) => println(z)
      case _ => println("没有匹配上")
    }

	// 4. 模式匹配_守卫
	val a = StdIn.readInt()  // 键盘输入操作

	a match {
    case _ if a >= 0 && a <= 3 => println("[0-3]")
		case _ if a >= 4 && a <= 8 => println("[4-8]")
		case _ => println("未匹配")
	}
	
    //5.变量声明中隐藏的模式匹配 (了解)
    println("---4.变量声明中隐藏的模式匹配---")
    val (x, y) = (1, 2)
    println(x) //1
    println(y) //2
    val (q, r) = BigInt(10) /% 3
    println(q) //3
    println(r) //1
    val arr4 = Array(1, 7, 2, 9)
    val Array(first, second, _*) = arr4
    println(first, second) //(1,7)

    //6.for循环中隐藏的模式匹配 (了解)
    println("---5.for循环中隐藏的模式匹配---")
    val map = Map("k1" -> "v1", "k2" -> "v2", "k3" -> "v3")
    //val map = Map(("k1","v1"),("k2","v2"),("k3","v3"))
    for ((k, v) <- map) {
      println(k + ":" + v)
    }

    //6.函数式操作中的模式匹配
    println("---6.函数式操作中的模式匹配---")
    map.foreach {
      case (k, v) => println(k, v)
    }

    //7.匹配样例类和样例对象
    println("---7.匹配样例类---")
    val arr = Array(StartTask("task1"), StopTask)
    val obj =arr(1)
    println(obj)
    obj match {
      case StartTask(name) => println("start")
      case StopTask => println("stop")
    }
  }
}

case class StartTask(name: String) //case class可以带参数,可以封装数据,也可以用作模式匹配
case object StopTask//case object一般都作为模式匹配来使用,不能够带参数

Option类型

作用: 为了避免空指针的错误, 保证调用方法的时候, 返回值不会是Null

定义

scala中,Option类型来表示可选值。这种类型的数据有两种形式:

  • Some(x):表示实际的值

  • None:表示没有值

相关的方法:

isEmpty: 判断是否为空, 如果有数据 返回 false 如果没有数据(None) 返回true
get :  用于从Option类型中获取数据, 如果类型为Nont 直接抛出异常, 表达出那个部分出现问题
getOrElse: 当Option类型为None的时候, 返回设置的默认值

示例说明

  • 定义一个两个数相除的方法,使用Option类型来封装结果
  • 然后使用模式匹配来打印结果
    不是除零,打印结果
    除零打印异常错误
  /**
    * 定义除法操作
    * @param a 参数1
    * @param b 参数2
    * @return Option包装Double类型
    */
  def dvi(a:Double, b:Double):Option[Double] = {
    if(b != 0) {
      Some(a / b)
    }
    else {
      None
    }
  }

  def main(args: Array[String]): Unit = {
    val result1 = dvi(1.0, 5)

    result1 match {
      case Some(x) => println(x)
      case None => println("除零异常")
    }
  }

示例二

示例说明

  • 重写上述案例,使用getOrElse方法,当除零时,或者默认值为0

参考代码

def dvi(a:Double, b:Double) = {
    if(b != 0) {
        Some(a / b)
    }
    else {
        None
    }
}

def main(args: Array[String]): Unit = {
    val result = dvi(1, 0).getOrElse(0)

    println(result)
}

scala中偏函数

偏函数可以提供了简洁的语法,可以简化函数的定义。配合集合的函数式编程,可以让代码更加优雅。

偏函数本质上就是一种函数

格式:

- 偏函数被包在花括号内没有match的一组case语句是一个偏函数
- 偏函数是PartialFunction[A, B]的一个实例
  - A代表输入参数类型
  - B代表返回结果类型

{
    case x => 表达式
    ...
}

示例说明

  • 定义一个列表,包含1-10的数字
  • 请将1-3的数字都转换为[1-3]
  • 请将4-8的数字都转换为[4-8]
  • 将其他的数字转换为(8-*]
package com.lee

import scala.io.StdIn

object Demo01 {
  /*
      - 定义一个列表,包含1-10的数字
      - 请将1-3的数字都转换为[1-3]
      - 请将4-8的数字都转换为[4-8]
      - 将其他的数字转换为(8-*]

   */
  def main(args: Array[String]): Unit = {

    val list1: List[Int] = (1 to 10).toList
    // 原生写法: 基于普通函数的操作
    val  list2: List[String] = list1.map(i => {

      if (i <= 3) {
        "[1-3]"
      } else if (i >= 4 && i <= 8) {
        "[4-8]"
      } else {
        "(8-*]"
      }
    })

    println(list2)

    // 偏函数实现:
    val list3: List[String] = list1.map {
      case i if i <= 3 => "[1-3]"
      case i if i >= 4 && i <= 8 => "[4-8]"
      case _ => "(8-*]"
    }

    println(list3)
    
  }
}

scala中正则表达式操作

使用scala如何通过正则判断字符串是否符合规则

定义

Regex类

  • scala中提供了Regex类来定义正则表达式

  • 要构造一个RegEx对象,直接使用String类的r方法即可

  • 建议使用三个双引号来表示正则表达式,不然就得对正则中的反斜杠来进行转义

val regEx = """正则表达式""".r

findAllMatchIn方法

  • 使用findAllMatchIn方法可以获取到所有正则匹配到的字符串

示例一

示例说明

  • 定义一个正则表达式,来匹配邮箱是否合法

    val r = “”“.+@.+…+”“”.r

  • 合法邮箱测试:qq12344@163.com

  • 不合法邮箱测试:qq12344@.com

package com.lee

import scala.util.matching.Regex

object Demo01 {


  def main(args: Array[String]): Unit = {

    //1.定义用于判定邮箱是否正确的正则表达式
    val regEx: Regex =  """.+@.+..+""".r

    //2. 定义一个邮箱
    val email = "45781212@qq.com"

    //3. 判定是否符合规则

    if (regEx.findAllMatchIn(email).size > 0){
        // 说明匹配到了数据, 说明传递的字符串是符合规则的
      println(email+": 符合规则")
    }else println("不符合规则")
  }
}

示例二

示例说明

找出以下列表中的所有不合法的邮箱

"38123845@qq.com", "a1da88123f@gmail.com", "zhansan@163.com", "123afadff.com"
package com.lee

import scala.util.matching.Regex

object Demo02 {

  def main(args: Array[String]): Unit = {

    //1. 定义一个正则表达式: 用于判断邮箱
    val regEx: Regex =  """.+@.+..+""".r

      //2. 定义一个集合, 内部放置多个邮箱地址
    val list = List("38123845@qq.com", "a1da88123f@gmail.com", "zhansan@163.com", "123afadff.com")

    //3. 判断过滤操作

    /*for( i <- list) {
      if(regEx.findAllMatchIn(i).size <=0 ){
          println(i)
      }
    }*/

    val list1 = list.filter(i => regEx.findAllMatchIn(i).size <= 0 )

    println(list1)
  }
}

scala中异常处理

在scala中如果出现了异常如何处理, 处理的方式有两种: 捕获异常 抛出异常

如何捕获异常 进行处理
格式:

try {
    // 代码
}
catch {
    case ex:异常类型1 => // 代码
    case ex:异常类型2 => // 代码
}
finally {
    // 代码
}

说明:
- try中的代码是我们编写的业务处理代码
-catch中表示当出现某个异常时,需要执行的代码
-finally中,是不管是否出现异常都会执行的代码

示例

示例说明

  • 使用try…catch来捕获除零异常
package com.lee

object Demo01 {

  /*
    示例
      示例说明
      - 使用try..catch来捕获除零异常
   */

  def main(args: Array[String]): Unit = {
    try {
      val a = 1 / 0

    } catch  {
      case  e:Exception => println("出现了异常操作......")
    }finally {
      println("模拟除零异常")
    }
  }

}
  • 如何抛出异常
package com.lee

object Demo02 {
  def main(args: Array[String]): Unit = {

    throw  new Exception("这是一个异常")

  }
}

基本抛出的语法是与java是一致的, 但是不同在于scala中不需要在方法上 显现将异常抛出, scala认为这种显示操作是一种设计败笔, scala默认不执行try 直接飘出处理

柯里化

柯里化, 主要的目的是为了进行参数分类, 有时候需要传递参数的时候, 可能要传递多个, 多个参数分为两类, 为了能够方便区分, 可以通过柯里化, 将方法拆分成一个参数多个参数列表形式

而且通过柯里化, 可以提供代码的维护性和便利性

​在实际使用中, 一般来说, 第一个 括号传递普通参数, 第二个括号传递制定一些规则

相关操作:

package com.lee

object Demo01 {

  /*
    - 编写一个方法,用来完成两个Int类型数字的计算 (+ - * / %)
    - 具体如何计算封装到函数中
    - 使用柯里化来实现上述操作


   */

  def main(args: Array[String]): Unit = {

    println(sumNum(10,20))

    val value1 = calculateNum(10,20)( (a,b) => a + b )
    val value2 = calculateNum(10,20)( (a,b) => a - b )
    val value3 = calculateNum(10,20)( (a,b) => a * b )
    val value4 = calculateNum(10,20)( (a,b) => a / b )
    val value5 = calculateNum(10,20)( (a,b) => a % b )
    println(value3)
  }
  //柯里化:
  def calculateNum(a:Int,b:Int)(fun1:(Int,Int) => Int ) ={
    fun1(a,b)
  }


  // 原始写法
  def sumNum(a:Int,b:Int) = {
    a + b
  }

  def  minusNum(a:Int,b:Int) = {
    a - b
  }

  def  multiplyNum(a:Int,b:Int) = {
    a * b
  }

  def  divideNum(a:Int,b:Int) = {
    a / b
  }

}

闭包

闭包其实就是一个函数,只不过这个函数的返回值依赖于声明在函数外部的变量。
可以简单认为,就是可以访问不在当前作用域范围的一个函数。

示例一

定义一个闭包

val y=10

val add=(x:Int)=>{
    x+y
}

println(add(5)) // 结果15

add函数就是一个闭包

示例二

柯里化就是一个闭包

  def add(x:Int)(y:Int) = {
    x + y
  }

上述代码相当于

  def add(x:Int) = {
    (y:Int) => x + y
  }

说明:

​ 闭包指的在执行函数的内容的时候, 函数中使用的变量并不是自己 {} 作用范围内的变量值, 而是使用比这个作用范围更大的 {}内的变量, 我们将这种行为, 称为闭包

闭包存在弊端:

​由于闭包 (函数) 会使用比起自己更大范围内数据, 就会导致这个更大范围内的数据的变量数据都被这个函数所加载到, 如果更大的作用范围下数据量非常的庞大, 也会导致这些变量被这个函数进行加载到内存, 从而导致内存使用增大, 然后执行变慢, 甚至出现内存溢出错误

隐式转换和隐式参数

所谓隐式转换,是指以implicit关键字声明的带有单个参数的方法。它是自动被调用的,自动将某种类型转换为另外一种类型。

使用步骤

  1. 在object中定义隐式转换方法(使用implicit)
  2. 在需要用到隐式转换的地方,引入隐式转换(使用import)
  3. 自动调用隐式转化后的方法

示例

示例说明

使用隐式转换,让File具备有read功能——实现将文本中的内容以字符串形式读取出来

步骤

  1. 创建RichFile类,提供一个read方法,用于将文件内容读取为字符串
  2. 定义一个隐式转换方法,将File隐式转换为RichFile对象
  3. 创建一个File,导入隐式转换,调用File的read方法
package com.lee

import java.io.File
import java.text.SimpleDateFormat

object Demo01 {

  class RichFile(file:File) {

    def read() = {
      println("执行了read方法, 读取数据....")
    }
  }

  class RichFile2(format:SimpleDateFormat) {

    def read() = {
      println("执行了read方法, 读取数据....")
    }

    def eat() = {
      println(" xxxxx")
    }

    def parse() = {
      println(" 执行parse")
    }
  }

  object  Rich_File {

    implicit  def file2RichFile(file:File) = new RichFile(file)
    implicit  def formatRichFile(format:SimpleDateFormat) = new RichFile2(format)

    implicit val pre_post: (String, String) = "<<<" -> ">>>"

  }
  // 方法可以带有一个标记为implicit的参数列表。这种情况,编译器会查找缺省值,提供给该方法。
  def book(name:String) ( implicit pre_post: (String, String)) ={

    println( pre_post._1 + name +  pre_post._2)

  }

  def main(args: Array[String]): Unit = {

    import  Rich_File._

    /*
    val file = new File("D:\\传智工作\\上课\\scala")

    file.read()

    val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    format.eat()

    format.parse()
    */


    book("斗破苍穹")
  }


}


什么时候会执行转换:

  • 当对象调用类中不存在的方法或者成员时,编译器会自动将对象进行隐式转换
  • 当方法中的参数的类型与目标类型不一致时
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

技术武器库

一句真诚的谢谢,胜过千言万语

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

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

打赏作者

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

抵扣说明:

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

余额充值