C++经典对话上关于模式的两篇好文章

6 篇文章 0 订阅

 

    偶然在网上找到C++经典对话系列这本好书,一本非常不错的书,感谢作者和译者们,下面是节录其中和设计模式有关的两篇   
 
                        Abstract Factory, Template Style
 
“我们知道这是什么吗?”我指着放在前面金属台上的设备。这是在冰原下勘探到的第一批物体之一。
 
珍妮摇着头。”没呢,伙计。它可能是个奖杯,飞船推进器,儿童玩具,或就只是垃圾。
上周,我们曾认为将能源系统制造出来了,供这个和其它一些设备用的,
但在加入燃料后什么也没发生。随着能源的远去,技术又重回到了画图板上,但他们认为很快能搞定它。.”
 
“凭什么?”
 
“希望。”
 
我瞄了一下墙上的钟。”好吧,时间差不多了。我在发抖,我很冷。
每次我们要求勘探队给我们些新的东西来鼓捣时,他们总弄出些新的不同的东西。我真希望能弄明白哪怕其中任何一个。”
 
“我希望我们对从这个‘artifact factory’中出产的东西能知道得更多些。”珍妮同意。
 
“这使我回想起以前发生在我身上的一些事,在那时……”
 
珍妮眨着她的眼睛。”告诉为,什么时候我们可以到个暖和的地方。”
 
 
--------------------------------------------------------------------------------
 
“坏消息,”我越过隔板向温迪嚷道。
 
“呒?”她探起头。
 
“你知道我上周写的类的吧。”
 
“不是十分清楚,说下去。”
 
我没在意。”好吧,”我继续着,”我必须为它实现一个类厂。Guru建议我看一下客户群写的类厂。”
 
“嗯,于是?”突然沮丧显现在她脸上:”噢。鲍勃写的,是吧?”
 
我沉重地点点头。”是的,我唯一能给予他的称赞是 —每当我打开他的代码,
我都能学会有些事不该如何实现。”温迪哈哈地笑着坐回位置上。
 
我叹了口气,然后check out源码。还好,没我想象的那么坏。
只不过是极大量的if语句嵌套-我做好了坏得多的准备的。
但已是够可怕的代码了,所以根据Guru的忠告,我准备实现一个Abstract Class Factory [1]。
由于没有多线程和并发的需求我决定将厂实现为singleton模式:
 
class BaseFactory
{
    typedef std::auto_ptr<Base> (*BaseCreateFn)();
    typedef std::map<std::string, BaseCreateFn> FnRegistry;
    FnRegistry registry;
    BaseFactory() {}
    BaseFactory(const BaseFactory &); // Not implemented
    BaseFactory & operator=(const BaseFactory &); // Not implemented
public:
    static BaseFactory & instance() { static BaseFactory bf; return bf; }
    bool RegCreateFn(const std::string &, BaseCreateFn);
    std::auto_ptr<Base> Create(const std::string &) const;
};
bool BaseFactory::RegCreateFn(const std::string & className, BaseCreateFn fn)
{
    registry[className] = fn;
    return true;
}
std::auto_ptr<Base> BaseFactory::Create(const std::string &className) const
{
    std::auto_ptr<Base> theObject(0);
    FnRegistry::const_iterator regEntry = registry.find(className);
    if (regEntry != registry.end())
    {
        theObject = regEntry->second();
    }
    return theObject;
}
在Base的实现文件中,我加入:
 
namespace
{
    std::auto_ptr<Base> CreateBase()
    {
        return std::auto_ptr<Base>(new Base);
    }
    bool dummy = BaseFactory::instance().RegCreateFn(“Base”, CreateBase);
}
“嗯,不错,”我思索着。”向厂注册一个函数后,生成实例就如数1、2、3那么简单”
 
int main()
{
    std::auto_ptr<Base> anObject = BaseFactory::instance().Create(“Base”);
}
我继续生成一个派生类,以测试通过厂来生成实例。在派生类的实现中,我加入:
 
