您的位置:首页 > 编程语言 > C语言/C++

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

2012-06-16 13:35 387 查看

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

分类:
C C++ 程序设计
2011-10-19 22:30
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





{

5

public:

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


42

private:

43

std::vector<ISupportReceiveDestroyMessage *> m_vRelateList;

44

};

45


46

class TriggerBase : public BaseObject,

47

public virtual ISupportReceiveDestroyMessage

48





{

49

public:

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


72

protected:

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


295

private:

296

std::vector<Relationship> m_vRelationships;

297

};

298


299

template <class Param>

300

class Trigger1 : public TriggerBase

301





{

302

public:

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


433

private:

434

std::vector<Channel1Base<Param> *> m_vChannels;

435

};

436


437

template <class Param1, class Param2>

438

class Trigger2 : public TriggerBase

439





{

440

public:

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


574

private:

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

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

刷新评论列表

找优秀程序员,就在博客园

IT新闻:

· Linux Mint将引入Gnome 3

· 分析师预计诺基亚第三季度或净亏3.3亿美元

· 谷歌与新泽西公交合作推广Google Wallet

· 百度获发改委云计算专项最高激励:腾讯阿里其次

· 三星:Galaxy Nexus为回避苹果专利特别设计

分享到:

上一篇:Review of Operating Systems

下一篇:异步消息的传递-回调机制
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: