好吧,又一种C++事件回调封装以及相关的零碎讨论

好吧,又一种C++事件回调封装以及相关的零碎讨论

分类: C C++ 程序设计 23人阅读 评论(0) 收藏 举报

好吧,又一种C++事件回调封装以及相关的零碎讨论

    事件回调机制的实现可能是C++领域里最大众化的代码游戏之一。
    一方面,C++并没有这个机制的语法层支持,这导致了众多商业和开源框架各自实现了风格迥异的事件回调。尤其是GUI方面,MFC提供了一层薄薄的消息映射;ATL用了一个thunk技术(不熟悉的可以google一下),简单的说就是偷偷的把this放到栈上;VCL够凶悍,直接扩充了编译器,提供了一个__closure关键字各种成员函数的指针通吃;QT的signal/slot很俏丽,也够强大……
    另一方面,如果做一个调查,当一个C++使用者比较熟悉C++的一些特性,亲手写过一些程序之后,想亲手封装一些东西,那么他会封装什么?我想stream IO包括socket)、配置文件、日志、内存池、线程、简单的容器以及本次说的事件回调绝对是高频选项。

    随便google或者百度一下,C++爱好者实现的事件回调或者相关论述多到十倍于足以证明我刚才的第二个看法的程度。
    比如这里这里这里,以及 这里

    ……

    如果这些还没让你厌倦,你可以尝试看看下面这个:
1 struct  BaseObject
2 {
3    virtual ~BaseObject(){}
4}
;
1 struct  ISupportReceiveDestroyMessage :  public   virtual  Interface
2 {
3    virtual ~ISupportReceiveDestroyMessage(){}
4    virtual void receiveDestroyMessage(BaseObject *pNotifier) = 0;
5}
;
6
struct  ISupportRelationship :  public   virtual  Interface
{
    
virtual ~ISupportRelationship(){}
    
virtual void regRelationship(ISupportReceiveDestroyMessage *pRelate) = 0;
    
virtual void unregRelationship(ISupportReceiveDestroyMessage *pRelate) = 0;
}

  1 class  CanUseTrigger :  public  BaseObject,
  2                        public   virtual  ISupportReceiveDestroyMessage,
  3                        public   virtual  ISupportRelationship
  4 {
  5public:
  6    virtual ~CanUseTrigger()
  7    {
  8        for(std::vector<ISupportReceiveDestroyMessage *>::iterator it = m_vRelateList.begin();
  9            it != m_vRelateList.end();
 10            it++)
 11        {
 12            (*it)->receiveDestroyMessage(this);
 13        }

 14    }

 15
 16    virtual void regRelationship(ISupportReceiveDestroyMessage *pRelate)
 17    {
 18        std::vector<ISupportReceiveDestroyMessage *>::iterator it = 
 19            std::find(m_vRelateList.begin(), m_vRelateList.end(), pRelate);
 20        
 21        if (it == m_vRelateList.end())
 22        {
 23            m_vRelateList.push_back(pRelate);
 24        }

 25    }

 26    
 27    virtual void unregRelationship(ISupportReceiveDestroyMessage *pRelate)
 28    {
 29        m_vRelateList.erase(std::remove(m_vRelateList.begin(), m_vRelateList.end(), pRelate),
 30                            m_vRelateList.end());
 31    }

 32
 33    virtual void receiveDestroyMessage(BaseObject *pNotifier)
 34    {
 35        ISupportReceiveDestroyMessage *pTmp = dynamic_cast<ISupportReceiveDestroyMessage *>(pNotifier);
 36        if (pTmp)
 37        {
 38            unregRelationship(pTmp);
 39        }

 40    }

 41
 42private:
 43    std::vector<ISupportReceiveDestroyMessage *> m_vRelateList;
 44}
;
 45
 46 class  TriggerBase :  public  BaseObject,
 47                      public   virtual  ISupportReceiveDestroyMessage
 48 {
 49public:
 50    virtual ~TriggerBase()
 51    {
 52        for (std::vector<Relationship>::iterator it =  m_vRelationships.begin();
 53             it != m_vRelationships.end();
 54             it++)
 55        {
 56            it->pOther->receiveDestroyMessage(this);
 57        }

 58    }

 59
 60    virtual void receiveDestroyMessage(BaseObject *pNotifier)
 61    {
 62        CanUseTrigger *pTmp = dynamic_cast<CanUseTrigger *>(pNotifier);
 63        if (!pTmp)
 64        {
 65            return;
 66        }

 67
 68        m_vRelationships.erase(std::remove(m_vRelationships.begin(), m_vRelationships.end(), Relationship(pTmp)),
 69                               m_vRelationships.end());
 70    }

 71        
 72protected:
 73    void regToUser(CanUseTrigger *pUser)
 74    {
 75        std::vector<Relationship>::iterator itUser
 76            = std::find(m_vRelationships.begin(), m_vRelationships.end(), Relationship(pUser));
 77            
 78        if (m_vRelationships.end() != itUser)
 79        {
 80            ++(itUser->RefCount);
 81        }

 82        else
 83        {
 84            m_vRelationships.push_back(Relationship(pUser));
 85            m_vRelationships[m_vRelationships.size() - 1].RefCount++;
 86            pUser->regRelationship(this);
 87        }

 88    }

 89
 90    void unregFormUser(CanUseTrigger *pUser)
 91    {
 92        std::vector<Relationship>::iterator itUser
 93            = std::find(m_vRelationships.begin(), m_vRelationships.end(), Relationship(pUser));
 94
 95        if (m_vRelationships.end() != itUser)
 96        {
 97            --(itUser->RefCount);
 98            if(itUser->RefCount < 1)
 99            {
100                m_vRelationships.erase(itUser);
101                pUser->unregRelationship(this);
102            }

103        }

104    }

105
106    struct Relationship
107    {
108        CanUseTrigger *pOther;
109        i32_t RefCount;
110        Relationship(CanUseTrigger *pUser) : pOther(pUser), RefCount(0){}
111
112        Relationship &operator=(const Relationship &rhs)
113        {
114            pOther = rhs.pOther;
115            RefCount = rhs.RefCount;
116
117            return *this;
118        }

119        bool operator==(const Relationship &rhs)
120        {
121            return pOther == rhs.pOther;
122        }

123        bool operator==(const CanUseTrigger *rhs)
124        {
125            return pOther == rhs;
126        }

127    }
;
128
129    template <class P>
130    struct Channel1Base
131    {
132        typedef struct{} is_member_t;
133        typedef struct{} is_not_member_t;
134    
135        virtual ~Channel1Base(){}
136        virtual void invoke(P p) = 0;
137        virtual bool equal(Channel1Base<P> *pOther) = 0;
138        virtual bool isOwner(CanUseTrigger *pCandidate) = 0;
139    }
;
140
141    template <class P1, class P2>
142    struct Channel2Base
143    {
144        typedef struct{} is_member_t;
145        typedef struct{} is_not_member_t;
146
147        virtual ~Channel2Base(){}
148        virtual void invoke(P1 p1, P2 p2) = 0;
149        virtual bool equal(Channel2Base<P1, P2> *pOther) = 0;
150        virtual bool isOwner(CanUseTrigger *pCandidate) = 0;
151    }
;
152
153    template <class P>
154    struct NakedChannel1 : public Channel1Base<P>
155    {
156        typedef Channel1Base<P>::is_not_member_t member_spec_t;
157        typedef void (* method_t)(P);
158
159        NakedChannel1(method_t pMethod) : m_pMethod(pMethod){}
160        ~NakedChannel1(){}
161        virtual void invoke(P p)
162        {
163            if (m_pMethod)
164            {
165                m_pMethod(p);
166            }

167        }

168
169        virtual bool equal(Channel1Base<P> *pOther)
170        {
171            NakedChannel1<P> *pTmp = dynamic_cast<NakedChannel1<P> *>(pOther);
172            if (!pTmp)
173            {
174                return false;
175            }

176
177            return m_pMethod == pTmp->m_pMethod;
178        }

179
180        virtual bool isOwner(CanUseTrigger *pCandidate)
181        {
182            return false;
183        }

184
185        method_t m_pMethod;
186    }
;
187
188    template <class T, class P>
189    struct MemberChannel1 : public Channel1Base<P>
190    {
191        typedef Channel1Base<P>::is_member_t member_spec_t;
192        typedef void (T:: *method_t)(P);
193
194        MemberChannel1(T *pUser, method_t pMethod) : m_pOwner(pUser), m_pMethod(pMethod){}
195        ~MemberChannel1(){}
196        virtual void invoke(P p)
197        {
198            if (m_pOwner && m_pMethod)
199            {
200                (m_pOwner->* m_pMethod)(p);
201            }

202        }

203
204        virtual bool equal(Channel1Base<P> *pOther)
205        {
206            MemberChannel1<T, P> *pTmp = dynamic_cast<MemberChannel1<T, P> *>(pOther);
207            if(!pTmp)
208            {
209                return false;
210            }

211
212            return (m_pOwner == pTmp->m_pOwner) && (m_pMethod == pTmp->m_pMethod);
213        }

214
215        virtual bool isOwner(CanUseTrigger *pCandidate)
216        {
217            return m_pOwner == pCandidate;
218        }

219
220        T *m_pOwner;
221        method_t m_pMethod;
222    }
;
223
224    template <class P1, class P2>
225    struct NakedChannel2 : public Channel2Base<P1, P2>
226    {
227        typedef Channel2Base<P1, P2>::is_not_member_t member_spec_t;
228        typedef void (* method_t)(P1, P2);
229    
230        NakedChannel2(method_t pMethod) : m_pMethod(pMethod){}
231        ~NakedChannel2(){}
232        virtual void invoke(P1 p1, P2 p2)
233        {
234            if (m_pMethod)
235            {
236                m_pMethod(p1, p2);
237            }

238        }

239
240        virtual bool equal(Channel2Base<P1, P2> *pOther)
241        {
242            NakedChannel2<P1, P2> *pTmp = dynamic_cast<NakedChannel2<P1, P2> *>(pOther);
243            if (!pTmp)
244            {
245                return false;
246            }

247            
248            return m_pMethod == pTmp->m_pMethod;
249        }

250
251        virtual bool isOwner(CanUseTrigger *pCandidate)
252        {
253            return false;
254        }

255
256        method_t m_pMethod;
257    }
;
258    
259    template <class T, class P1, class P2>
260    struct MemberChannel2 : public Channel2Base<P1, P2>
261    {
262        typedef Channel2Base<P1, P2>::is_member_t member_spec_t;
263        typedef void (T:: *method_t)(P1, P2);
264
265        MemberChannel2(T *pUser, method_t pMethod) : m_pOwner(pUser), m_pMethod(pMethod){}
266        ~MemberChannel2(){}
267        virtual void invoke(P1 p1, P2 p2)
268        {
269            if (m_pOwner && m_pMethod)
270            {
271                (m_pOwner->* m_pMethod)(p1, p2);
272            }
            
273        }

274
275        virtual bool equal(Channel2Base<P1, P2> *pOther)
276        {
277            MemberChannel2<T, P1, P2> *pTmp = dynamic_cast<MemberChannel2<T, P1, P2> *>(pOther);
278            if (!pTmp)
279            {
280                return false;
281            }

282
283            return (m_pOwner == pTmp->m_pOwner) && (m_pMethod == pTmp->m_pMethod);
284        }

285
286        virtual bool isOwner(CanUseTrigger *pCandidate)
287        {
288            return m_pOwner == pCandidate;
289        }

290        
291        T *m_pOwner;
292        method_t m_pMethod;
293    }
;
294    
295private:
296    std::vector<Relationship> m_vRelationships;
297}
;
298
299 template  < class  Param >
300 class  Trigger1 :  public  TriggerBase
301 {
302public:
303    ~Trigger1()
304    {
305        for (std::vector<Channel1Base<Param> *>::iterator it =  m_vChannels.begin();
306            it != m_vChannels.end();
307            it++)
308        {
309            delete (*it);
310        }

311    }

312
313    virtual void receiveDestroyMessage(BaseObject *pNotifier)
314    {
315        CanUseTrigger *pTmp = dynamic_cast<CanUseTrigger *>(pNotifier);
316        if (!pTmp)
317        {
318            return;
319        }

320
321        for (std::vector<Channel1Base<Param> *>::iterator it =  m_vChannels.begin();
322             it != m_vChannels.end();
323             )
324        {
325            CanUseTrigger *pTmp = dynamic_cast<CanUseTrigger *>(pNotifier);
326            if (pTmp && (*it)->isOwner(pTmp))
327            {
328                it = m_vChannels.erase(it);
329            }

330            else
331            {
332                ++it;
333            }

334        }

335
336        TriggerBase::receiveDestroyMessage(pNotifier);
337    }

338
339    void fire(Param p)
340    {
341        for (std::vector<Channel1Base<Param> *>::iterator it =  m_vChannels.begin();
342             it != m_vChannels.end();
343             it++)
344        {
345            (*it)->invoke(p);
346        }

347    }

348
349    void add(void (* pMethod)(Param))
350    {
351        Channel1Base<Param> *pChannel = new NakedChannel1<Param>(pMethod);
352        std::vector<Channel1Base<Param> *>::iterator it = m_vChannels.begin();
353        
354        for (; it != m_vChannels.end(); it++)
355        {
356            if ((*it)->equal(pChannel))
357            {
358                break;
359            }
            
360        }

361
362        if (it != m_vChannels.end())
363        {
364            m_vChannels.push_back(pChannel);
365        }

366    }

367
368    template <class TUser>
369    void add(TUser *pUser, void (TUser:: *pMethod)(Param))
370    {
371        //assert(dynamic_cast<CanUseTrigger *>(pUser));
372        
373        Channel1Base<Param> *pChannel = new MemberChannel1<TUser, Param>(pUser, pMethod);
374        std::vector<Channel1Base<Param> *>::iterator it = m_vChannels.begin();
375        
376        for (; it != m_vChannels.end(); it++)
377        {
378            if ((*it)->equal(pChannel))
379            {
380                break;
381            }
            
382        }

383
384        if (it == m_vChannels.end())
385        {
386            m_vChannels.push_back(pChannel);
387            regToUser(pUser);
388        }

389    }

390    
391    void dec(void (* pMethod)(Param))
392    {
393        NakedChannel1<Param> TempChannel(pMethod);
394        std::vector<Channel1Base<Param> *>::iterator it = m_vChannels.begin();
395        
396        for (; it != m_vChannels.end(); it++)
397        {
398            if ((*it)->equal(&TempChannel))
399            {
400                break;
401            }
            
402        }

403
404        if (it != m_vChannels.end())
405        {
406            m_vChannels.erase(it);
407        }

408    }

409
410    template <class TUser>
411    void dec(TUser *pUser, void (TUser:: *pMethod)(Param))
412    {
413        std::assert(dynamic_cast<CanUseTrigger *>(pUser));
414        
415        MemberChannel1<TUser, Param> TempChannel(pMethod);
416        std::vector<Channel1Base<Param> *>::iterator it = m_vChannels.begin();
417        
418        for (; it != m_vChannels.end(); it++)
419        {
420            if ((*it)->equal(&TempChannel))
421            {
422                break;
423            }
            
424        }

425
426        if (it != m_vChannels.end())
427        {
428            m_vChannels.erase(it);
429            
430        }

431    }

432
433private:
434    std::vector<Channel1Base<Param> *> m_vChannels;
435}
;
436
437 template  < class  Param1,  class  Param2 >
438 class  Trigger2 :  public  TriggerBase
439 {
440public:
441    ~Trigger2()
442    {
443        for (std::vector<Channel2Base<Param1, Param2> *>::iterator it =  m_vChannels.begin();
444            it != m_vChannels.end();
445            it++)
446        {
447            delete (*it);
448        }

449    }

450
451    virtual void receiveDestroyMessage(BaseObject *pNotifier)
452    {
453        CanUseTrigger *pTmp = dynamic_cast<CanUseTrigger *>(pNotifier);
454        if (!pTmp)
455        {
456            return;
457        }

458
459        for (std::vector<Channel2Base<Param1, Param2> *>::iterator it =  m_vChannels.begin();
460             it != m_vChannels.end();
461             )
462        {
463            CanUseTrigger *pTmp = dynamic_cast<CanUseTrigger *>(pNotifier);
464            if (pTmp && (*it)->isOwner(pTmp))
465            {
466                it = m_vChannels.erase(it);
467            }

468            else
469            {
470                ++it;
471            }

472        }

473
474        TriggerBase::receiveDestroyMessage(pNotifier);
475    }

476
477    void fire(Param1 p1, Param2 p2)
478    {
479        for (std::vector<Channel2Base<Param1, Param2> *>::iterator it
480                 =  m_vChannels.begin();
481             it != m_vChannels.end();
482             it++)
483        {
484            (*it)->invoke(p1, p2);
485        }

486    }

487
488    void add(void (* pMethod)(Param1, Param2))
489    {
490        Channel2Base<Param1, Param2> *pChannel
491            = new NakedChannel2<Param1, Param2>(pMethod);
492        std::vector<Channel2Base<Param1, Param2> *>::iterator it = m_vChannels.begin();
493        
494        for (; it != m_vChannels.end(); it++)
495        {
496            if ((*it)->equal(pChannel))
497            {
498                break;
499            }
            
500        }

501
502        if (it == m_vChannels.end())
503        {
504            m_vChannels.push_back(pChannel);
505        }

506    }

507
508    template <class TUser>
509    void add(TUser *pUser, void (TUser:: *pMethod)(Param1, Param2))
510    {
511        //std::assert(dynamic_cast<CanUseTrigger *>(pUser));
512        
513        Channel2Base<Param1, Param2> *pChannel
514            = new MemberChannel2<TUser, Param1, Param2>(pUser, pMethod);
515        std::vector<Channel2Base<Param1, Param2> *>::iterator it = m_vChannels.begin();
516        
517        for (; it != m_vChannels.end(); it++)
518        {
519            if ((*it)->equal(pChannel))
520            {
521                break;
522            }
            
523        }

524
525        if (it == m_vChannels.end())
526        {
527            m_vChannels.push_back(pChannel);
528            regToUser(pUser);
529        }

530    }

531    
532    void dec(void (* pMethod)(Param1, Param2))
533    {
534        NakedChannel2<Param1, Param2> TempChannel(pMethod);
535        std::vector<Channel2Base<Param1, Param2> *>::iterator it = m_vChannels.begin();
536        
537        for (; it != m_vChannels.end(); it++)
538        {
539            if ((*it)->equal(&TempChannel))
540            {
541                break;
542            }
            
543        }

544
545        if (it != m_vChannels.end())
546        {
547            m_vChannels.erase(it);
548        }

549    }

550
551    template <class TUser>
552    void dec(TUser *pUser, void (TUser:: *pMethod)(Param1, Param2))
553    {
554        std::assert(dynamic_cast<CanUseTrigger *>(pUser));
555
556        MemberChannel2<TUser, Param1, Param2> TempChannel(pMethod);
557        std::vector<Channel2Base<Param1, Param2> *>::iterator it = m_vChannels.begin();
558        
559        for (; it != m_vChannels.end(); it++)
560        {
561            if ((*it)->equal(&TempChannel))
562            {
563                break;
564            }
            
565        }

566
567        if (it != m_vChannels.end())
568        {
569            m_vChannels.erase(it);
570            
571        }

572    }

573
574private:
575    std::vector<Channel2Base<Param1, Param2> *> m_vChannels;
576}
;
577

一些讨论:
1. 为了防止对象析构后其方法被调用,我采用了一个公用的基类实现析构之前的互相通知,这要求相关的类都要继承自CanUseTrigger类,这显然是个“不情之请”,人家本来就有公用基类怎么办?

2. 使用这个机制的代码大概是这样:
class Foo : public CanUseTrigger
{
public:
    void doSomething(int i){/*...*/}
};

class Bar
{
public:
   Trigger<int> OnSomeEvent;
    void someEvent()
    {
        this->OnSomeEvent.fire(0);
    }
};

int main()
{
    Foo f;
    Bar b;
    b.OnSomeEvent.add(&f, &Foo::doSomething); 
    b.someEvent();
}

注意绿色的那行,我要是不知道f的类型怎么办?这种情况在OO编程中太常见了。事实上大部分基于模板的解决方式(至少是我见过的)都存在这个问题。怎么解决?我也不知道,maybe, We need typeof.

4. 真实世界中的事件回调,还有一个强大的boost::slot,连仿函数都封装进去了。

5. 各种C++事件回调机制中,VCL的方法可能是最方便的,但是只有borland的编译器才能识别__closure关键字。MFC的(其实就是windows的)方法可能是最灵活的,你可以轻松的实现一个线程到另一个线程的回调,这有时候非常有用,尤其是需要将现成的库中异步调用转换成同步调用时。

