21.Swift-协议


Protocol(协议)定义方法、属性和其他为了完成某个特殊功能的要求的蓝图。协议并不提供任何具体实现,只描述了实现将会是什么样子。协议可以被类、结构体或者枚举遵守,并根据协议要求,提供具体的实现。

 

遵循者需要提供协议指定的成员,如属性,方法,操作符,下标等。


一、协议语法

协议的定义与类,结构体,枚举的定义非常相似,如下所示:

protocol SomeProtocol{

    //protocol definition goes here

}


在类,结构体,枚举的名称后加上协议名称,中间以冒号:分隔即可实现协议;实现多个协议时,各协议之间用逗号,分隔,如下所示:

struct SomeStructure:FirstProtocol,AnotherProtocol{
//structure definition gose here

}


当某个类含有父类的同时并实现了协议,应当把父类放在所有的协议之前,如下所示:

class SomeClass:SomeSuperClass,FirstProtocol,AnotherProtocol{
//class definition goes here

}



二、属性要求

协议能够要求其遵循者必须含有一些特定名称和类型的实例属性(instance property)或类属性 (type property),也能够要求属性的(设置权限)settable 和(访问权限)gettable,但它不要求属性是存储型属性(stored property)还是计算型属性(calculate property)。


如果协议要求一个属性是可读写的,那么这个属性要求是不能是常量存储属性或者只读计算属性。如果协议只要求属性是可读的,那么你也可以为属性添加setter方法。


通常前置var关键字将属性声明为变量。在属性声明后写上{ get set }表示属性为可读写的。{ get }用来表示属性为可读的。

protocol SomeProtocol{

    var mustBeSettable:Int{get set}

    var doesNotNeedToBeSettable:Int{get}

}


用类来实现协议时,使用class关键字来表示该属性为类成员;用结构体或枚举实现协议时,则使用static关键字来表示:

protocol AnotherProtocol{

    class var someTypeProperty: Int {get set}

}


本例中有一个只有一个实例属性要求的一个协议:

protocol FullyNamed{

    var fullName:String{get}

}


FullyNamed协议含有fullName属性。因此其遵循者必须含有一个名为fullName,类型为String的可读属性。

struct Person:FullyNamed {

    var fullName:String

}

let john = Person(fullName: "John Appleseed")


Person结构体含有一个名为fullName的存储型属性,完整的遵循了协议。(若协议未被完整遵循,编译时则会报错)。


如下所示,Startship类遵循了FullyNamed协议:

class Startship:FullyNamed{

    var prefix:String?

    var name:String

    init(name:String,prefix:String? = nil){

        self.name = name

        self.prefix = prefix

    }

    var fullName:String{

        return (prefix != nil ? prefix! + " " : "") + name

    }

}

var ncc1701 = Startship(name: "Enterprise", prefix: "USS")


Starship类将fullName实现为可读的计算型属性。它的每一个实例都有一个名为name的必备属性和一个名为prefix的可选属性。 当prefix存在时,将prefix插入到name之前来为Starship构建fullName。


三、方法要求

协议能够要求其遵循者必备某些特定的实例方法和类方法。协议方法的声明与普通方法声明相似,但它不需要方法内容。

 

注意: 协议方法支持变长参数(variadic parameter),不支持默认参数(default parameter)。

 

前置class关键字表示协议中的成员为类成员;当协议用于被枚举或结构体遵循时,则使用static关键字如下所示:

protocol SomeProtocol{

    class func someTypeMethod()

}


下面定义一个只包含一个实例方法要求的协议:

protocol RandomNumberGenerator{

    func random() -> Double

}


RandomNumberGenerator协议要求其遵循者必须拥有一个名为random, 返回值类型为Double的实例方法。(我们假设随机数在[0,1]区间内)。

 

LinearCongruentialGenerator类遵循了RandomNumberGenerator协议,并提供了一个叫做线性同余生成器(linear congruential generator)的伪随机数算法。

class LinearCongruentialGenerator:RandomNumberGenerator{

    var lastRandom = 42.0

    let m = 139968.0

    let a = 3877.0

    let c = 2973.0