namespace
{
    std::auto_ptr<Derived> CreateDerived()
    {
        return std::auto_ptr<Derived>(new Derived);
    }
    bool dummy = BaseFactory::instance().RegCreateFn(“Derived”, CreateDerived);
}
但编译器冷冷地阻住了我-它提示不能将CreateDerived转换为所需类型。
在迷惑了好一阵之后,我想起以前经历过的隐式地转换为指向Base的指针的问题 [2]。
我认识到我又一次遇到编译器不能进行隐式指针转换问题,所以我稍稍修改了create函数:
 
namespace
{
    std::auto_ptr<Base> CreateDerived()
    {
        return std::auto_ptr<Base>(new Derived);
    }
    bool dummy = BaseFactory::instance().RegCreateFn(“Derived”, CreateDerived);
}
回顾一下基类,我注意到注册代码几乎相同,所以我写了个替代的宏:
 
#define REGISTER_CLASS(BASE_CLASS, DERIVED_CLASS) /
namespace /
{ /
    std::auto_ptr<BASE_CLASS> Create##DERIVED_CLASS() /
    { /
        return std::auto_ptr<BASE_CLASS>(new DERIVED_CLASS); /
    } /
    bool dummy=BaseFactory::instance().RegCreateFn( /
        #DERIVED_CLASS, Create##DERIVED_CLASS); /
}
使用这个宏很简单:
 
REGISTER_CLASS(Base, Base)
“近一小时的工作做得不错,”我在完成时嘟囔着。
 
“是的,确实如此,我的孩子。但你还能做得更好。”
 
Guru轻柔的声音出现在身后时,我还是会被吓一跳。”什么-?”
 
“这样展开的宏,”她解释道,”是难以阅读和理解的,
并且宏不是类型安全的。还有,这个bool变量是什么目的?”
 
“这个嘛,”我有些抵触地答道,”它是我唯一能想到的,
能确保注册函数被自动调用的方法。我不想在每增加一个新类时都要修改典型厂的代码。”
 
她点点头。”很聪明的想法,我的孩子。但是,你的厂不足够通用。
会有越来越多的需要abstract factory的类。我期望你创建这样一个厂:
能在它被应用的每个类层次上自动处理,而不是要求修改
(create a factory that will handle, not require, modification for each class hierarchy it will be used on)。”
 
“一个通用的abstract factory?这有些苛求,不是吗?”
 
“徒弟,你从不畏惧挑战,是吧?‘Experience is by industry achieved... “
 
“‘... and perfected by the swift course of time’,”我接道[3]。
“我从不畏惧挑战,但我不能将进度表置之不理地花费大量时间。”
 
“你已经几乎就做到了。你所需要的就是考虑通用,”她指点道。
 
“考虑通用……也就是说,模板!”我嚷着转向显示器。几分钟之后,我向身后瞄了一下,
Guru如出现时一样已悄悄地不见了。我稍稍有些哆嗦,又转回去继续工作。
 
过了一会儿,我实现了一个厂模板:
 
template <class ManufacturedType, typename ClassIDKey=std::string>
class GenericFactory
{
    typedef std::auto_ptr<ManufacturedType> (*BaseCreateFn)();
    typedef std::map<ClassIDKey, BaseCreateFn> FnRegistry;
    FnRegistry registry;
    GenericFactory();
    GenericFactory(const GenericFactory&); // Not implemented
    GenericFactory &operator=(const GenericFactory&); // Not implemented
public:
    static GenericFactory &instance();
    void RegCreateFn(const ClassIDKey &, BaseCreateFn);
    std::auto_ptr<ManufacturedType> Create(const ClassIDKey &className) const;
};
我认为,不是所有的类都用std::string作为键值,所以我将键值类型作为一个模板参数。
所有函数的实现都和我在BaseFactory中写过的一模一样,除了那个注册函数之外。我用一个更优雅的模板来实现它的:
 
template <class AncestorType, class ManufacturedType, typename ClassIDKey=std::string>
class RegisterInFactory
{
public:
    static std::auto_ptr<AncestorType> CreateInstance()
    {
        return std::auto_ptr<AncestorType>(new ManufacturedType);
    }
    RegisterInFactory(const ClassIDKey &id)
    {
        GenericFactory<AncestorType>::instance().RegCreateFn(id, CreateInstance);
    }
};
现在,每个从基类派生的类只需要增加一行代码就可以获得类型安全的creation函数:
 
RegisterInFactory<Base, Base> registerMe(“Base”);
模板的构造函数向creation registry注册它的类名[4]。
 
我向后一瞄,正好看见Guru走过来。我偷笑了一下;; for once I was slightly ahead of her game至少有一次,
我预见了她的把戏。”很好,徒弟,”她走到我身后说道。”你的厂是通用的、可移植的,不依赖于注册表、DLL或其它复杂的技巧。”
 
“但还有一个缺点,”我打断说。 “因为这个注册行为依赖于一个static对象必须已被初始化,
如果在main被执行前就使用这个厂的话,不确保所有的creation函数成功注册。”
 
“不错,徒弟,你的厂依赖于静态初始化顺序问题,先知Cline讲解过这个问题 [5]。”她转身离开了。
 
“等一下,”我喊道。Guru转过身。我试图用一个文雅的方式询问盘据一个在我脑海中的问题。
Guru在等待时向耳后捋了一下头发。”是关于鲍勃的。既然他是如此差的一个程序员,”我试图说道,”怎么……”
 
“为什么他还在这儿待着?”Guru接道。我点点头。Guru思索了一下。”你注意他老惹麻烦?
主管认为他有两把刷子,不会犯错。因为我们……嗯……过去,鲍勃让他们相信,我对他的抱怨是源于作为前妻的怀恨。
并且,对他而言,任何与我意见一致的人都是参与了我与他作对的阴谋-他的说法,不是我的。”
 
“真是无赖。你为什么容忍他?你可以另找一个同样的工作的。”我掰着手指。
 
她耸了一下肩。”除了鲍勃,我喜欢这儿。此外,如果我离开了,就不能享受这样挑他错的快乐了。”
我们都笑了。”但是,说真的,”她接着说道,”这个公司有着大量的成长机会。
比如,我们新的医疗设备部门将需要一个软件开发的核心人物-我在申请这个位置。”
 
“太好了!祝你好运,”我答道。Guru点头示谢。我回到我的工作间,以完成abstract factory模板的最后部分。
 
 
--------------------------------------------------------------------------------
 
消息传来时,已是几天后的深夜。但我直到第二天早上起来后才知道的。还睡眼惺忪的,
我陷入混乱,不同寻常的噪音使得我用手捂住耳朵缩成一团。”怎么回事?”
我向已经先到的十几人嘟囔着,他们正高兴地互相交谈, “一大早怎么那么开心?”
 
主管吉尔伯笑着答道,”我们认为我们找到它了,我的孩子,我想我们是办到了。”
 
“什么?”我一头雾水。
 
“看起来能源恢复了,”珍妮说得很简捷。
 