posted on 2009-07-26 22:59 欲三更 阅读(1881) 评论(11)  编辑 收藏 引用

<!---->

评论

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论[未登录] 2009-07-27 12:18 Chen Jiecao

这种折叠式的代码怎么搞?
不知道cppblog该怎么设置  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-27 13:56 欲三更

@Chen Jiecao
不是有“插入代码”那个按钮吗?  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-27 21:00 CY

你提到的第1点,在你代码中,是不是由那个CanUseTrigger类里面提到的 m_vRelateList进行管理的,是一个静态成员对象,放了所有绑定的内容列表?然后有对象析构时,就自动检查列表的内容?  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-28 04:47 欲三更

@CY
哪有static对象啊?是std::,看花眼了吧?
  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-28 04:49 欲三更

@CY
简单的说就是Trigger和CanUseTrigger都保存着与它发生关系的对象列表,然后析构时就通知对方,就是这样。  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-28 10:21 CY

哦,明白了。
之前想过要自动解除绑定,就需要一种管理类,管理类不要被使用者看到,就实现在基类中,用一个静态容器。
看你代码到看到一点这样的迹象,以为和我想的一样就没有继续看了~  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-28 16:17 没意思

VC里面可以用__hook关键字,委托在编译器层面就是很容易实现的事情
建议看看fastdelegate和functor,楼主这个版本,额。。。  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-28 20:28 欲三更