    func random() -> Double {

        lastRandom = ((lastRandom * a + c) % m)

        return lastRandom / m

    }

}

let generator = LinearCongruentialGenerator()

println("Here's a random number:\(generator.random())")//Here's a random number:0.184606481481481

println("And another one:\(generator.random())")//And another one:0.74056927297668



四、突变方法要求

能在方法或函数内部改变实例类型的方法称为突变方法在值类型(Value Type)(结构体和枚举)中的函数前缀加上mutating关键字来表示该函数允许改变该实例和其属性的类型。 

 

类中的成员为引用类型(Reference Type),可以方便的修改实例及其属性的值而无需改变类型;而结构体和枚举中的成员均为值类型(Value Type),修改变量的值就相当于修改变量的类型,而Swift默认不允许修改类型,因此需要前置mutating关键字用来表示该函数中能够修改类型)

 

注意:用class实现协议中的mutating方法时,不用写mutating关键字;用结构体、枚举实现协议中的mutating方法时,必须写mutating关键字。

 

如下所示,Togglable协议含有toggle函数。根据函数名称推测,toggle可能用于切换或恢复某个属性的状态。mutating关键字表示它为突变方法:

protocol Toggleable{

    mutating func toggle()

}


当使用枚举或结构体来实现Togglabl协议时,必须在toggle方法前加上mutating关键字。

 

如下所示,OnOffSwitch枚举遵循了Togglable协议,On,Off两个成员用于表示当前状态

enum OnOffSwith:Toggleable{

    case Off,On

    mutating func toggle() {

        switch self{

        case .Off:

            self = On

        case .On:

            self = Off

        }

    }

}

var lightSwitch = OnOffSwith.Off

lightSwitch.toggle()//lightSwith is now equal to .On



五、构造器要求

协议可以要求遵守协议者具体实现特定的构造器。可以用像正常的构造器那样的定义语法在协议中定义,但是没有具体实现。

protocol SomeProtocol{
init(someParameter:Int)


}



1、类实现协议构造器要求

你可以在协议遵守类中用指定构造器或者便利构造器实现协议构造器,不管是用哪种方式,都必须在构造器前面加上required关键字来标识它为必须的构造器。

class SomeClass:SomeProtocol{
required init(someParameter:Int){
//initializer implementation goes here

}

}


使用required关键字标识是为了确保让协议的遵守类的子类们都遵守这个协议。


注意:如果协议遵守类加了final关键字,就不必要在协议构造器前面加上required关键字,因为final类是不能被继承的。


如果子类重写了父类的指定构造器,并且实现的协议构造器和这个指定构造器是匹配的,那么,在构造器前面同时加上required和override关键字:

protocol SomeProtocol1{

    init()

}

class SomeSuperClass{

    init(){

        //initializer implementation goes here

    }

}

class SomeSubClass:SomeSuperClass,SomeProtocol1{

    //required from SomeProtocol conformance

    //override from SomeSuperClass

    required override init(){

        //initializer implementation goes here

    }

}



2、可失败构造器要求

协议可以为协议遵守者定义可失败构造器。

协议遵守者可以用可失败构造器或者不可失败构造器来实现协议中定义的可失败构造器。不可失败的协议构造器可以用不可失败构造器或者隐式展开可失败构造器来实现。



六、协议类型

协议本身不实现任何功能,但你可以将它当做类型来使用。

 

使用场景: 

1. 作为函数,方法或构造器中的参数类型,返回值类型

2. 作为常量,变量,属性的类型

3. 作为数组,字典或其他容器中的元素类型

 

注意:协议类型应与其他类型(Int,Double,String)的写法相同,使用驼峰式


class Dice{

    let sides:Int

    let generator:RandomNumberGenerator

    init(sides:Int,generator:RandomNumberGenerator){

        self.sides = sides

        self.generator = generator

    }

    func roll() -> Int{

        return Int(generator.random() * Double(sides)) + 1

    }

}

这里定义了一个名为 Dice的类,用来代表桌游中的N个面的骰子。

 

Dice含有sides和generator两个属性,前者用来表示骰子有几个面,后者为骰子提供一个随机数生成器。由于后者为RandomNumberGenerator的协议类型。所以它能够被赋值为任意遵循该协议的类型。

 