 --------------------------------------------------------------------------------------------------------------
 
---------------------------------------------------------------------------------------------------------------
 
---------------------------------------------------------------------------------------------------------------
 
                                                           
 
与大虾对话: 领悟设计模式
我 --- 一个追求上进的C++程序员,尚在试用期,聪明但是经验不足。
 
Wendy --- 公司里的技术大拿,就坐在我旁边的隔间里,C++大虾,最了不起的是,她是个女的!她什么都好,就是有点刻薄,
 
我对她真是又崇拜又嫉妒。
 
 
--------------------------------------------------------------------------------
 
I. Virtually Yours -- Template Method模式
我在研究Wendy写的一个类。那是她为这个项目写的一个抽象基类,而我的工作就是从中派生出一个具象类(concrete class)。
这个类的public部分是这样的:
 
class Mountie {
public:
    void read( std::istream & );
    void write( std::ostream & ) const;
    virtual ~Mountie();
很正常,virtual destructor表明这个类打算被继承。那么再看看其protected部分:
 
protected:
    virtual void do_read( std::istream & );
    virtual void do_write( std::ostream & ) const;
也不过就是一会儿的功夫,我识破了Wendy的把戏:她在使用template method模式。public成员函数read和write是非虚拟的,
它们肯定是调用protected部分do_read/do_write虚拟成员函数来完成实际的工作。
 
啊,我简直为自己的进步而飘飘然了!哈,Wendy,这回你可难不住我,还有什么招数?
尽管放马过来... 突然,笑容在我脸上凝固,因为我看到了其private部分:
 
private:
    virtual std::string classID() const = 0;
这是什么?一个private纯虚函数,能工作么?我站了起来,
 
“Wendy,你的Mountie类好像不能工作耶,它有一个private virtual function。”
 
“你试过了?”她连头都不抬。
 
“嗯,那倒是没有啦,可是想想也不行啊?我的派生类怎么能override你的private函数呢?” 我嘟囔着。
 
“嗬,你倒是很确定啊!”Wendy的声音很轻柔,“你怎么老是这也不行,
那也不行的,这几个月跟着我你就没学到什么东西吗?小菜鸟。”
 
真是可恶啊...
 
“小菜鸟,你全都忘了,访问控制级别跟一个函数是不是虚拟的根本没关系。
判断一个函数是动态绑定还是静态绑定是函数调用解析的最后一个步骤。好好读读标准的3.4和5.2.2节吧。”
 
我完全处于下风,只好采取干扰战术。“好吧,就算你说的不错,我也还是不明白,何必把它设为private?”
 
“我且问你,倘若你不想让一个类中的成员函数被其他的类调用,应当如何处理?”
 
“当然是把它设置为private的,” 我回答道。
 
“那么你去看看我的Mountie类实现,特别是write()函数的实现。”
 
我正巴不得逃开Wendy那刺人的目光,便转过头去在我的屏幕上搜索,很快,我找到了:
 
void Mountie::write(std::ostream &Dudley) const
{
    Dudley << classID() << std::endl;
    do_write(Dudley);
}
嗨,最近卡通片真是看得太多了,居然犯这样的低级失误。还是老是承认吧:
“好了,我明白了。classID()是一个实现细节,用来在保存对象时指示具象类的类型,
派生类必须覆盖它,所以必须是纯虚的。但是既然是实现细节,就应该设为private的。”
 
“这还差不多,小菜鸟。”大虾点了点头,“现在给我解释一下为什么do_read()和do_write()是protected的?”
 
这个问题并不难,我组织了一下就回答:“因为派生类对象需要调用这两个函数的实现来读写其中的基类对象。”
 
“很好很好,”大虾差不多满意了,“不过,你再解释解释为什么我不把它们设为public的?”
 
现在我感觉好多了:“因为调用它们的时候必须以一种特定的方式进行。
比如do_write()函数,必须先把类型信息写入,再把对象信息写入,这样读取的时候,
负责生成对象的模块首先能够知道要读出来的对象是什么类型的,然后才能正确地从流中读取对象信息。”
 
“聪明啊,我的小菜鸟!”Wendy停顿了一下,“就跟学习外国口语一样,
学习C++也不光是掌握语法而已,还必须要掌握大量的惯用法。”
 
“是啊是啊,我正打算读Coplien的书...”
 
[译者注:就是James Coplien 1992年的经典著作Advanced C++ Programming Style and Idioms]
 
大虾挥了挥她的手,“冷静,小菜鸟,我不是指先知Coplien的那本书,我是指某种结构背后隐含的惯用法。
比如一个类有virtual destructor,相当于告诉你说:‘嗨,我是一个多态基类,来继承我吧!’
而如果一个类的destructor不是虚拟的,则相当于是在说:‘我不能作为多态基类,看在老天的份上,别继承我。’”
 
“同样的,virtual函数的访问控制级别也具有隐含的意义。一个protected virtual function告诉你:
‘你写的派生类应该,哦,可是说是必须调用我的实现。’而一个private virtual function是在说:
‘派生类可以覆盖,也可以不覆盖我,随你的便。但是你不可以调用我的实现。’”
 
我点点头,告诉她我懂了,然后追问道:“那么public virtual function呢?”
 
“尽可能不要使用public virtual function。”她拿起一支笔写下了以下代码:
 
class HardToExtend
{
public:
        virtual void f();
};
 void HardToExtend::f()
{
       // Perform a specific action
}
“假设你发布了这个类。在写第二版时,需求有所变化,你必须改用Template Method。可是这根本不可能,你知道为什么?”
 
“呃,这个...,不知道。”
 
“由两种可能的办法。其一,将f()的实现代码转移到一个新的函数中,然后将f()本身设为non-virtual的:
 
class HardToExtend
{
// possibly protected
    virtual void do_f();
public:
    void f();
};
void HardToExtend::f()
{
    // pre-processing
    do_f();
    // post-processing
}
void HardToExtend::do_f()
{
    // Perform a specific action
}
然而你原来写的派生类都是企图override函数f()而不是do_f()的,你必须改变所有的派生类实现,
只要你错过了一个类,你的类层次就会染上先知Meyers所说的‘精神分裂的行径’。”
[译者注:参见Scott Meyers,Effective C++, Item 37,绝对不要重新定义继承而来的非虚拟函数]
 
“另一种办法是将f()移到private区域,引入一个新的non-virtual函数:”
 
class HardToExtend
{
// possibly protected
    virtual void f();
public:
    void call_f();
};
“这会导致无数令人头痛的问题。首先,所有的客户都企图调用f()而不是call_f(),现在它们的代码都不能编译了。
更有甚者,大部分派生类都回把f()放在public区域中,这样直接使用派生类的用户可以访问到你本来想保护的细节。”
 
“对待虚函数要象对待数据成员一样,把它们设为private的,直到设计上要求使用更宽松的访问控制再来调整。
要知道由private入public易,由public入private难啊!”
 
[译者注:这篇文章所表达的思想具有一定的颠覆性,因为我们太容易在基类中设置public virtual function了,
Java中甚至专门为这种做法建立了interface机制,现在竟然说这不好!一时间真是接受不了。
但是仔细体会作者的意思,他并不是一般地反对public virtual function,只是在template method大背景下给出上述原则。
虽然这个原则在一般的设计中也是值得考虑的,但是主要的应用领域还是在template method模式中。
当然,template method是一种非常有用和常用的模式,因此也决定了本文提出的原则具有广泛的意义。]
 
 
--------------------------------------------------------------------------------
 
II. Visitor模式
我正在为一个设计问题苦恼。试用期快结束了,我希望自己解决这个问题,来证明自己的进步。
每个人都记得自己的第一份工作吧,也都应该知道在这个时候把活儿做好是多么的重要!
我亲眼看到其他的新雇员没有过完试用期就被炒了鱿鱼,就是因为他们不懂得如何对付那个大虾...,
别误会,我不是说她不好,她是我见过最棒的程序员,可就是有点刻薄古怪...。现在我拜她为师,
不为别的,就是因为我十分希望能达到她那个高度。
 
我想在一个类层次(class hierarchy)中增加一个新的虚函数,但是这个类层次是由另外一帮人维护的,其他人碰都不能碰:
 
class Personnel
{
public:
 virtual void Pay ( /*...*/ ) = 0;
 virtual void Promote( /*...*/ ) = 0;
 virtual void Accept ( PersonnelV& ) = 0;
 // ... other functions ...
};
 
class Officer : public Personnel { /* override virtuals */ };
class Captain : public Officer { /* override virtuals */ };
class First : public Officer { /* override virtuals */ };
我想要一个函数,如果对象是船长(Captain)就这么做,
如果是大副(First Officer)就那么做。Virtual function正是解决之道,
在Personnel或者Officer中声明它,而在Captain和First覆盖(override)它。
 
糟糕的是,我不能增加这么一个虚函数。我知道可以用RTTI给出一个解决方案:
 
void f( Officer &o )
{
 if( dynamic_cast<Captain*>(&o) )
    /* do one thing */
 else if( dynamic_cast<First*>(&o) )
    /* do another thing */
}
 
int main()
{
 Captain k;
 First s;
 f( k );
 f( s );
}
但是我知道使用RTTI是公司编码标准所排斥的行为,我对自己说:
“是的,虽然我以前不喜欢RTTI,但是这回我得改变对它的看法了。很显然,除了使用RTTI,别无它法。”
 
“任何问题都可以通过增加间接层次的方法解决。”
 
我噌地一下跳起来,那是大虾的声音,她不知道什么时候跑到我背后,“啊哟,您吓了我一跳...您刚才说什么?”
 
“任何问...”
 
“是的,我听清楚了,”我也不知道哪来的勇气,居然敢打断她,
“我只是不知道您从哪冒出来的。”其实这话只不过是掩饰我内心的慌张。
 
“哈,算了吧,小菜鸟,”大虾斜着眼看着我,“你以为我不知道你心里想什么!
”她把声音提高了八度,直盯着我,“那些可怜的C语言门徒才会使用switch语句处理不同的对象类型。你看:”
 
/* A not-atypical C program */
void f(struct someStruct *s)
{
 switch(s->type) {
 case APPLE:
    /* do one thing */
    break;
 case ORANGE:
    /* do another thing */
    break;
 /* ... etc. ... */
 }
}
“这些人学习Stroustrup教主的C++语言时,最重要的事情就是学习如何设计好的类层次。”
 
“没错,”我又一次打断她,迫不及待地想让Wendy明白,我还是有两下子的,
“他们应该设计一个Fruit基类,派生出Apple和Orange,用virtual function来作具体的事情。
 
“很好,小菜鸟。C语言门徒通常老习惯改不掉。但是,你应该知道,
通过使用virtual function,你增加了一个间接层次。”她放下笔,“你所需要的不就是一个新的虚函数吗?”
 
“是的。可是我没有权力这么干。”
 
“因为你无权修改类层次,对吧!”
 
“您终于了解了情况,我们没法动它。也不知道这个该死的类层次是哪个家伙设计的...” 我嘀嘀咕咕着。
 
“是我设计的。”
 
“啊...,真的?!这个,嘿嘿...”,我极为尴尬。
 
“这个类层次必须非常稳定,因为有跨平台的问题。但是它的设计允许你增加新的virtual function,
而不必烦劳RTTI。你可以通过增加一个间接层次的办法解决这个问题。请问,Personnel::Accept是什么?”
 
”嗯,这个...”
 
“这个类实现了一个模式,可惜这个模式的名字起得不太好,是个PNP,叫Visitor模式。”
 
[译者注:PNP,Poor-Named Pattern, 没起好名字的模式]
 
“啊,我刚刚读过Visitor模式。但是那只不过是允许若干对象之间相互迭代访问的模式,不是吗?”
 
她叹了一口气,“这是流行的错误理解。那个V,我觉得毋宁说是Visitor,还不如说是Virtual更好。
这个PNP最重要的用途是允许在不改变类层次的前提下,向已经存在的类层次中增加新的虚函数。
首先来看看Personnel及其派生类的Accept实现细节。”她拿起笔写下:
 
void Personnel::Accept( PersonnelV& v )
 { v.Visit( *this ); }
 
void Officer::Accept ( PersonnelV& v )
 { v.Visit( *this ); }
 
void Captain::Accept ( PersonnelV& v )
 { v.Visit( *this ); }
 
void First::Accept ( PersonnelV& v )
 { v.Visit( *this ); }
“Visitor的基类如下:”
 
class PersonnelV/*isitor*/
{
public:
 virtual void Visit( Personnel& ) = 0;
 virtual void Visit( Officer& ) = 0;
 virtual void Visit( Captain& ) = 0;
 virtual void Visit( First& ) = 0;
};
“啊,我记起来了。当我要利用Personnel类层次的多态性时,
我只要调用Personnel::Accept(myVisitorObject)。由于Accept是虚函数,
我的myVisitorObject.Visit()会针对正确的对象类型调用,根据重载法则,
编译器会挑选最贴切的那个Visit来调用。这不相当于增加了一个新的虚拟函数了吗?”
 
“没错,小菜鸟。只要类层次支持Accept,我们就可以在不改动类层次的情况下增加新的虚函数了。”
 
“好了,我现在知道该怎么办了”,我写道:
 
class DoSomething : public PersonnelV
{
public:
 virtual void Visit( Personnel& );
 virtual void Visit( Officer& );
 virtual void Visit( Captain& );
 virtual void Visit( First& );
};
 
void DoSomething::Visit( Captain& c )
{
 if( femaleGuestStarIsPresent )
    c.TurnOnCharm();
 else
    c.StartFight();
}
 
void DoSomething::Visit( First& f )
{
 f.RaiseEyebrowAtCaptainsBehavior();
}
void f( Personnel& p )
{
 p.Accept( DoSomething() ); // 相当于 p.DoSomething()
}
 
int main()
{
 Captain k;
 First s;
 
 f( k );
 f( s );
}
大虾满意地笑了,“也许这个模式换一个名字会更好理解,可惜世事往往不遂人意...”。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值