@没意思
我看过fastdelegate,包括作者那篇文章。
这方面各种各样的实现有,从boost的大一统封装,到跟C++基本上已经没有关系的thunk技术。我写这个东西的出发点主要就是不用高级复杂的技术和编译器特性,尽量在直白简单的层次上实现这个功能,至于成品的成色...我工作中用C++ Builder,有__closure关键字可用。
而且就纯C++内部来说,我倾向于“委托”这个东西接收的应该是个完成特定功能的接口指针而不是符合某种签名函数。  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-29 17:26 DraZet

崩溃了,这么长的代码一行注释都没有,看得难受,建议把注释加上去,方便大家理解  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论 2009-07-31 16:56 yisa

恩 做法还是蛮通用的
建议:
1. del监听代价太大, 应该用双向链表来自动提供节点解除
2. 可以强化Channel的设计, 减少消息发送者的内容

比如:
Struct CallbackNode
{
~CallbackNode(){Detach();}
void Detach(){...自动从自己挂接的地方(一个双向链表)中移除...}
virtural void Exec()= 0;
Callee* _callee;
Func _callbackFunc;
额外信息: 需要记录自己在双向链表中的前后节点
}

typedef doubleList<CallbackNode> _DList;

class ViCaller
{
void Invoke();
DList _channel;
}

每个CallbackNode可以交给每个监听者callee去管理,
如果 caller先析构, DList 可以自动解掉DList 的Node
如果 监听者callee先析构, Node需要被析构, 这样自动从DList 解除, 自然更不会发生回调

在下愚见
QQ 348360855 yisa
  回复  更多评论   

# re: 好吧,又一种C++事件回调封装以及相关的零碎讨论[未登录] 2009-08-03 14:32 欲三更

@yisa
嗯,这个做法确实避免了“关系”的冗余。  回复  更多评论   

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值