此外,使用构造器(init)来代替之前版本中的setup操作。构造器中含有一个名为generator,类型为RandomNumberGenerator的形参,使得它可以接收任意遵循RandomNumberGenerator协议的类型。

 

roll方法用来模拟骰子的面值。它先使用generator的random方法来创建一个[0-1]区间内的随机数种子,然后加工这个随机数种子生成骰子的面值。

 

如下所示,LinearCongruentialGenerator的实例作为随机数生成器传入Dice的构造器

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())

for _ in 1...5{

    println("Random dice roll is \(d6.roll())")

}



七、委托

委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能交由(委托)给其他的类型。

 

委托模式的实现很简单: 定义协议来封装那些需要被委托的函数和方法, 使其遵循者拥有这些被委托的函数和方法。

 

委托模式可以用来响应特定的动作或接收外部数据源提供的数据,而无需要知道外部数据源的类型。

 

下文是两个基于骰子游戏的协议:

protocol DiceGame{

    var dice:Dice{get}

    func play()

}

protocol DiceGameDelegate{

    func gameDidStart(game:DiceGame)

    func game(game:DiceGame,didStartNewTurnWithDiceRoll diceRoll:Int)

    func gameDidEnd(game:DiceGame)

}


DiceGame协议可以在任意含有骰子的游戏中实现,DiceGameDelegate协议可以用来追踪DiceGame的游戏过程。

 

如下所示,SnakesAndLadders是Snakes and Ladders。新版本使用Dice作为骰子,并且实现了DiceGame和DiceGameDelegate协议

class SnakesAndLadders:DiceGame{

    let finalSquare = 25

    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())

    var square = 0

    var board:[Int]

    init(){

        board = [Int](count: finalSquare + 1, repeatedValue: 0)

        board[03] = +8

        board[06] = +11

        board[09] = +9

        board[10] = +2

        board[14] = -10

        board[19] = -11

        board[22] = -2

        board[24] = -8

    }

    var delegate:DiceGameDelegate?

    func play() {

        square = 0

        delegate?.gameDidStart(self)

        gameLoop:while square != finalSquare{

            let diceRoll = dice.roll()

            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)

            switch square + diceRoll {

            case finalSquare:

                break gameLoop

            case let newSquare where newSquare > finalSquare:

                continue gameLoop

            default:

                square += diceRoll

                square += board[square]

            }

        }

        delegate?.gameDidEnd(self)

    }

}

这个版本的游戏用一个类SnakesAndLadders来实现,它遵守了DiceGame协议。它提供了一个dice属性和一个play方法(dice属性是常量,因为它在初始化完成之后不必要修改并且协议只要求dice是可获取的。)

游戏的初始化设置(setup)被SnakesAndLadders类的构造器(initializer)实现。所有的游戏逻辑被转移到了play方法中。

 

注意:因为delegate并不是该游戏的必备条件,delegate被定义为遵循DiceGameDelegate协议的可选属性

DicegameDelegate协议提供了三个方法用来追踪游戏过程。被放置于游戏的逻辑中,即play()方法内。分别在游戏开始时,新一轮开始时,游戏结束时被调用。

 

因为delegate是一个遵循DiceGameDelegate的可选属性,因此在play()方法中使用了可选链来调用委托方法。 若delegate属性为nil, 则委托调用优雅地失效。若delegate不为nil,则委托方法被调用

 

如下所示,DiceGameTracker遵循了DiceGameDelegate协议:

class DiceGameTracker:DiceGameDelegate{

    var numberOfTurns = 0

    func gameDidStart(game: DiceGame) {

        numberOfTurns = 0

        if game is SnakesAndLadders{

            println("Started a new game of Snakes and Ladders")

            println("The game is using a \(game.dice.sides)-sides dice")

        }

    }

    func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {

        ++numberOfTurns

        println("Rolled a \(diceRoll)")

    }

    func gameDidEnd(game: DiceGame) {

        println("The game lasted for \(numberOfTurns) turns")

    }

}

DiceGameTracker实现了DiceGameDelegate协议的方法要求,用来记录游戏已经进行的轮数。 当游戏开始时,numberOfTurns属性被赋值为0;在每新一轮中递加;游戏结束后,输出打印游戏的总轮数。

 

gameDidStart方法从game参数获取游戏信息并输出。game在方法中被当做DiceGame类型而不是SnakeAndLadders类型,所以方法中只能访问DiceGame协议中的成员。但是,在方法中依然可以通过类型转换来访问特定类型实例。在本例中,它检查了game参数是否是SnakesAndLadders的实例,如果是的话就打印相关信息。

gameDidStart方法也访问了game参数的dice属性,因为game遵守了DiceGame协议,确保了它拥有dice属性。

 

DiceGameTracker的运行情况,如下所示:

let tracker = DiceGameTracker()

let game = SnakesAndLadders()

game.delegate = tracker

game.play()

/*

Started a new game of Snakes and Ladders

The game is using a 6-sides dice

Rolled a 2

Rolled a 5

Rolled a 2

Rolled a 4

Rolled a 3

Rolled a 4

Rolled a 1

Rolled a 4

Rolled a 5

The game lasted for 9 turns

*/


八、在扩展中添加协议成员

即使你无权访问已有类型,你依然可以通过扩展来使已有类型遵守一个新的协议。扩展可以为已有类型添加属性、方法和附属脚本,也可以添加一个协议可能需要的任何要求。

注意:一旦某个类型在扩展中遵守了某个协议,所有的本类型的实例都会自动遵守这个协议。

例如,下面一个TextRepresentable协议可以被任何可以以文本形式展现自己(可以是自己的描述或者当前状态的文字信息)的类型来实现:

protocol TextRepresentable{

    func asText() -> String

}


之前的Dice可以被扩展来遵守TextRepresentable协议:

extension Dice:TextRepresentable{

    func asText() -> String {

        return "A \(sides)-sided dice"

    }

}


从现在起,Dice类型的实例可被当作TextRepresentable类型:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())

println(d12.asText())//A 12-sided dice



SnakesAndLadders类也可以通过扩展的方式来遵循协议:

extension SnakesAndLadders:TextRepresentable{

    func asText() -> String {

        return "A game of Snakes and Ladders with \(finalSquare) squares"

    }

}

println(game.asText())//A game of Snakes and Ladders with 25 squares


通过扩展补充协议遵守声明

当一个类型已经实现了一个协议中的所有要求,却没有声明遵守这个协议,可以通过一个空的扩展来补充协议声明

struct Hamster{

    var name:String

    func asText() -> String{

        return "A hamster named \(name)"

    }

}


extension Hamster:TextRepresentable{}


从现在起,Hamster的实例可以作为TextRepresentable类型使用

let simonTheHamster = Hamster(name: "Simon")

let somethingTextRepresentable:TextRepresentable = simonTheHamster

println(simonTheHamster.asText())//A hamster named Simon

println(somethingTextRepresentable.asText())//A hamster named Simon


注意:即时满足了协议的所有要求,类型也不会自动遵守协议,因此你必须为它做出明显的协议声明。


九、集合中的协议类型

协议类型可以被集合使用,表示集合中的元素均为协议类型:

let things:[TextRepresentable] = [game,d12,simonTheHamster]

for thing in things{

    println(thing.asText())

}

thing被当做是TextRepresentable类型而不是Dice,DiceGame,Hamster等类型。因此能且仅能调用asText方法。



十、协议的继承

协议能够继承一到多个其他协议。语法与类的继承相似,多个协议间用逗号,分隔。

如下所示,PrettyTextRepresentable协议继承了TextRepresentable协议

protocol PrettyTextRepresentable:TextRepresentable{

    func asPrettyText() -> String

}


本例中PrettyTextRepresentable协议继承了TextRepresentable协议并新增了一个方法要求,任何遵守PrettyTextRepresentable协议的类型必须同时满足TextRepresentable和PrettyTextRepresentable协议的要求。


如下所示,用扩展为SnakesAndLadders遵循PrettyTextRepresentable协议:

extension SnakesAndLadders:PrettyTextRepresentable{

    func asPrettyText() -> String {

        var output = asText() + ":\n"

        for index in 1 ... finalSquare{

            switch board[index]{

            case let ladder where ladder > 0 :

                output += "+ "

            case let snake where snake < 0 :

                output += "- "

            default:

                output += "0 "

            }

        }

        return output

    }

}

println(game.asPrettyText())//A game of Snakes and Ladders with 25 squares:\n0 0 + 0 0 + 0 0 + + 0 0 0 - 0 0 0 0 - 0 0 - 0 - 0



十一、只对类有效的协议  Class-Only Protocol

你可以通过在协议的继承链前面加上class关键字来限制只有类类型(而不是结构体或枚举类型)可以遵守这个协议。class关键字必须出现在所有继承的协议前面:

protocol SomeClassOnlyProtocol:class,SomeInheritedProtocol{
//class-only protocol definition goes here

}

SomeClassOnlyProtocol协议的遵守者必须是类类型,如果尝试让结构体或者枚举遵守这个协议,编译会出错。


注意:当协议定义的要求假设或者需要遵守者是引用类型时,使用class-only的协议。



十二、协议合成

一个类型同时遵守很多协议是很有用的。你可以用protocol<SomeProtocol,AnotherProtocol>的形式来合并多个协议成为一个要求。 

protocol Named{

    var name:String{get}

}

protocol Aged{

    var age:Int{get}

}

struct Person:Named,Aged {

    var name:String

    var age:Int

}

//注意不能写成这样

//struct Person:protocol<Named,Aged> {

//    var name:String

//    var age:Int

//}

func wishHappyBirthday(celebrator:protocol<Named,Aged>){

    println("Happy birthday \(celebrator.name) - your're \(celebrator.age)")

}

let birthdayPerson = Person(name: "Malcolm", age: 21)

wishHappyBirthday(birthdayPerson)//Happy birthday Malcolm - your're 21


Named协议包含String类型的name属性;Aged协议包含Int类型的age属性。Person结构体遵循了这两个协议。

 

wishHappyBirthday函数的形参celebrator的类型为protocol<Named,Aged>。可以传入任意遵循这两个协议的类型的实例。

 

注意:协议合成并不会生成一个新协议类型,而是将多个协议合成为一个临时的本地协议,超出范围后立即失效。



十三、检查协议的一致性

使用is检验协议一致性,使用as将协议类型向下转换(downcast)为的其他协议类型。检验与转换的语法和之前相同(详情查看类型检查):

1. is操作符用来检查实例是否遵循了某个协议。

2. as?返回一个可选值,当实例遵循协议时,返回该协议类型;否则返回nil

3. as用以强制向下转换型。如果转换失败会导致运行时错误。


@objc protocol HasArea{

    var area:Double{get}

}

注意:只有协议使用了@objc定义,你才可以检查协议的一致性。@objc用来表示暴露给Objective-C的代码,此外,@objc型协议只对类有效,因此只能在类中检查协议的一致性。详情查看Using Siwft with Cocoa and Objectivei-C。


下面定义了两个遵守HasArea协议的类Circle和Country:

class Circle:HasArea{

    let pi = 3.141927

    var radius:Double

    var area:Double{return pi * radius * radius}

    init(radius:Double){self.radius = radius}

}


class Country:HasArea {

    var area:Double

    init(area:Double){self.area = area}

}

Circle类用基于radius的计算属性实现了协议的要求的area属性。Country类直接用存储属性实现了协议要求的area属性。


下面定义一个没有实现HasArea协议的类Animal:

class Animal{

    var legs:Int

    init(legs:Int){

        self.legs = legs

    }

}

Circle,Country,Animal并没有一个相同的基类,所以采用AnyObject类型的数组来装载在他们的实例,如下所示:

let objects:[AnyObject] = [

    Circle(radius: 2.0),

    Country(area: 243_610),

    Animal(legs: 4)

]

如下所示,在迭代时检查object数组的元素是否遵循了HasArea协议:

for object in objects{

    if let objectWithArea = object as? HasArea{

        println("Area is \(objectWithArea.area)")

    }else{

        println("Something that does not have an area")

    }

}

/*

Area is 12.567708

Area is 243610.0

Something that does not have an area

*/

当数组中的元素遵循HasArea协议时,通过as?操作符将其可选绑定(optional binding)到objectWithArea常量上。

 

objects数组中元素的类型并不会因为向下转型而改变,当它们被赋值给objectWithArea时只被视为HasArea类型,因此只有area属性能够被访问。



十四、可选协议要求

可选协议含有可选成员,其遵循者可以选择是否实现这些成员。在协议中使用@optional关键字作为前缀来定义可选成员。

 

可选协议在调用时使用可选链,详细内容在可选链章节中查看。

 

像someOptionalMethod?(someArgument)一样,你可以在可选方法名称后加上?来检查该方法是否被实现。可选方法和可选属性都会返回一个可选值(optional value),当其不可访问时,?之后语句不会执行,并返回nil。

 

注意:可选协议只能在含有@objc前缀的协议中生效。且@objc的协议只能被类遵循。

 

Counter类使用CounterDataSource类型的外部数据源来提供增量值(increment amount),如下所示:

@objc protocol CountertDataSource{

    optional func incrementForCount(count:Int) -> Int

    optional var fixedIncrement:Int{get}

}


CounterDataSource含有incrementForCount的可选方法和fiexdIncrement的可选属性。

 

注意:CounterDataSource中的属性和方法都是可选的,因此可以在类中声明但不实现这些成员,尽管技术上允许这样做,不过最好不要这样写。

 

Counter类含有CounterDataSource?类型的可选属性dataSource,如下所示:

@objc class Counter{

    var count = 0

    var dataSource:CountertDataSource?

    func increment(){

        if let amount = dataSource?.incrementForCount?(count){

            count += amount

        }else if let amount = dataSource?.fixedIncrement?{

            count += amount

        }

    }

}


count属性用于存储当前的值,increment方法用来为count赋值。

 

increment方法通过可选链,尝试从两种可选成员中获取count。

 

由于dataSource可能为nil,因此在dataSource后边加上了?标记来表明只在dataSource非空时才去调用incrementForCount方法。

即使dataSource存在,但是也无法保证其是否实现了incrementForCount方法,因此在incrementForCount方法后边也加有?标记。

在调用incrementForCount方法后,Int型可选值通过可选绑定(optional binding)自动拆包并赋值给常量amount。

 

当incrementForCount不能被调用时,尝试使用可选属性fixedIncrement来代替。

 

ThreeSource实现了CounterDataSource协议,如下所示:

class ThreeSource:CountertDataSource{

    let fixedIncrement = 3

}

可以用ThreeSource的实例作为Counter实例的dataSource:

var counter = Counter()

counter.dataSource = ThreeSource()

for _ in 1 ... 4 {

    counter.increment()

    println(counter.count)

}


TowardsZeroSource实现了CounterDataSource协议中的incrementForCount方法,如下所示:

class TowardsZeroSource:CountertDataSource{

    func incrementForCount(count: Int) -> Int {

        if count == 0 {

            return 0

        }else if count < 0 {

            return 1

        }else{

            return -1

        }

    }

}

counter.count = -4

counter.dataSource = TowardsZeroSource()

for _ in 1 ... 5{

    counter.increment()

    println(counter.count)

}

/*

-3

-2

-1

0

0

*/



十五、总结

本章介绍了Swift中的协议和委托的使用,跟OC里面的协议和委托的设计原理和目的是一样的,只是Swift中的协议和委托的语法不一样。有一下几点区别和需要注意的地方:

1、在Swift中类、结构体和枚举都可以遵守协议,不过也可以定义只对类有效的协议。如果类型要遵守多个协议,多个协议加在类型加冒号后面并用逗号隔开,如果类型有父类,父类一定要写在协议前面。

2、注意协议对属性、方法和构造器的要求。

3、Swift中的协议可以当做类型使用,可以被继承。

4、可以用协议合成语法临时合并多个协议为一个临时的本地协议。

5、可以在类中对协议一致性进行检查,也可以定义可选的协议要求,只是都必须定义协议的时候加上@objc关键字,并且@objc协议只能被类所遵循。

6、可以通过扩展使已有的类型遵循指定的协议。



参考:

1、The Swift Programming Language

2、http://www.cocoachina.com/ios/20140612/8793.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值