使用 .NET 对事件进行编程
作者:Ted Pattison |
您可能已经对事件进行编程若干年了,但是迁移到 .NET Framework 仍然需要您重新检查事件的内部工作,因为 .NET Framework 中的事件位于委托的顶层。 对委托的了解越多,对事件进行编程时所具有的驾驭能力越强。 开始使用公共语言运行库 (CLR) 的某个事件驱动框架(例如 Windows® Forms 或 ASP.NET)时,理解事件在较低的级别如何工作至关重要。 本月我的目标是使您理解事件在较低的级别如何工作。 什么是事件? 事件是一种形式化的软件模式,在该模式中,通知源将对一个或多个处理程序方法进行回调。 因此,事件类似于接口和委托,因为它们提供了设计使用回调方法的应用程序的方法。 但是,事件极大地提高了工作效率,因为它们使用起来比接口或委托更容易。 事件允许编译器和 Visual Studio® .NET IDE 在幕后为您做大量的工作。 涉及事件的设计基于事件源和一个或多个事件处理程序。 事件源可以是一个类也可以是一个对象。 事件处理程序是绑定到处理程序方法的委托对象。 图 1 显示了绑定到其处理程序方法的事件源的高级别视图。 ![]() 图 1 事件源和处理程序 每个事件都是根据特定的委托类型定义的。 对于事件源定义的每个事件,有一个基于事件的基础委托类型的私有字段。 该字段用于跟踪多路广播委托对象。 事件源还提供允许您注册所需数量的事件处理程序的公用注册方法。 当您创建事件处理程序(委托对象)并在事件源中注册它时,事件源只是将新的事件处理程序追加到列表的结尾。 然后,事件源可以使用私有字段在多路广播委托上调用 Invoke,该多路广播委托将依次执行所有注册的事件处理程序。 事件的真正的妙处在于对其进行设置的大量工作都已经为您做好了。 正如您很快就会看到的,无论任何时候您定义事件时,Visual Basic® .NET 编译器都会通过自动添加私有委托字段和公用注册方法帮助您工作。 您还将看到 Visual Studio .NET 可以通过代码生成器提供更多的帮助,代码生成器可以自动发出适用于您的处理程序方法的主干定义。 对事件进行编程 由于 .NET 中的事件建立在委托的顶层,因此它们的基础的管道详细信息与较低版本的 Visual Basic 中所一直使用的截然不同。 但是,Visual Basic .NET 的语言设计者们在保持事件编程的语法与较低版本的 Visual Basic 一致方面做得很好。 在很多情况下,对事件进行编程涉及的语法与您习惯使用的熟悉的老语法相同。 例如,您将使用 Event、RaiseEvent 和 WithEvents 等关键字,而它们的行为方式与其在较低版本的 Visual Basic 中的行为方式几乎完全相同。 让我们通过创建一个基于事件的简单的回调设计开始。 首先,我需要通过使用 Event 关键字在类定义内定义一个事件。 必须根据特定的委托类型定义每个事件。 下面是定义自定义委托类型和用来定义事件的类的一个示例: Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal) Class BankAccount Public Event LargeWithdraw As LargeWithdrawHandler '*** other members omitted End Class 在本示例中,LargeWithdraw 事件被定义为实例成员。 在本设计中,BankAccount 对象将充当事件源。 如果希望类而不是对象充当事件源,应该使用 Shared 关键字将事件定义为共享成员。 对事件进行编程时,知道编译器在幕后为您做了大量额外的工作这一点很重要。 例如,当您将我刚才向给您看过的 BankAccount 类的定义编译到程序集时,您认为编译器会做什么? 图 2 显示了在中间语言反汇编程序 ILDasm.exe 中检查生成的类定义时,该定义是什么样的。 该视图毫无保留地向您显示了 Visual Basic .NET 编译器在幕后做了多少工作来帮助您。 ![]() 图 2 ILDasm 中的类定义 当您定义事件时,编译器在类定义内生成四个成员。 第一个成员是基于委托类型的私有字段。 该字段用于跟踪对委托对象的引用。 编译器通过采用事件本身的名称并添加后缀“Event”生成该私有字段的名称。 这意味着创建名为 LargeWithdraw 的事件将导致创建名为 LargeWithdrawEvent 的私有字段。 编译器还生成两个方法,帮助注册和注销将成为事件处理程序的委托对象。 这两个方法使用标准的命名规则进行命名。 用于注册事件处理程序的方法使用事件的名称,并带有前缀“add_”。 用于注销事件处理程序的方法使用事件的名称,并带有前缀“remove_”。 因此,为 LargeWithdraw 事件创建的两个方法名为 add_LargeWithdraw 和 remove_LargeWithdraw。 Visual Basic .NET 编译器通过调用 Delegate 类的 Combine 方法为将委托对象作为参数接受并将其添加到处理程序列表中的 add_LargeWithdraw 生成一个实现。 编译器通过在 Delegate 类中调用 Remove 方法为从列表中删除一个处理程序方法的 remove_LargeWithdraw 生成一个实现。 第四个也是最后一个添加到类定义中的成员是表示事件本身的成员。 在图 2 中,您应该能够找到名为 LargeWithdraw 的事件成员。 它是旁边带有一个倒三角的成员。 但是,您应该注意到,该事件并不象其它三个成员一样真的是一个物理成员。 相反,它是一个仅包含元数据的成员。 此仅包含元数据的事件成员很有价值,因为它可以向该类支持的编译器和其他开发工具通知 .NET Framework 中事件注册的标准模式。 该事件成员还包含注册方法和注销方法的名称。 这使得 Visual Basic .NET 和 C# 等托管语言的编译器可以在编译时查找注册方法的名称。 Visual Studio .NET 是查找此仅包含元数据的事件成员的开发工具的另一个很好的示例。 当 Visual Studio .NET 发现类定义包含事件时,它将自动生成处理程序方法的主干定义以及将它们作为事件处理程序进行注册的代码。 在开始讨论激发事件之前,我想提出一个有关用于定义事件的委托类型的限制。 用于定义事件的委托类型不能有返回值。 您必须使用 Sub 关键字而不是 Function 关键字定义委托类型,如下所示: '*** can be used for events Delegate Sub BaggageHandler() Delegate Sub MailHandler(ItemID As Integer) '*** cannot be used for events Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String 对此限制有很充分的原因。 当涉及与若干处理程序方法绑定的多路广播委托时,处理返回值相当困难。 在多路广播委托上调用 Invoke 返回与调用列表中的最后一个处理程序方法相同的值。 但是,捕获较早在列表中出现的处理程序方法的返回值并不那么简单。 不需要捕获多个返回值只会使事件更容易使用。 激发事件 现在,让我们修改 BankAccount 类使其在提款数量超出 $5000 阈值时能够激发一个事件。 激发 LargeWithdraw 事件的最简单的方法是在一个方法、属性或构造函数的实现中使用 RaiseEvent 关键字。 您可能会觉得该语法很熟悉,因为它类似于您在较低版本的 Visual Basic 中使用的语法。 下面是从 Withdraw 方法激发 LargeWithdraw 事件的一个示例: Class BankAccount Public Event LargeWithdraw As LargeWithdrawHandler Sub Withdraw(ByVal Amount As Decimal) '*** send notifications if required If (Amount > 5000) Then RaiseEvent LargeWithdraw(Amount) End If '*** perform withdrawal End Sub End Class |
RaiseEvent LargeWithdraw(Amount)
Visual Basic .NET 编译器将此表达式扩展为在保留多路广播委托对象的私有字段上调用 Invoke 的代码。 换句话说,使用 RaiseEvent 关键字与在以下 snippet 中编写代码具有完全相同的效果:
If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
注意,Visual Basic .NET 编译器生成的代码执行检查以确保 LargeWithdrawEvent 字段包含对某个对象的有效引用。 这是因为 LargeWithdrawEvent 字段的值在第一个处理程序方法注册之前一直为 Nothing。 因此,除非当前至少有一个处理程序方法已注册,否则生成的代码并不尝试调用 Invoke。
您应该能够对激发事件进行观察。 使用 RaiseEvent 关键字或者根据编译器自动生成的 LargeWithdrawEvent 私有字段直接进行编程通常并没有什么分别。 两种方法都生成相同的代码:
'*** this code RaiseEvent LargeWithdraw(Amount) '*** is the same as this code If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
在很多情况下,您可能喜欢使用 RaiseEvent 关键字语法,因为它需要的键入较少,生成的代码较简洁。 但是,在某些情况下,当您需要较多控制时,根据 LargeWithdrawEvent 私有字段进行明确编程可能会有意义。 让我们看一个这种情况的示例。
想象以下情况:BankAccount 对象有三个事件处理程序已注册以接收 LargeWithdraw 事件的通知。 如果使用 RaiseEvent 关键字触发事件并且调用列表中的第二个事件处理程序出现异常,将会出现什么情况? 包含 RaiseEvent 语句的代码行将接收运行时异常,但是您可能没办法确定哪个事件处理程序导致异常。 而且,可能没有办法处理第二个事件处理程序导致的异常,也没有办法按预期方式在执行第三个事件处理程序的位置继续进行。
但是,如果您愿意根据 LargeWithdrawEvent 私有字段进行编程,则可以更适当的方式处理事件处理程序导致的异常。 检查图 3 中的代码。 正如您所看到的,降至一个较低的级别并根据该私有委托字段进行编程可以提供额外程度的控制。 您可以恰当地处理异常,然后继续执行较晚出现在列表中的事件处理程序。 与 RaiseEvent 语法相比,该方法具有明显的好处,在该方法中一个事件处理程序导致的异常将阻止执行较晚出现在调用列表中的任何事件处理程序。
创建和注册事件处理程序
现在,您已经知道如何定义和激发事件,该是讨论如何创建事件处理程序并在给定源中注册它的时候了。 有两种不同的方法可以在 Visual Basic .NET 中完成以上操作。 第一种方法称为动态事件绑定,涉及 AddHandler 关键字的使用。 第二种方法称为静态事件绑定,涉及熟悉的 Visual Basic 关键字 WithEvents 的使用。 我打算在下一期讨论静态事件绑定。 所以现在,让我们来看一看动态事件绑定的工作原理。
请记住,事件处理程序是一个委托对象。 因此,可以通过从事件所基于的委托类型实例化一个委托对象,创建一个事件处理程序。 创建此委托对象时,必须将其绑定到要成为事件处理程序的目标处理程序方法。
创建事件处理程序后,必须通过在事件源上调用特定的注册方法在特定的事件中注册它。 回忆一下,LargeWithdraw 事件的注册方法名为 add_LargeWithdraw。 当您调用 add_LargeWithdraw 方法并将委托对象作为参数传递时,事件源将委托对象添加到将接收事件通知的事件处理程序列表中。
有关事件注册会出现混淆的是您从未直接调用 add_LargeWithdraw 等方法。 实际上,如果您按名称访问事件注册方法,Visual Basic .NET 编译器将生成编译时错误。 但是,您可以使用包括 AddHandler 语句的替代语法。 当您使用 AddHandler 语句时,Visual Basic .NET 编译器生成为您调用事件注册方法的代码。
让我们来看一个使用动态事件注册绑定几个事件处理程序的示例。 想象您已经在 AccountHandlers 类中编写了以下共享方法的集合:
Class AccountHandlers Shared Sub LogWithdraw(ByVal Amount As Decimal) '*** write withdrawal info to log file End Sub Shared Sub GetApproval(ByVal Amount As Decimal) '*** block until manager approval End Sub End Class
如果要将这些方法用作 BankAccount 类的 LargeWithdraw 事件的事件处理程序,您应该做什么? 让我们从创建绑定到处理程序 LogWithdraw 的事件处理程序开始。 首先,您必须创建将成为事件处理程序的委托对象:
Dim handler1 As LargeWithdrawHandler handler1 = AddressOf AccountHandlers.LogWithdraw
然后,您必须使用 AddHandler 语句在事件源中注册该新的委托对象。 当您使用 AddHandler 语句注册事件处理程序时,您需要传递两个参数,类似以下内容:
AddHandler <event>, <delegate object>
AddHandler 需要的第一个参数是对类或对象的事件进行求值的表达式。 第二个参数是对将被绑定为事件处理程序的委托对象的引用。 下面是使用 AddHandler 语句在 BankAccount 对象的 LargeWithdraw 事件中注册事件处理程序的一个示例:
'*** create bank account object Dim account1 As New BankAccount() '*** create and register event handler Dim handler1 As LargeWithdrawHandler handler1 = AddressOf AccountHandlers.LogWithdraw AddHandler account1.LargeWithdraw, handler1
当您使用 AddHandler 关键字注册 LargeWithdraw 事件的事件处理程序时,Visual Basic .NET 编译器将扩展此代码以调用注册方法 add_LargeWithdraw。 执行包含 AddHandler 语句的代码后,您的事件处理程序已就位,并已准备就绪可以进行通知。 因此,无论任何时候 BankAccount 对象激发 LargeWithdraw 事件时,都将执行 LogWithdraw 方法。
在上一示例中,我使用了较长形式的语法以便确切地说明创建和注册事件处理程序时所发生的事情。 但是,明白了原理之后,您可能希望使用更简洁的语法来实现同样的目标,如下所示:
'*** create bank account object Dim account1 As New BankAccount() '*** register event handlers AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.GetApproval
由于 AddHandler 语句期望将委托对象作为第二个参数引用,因此您可以使用 AddressOf 运算符的速记语法,后跟目标处理程序方法。 当 Visual Basic .NET 编译器发现这种情况后,它就会生成额外的代码以创建将成为事件处理程序的委托对象。
Visual Basic .NET 语言的 AddHandler 语句由 RemoveHandler 语句补充。 RemoveHandler 需要的两个参数与 AddHandler 相同,但是它具有相反的效果。 它通过调用事件源提供的 remove_LargeWithdraw 方法从已注册处理程序列表中删除目标处理程序方法:
Dim account1 As New BankAccount() '*** register event handler AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw '*** unregister event handler RemoveHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
现在,您已经看到了使用事件实现回调设计所需的所有步骤。上面的代码显示了一个完整的应用程序,在该应用程序中两个事件处理程序已注册以接收来自 BankAccount 对象的 LargeWithdraw 事件的回调通知。
小结
虽然使用事件的动机和某些语法与较低版本的 Visual Basic 相比都没有变,但是,您必须承认现在情况有些不同了。 正如您所看到的,您对如何响应事件的控制能力比以前更强了。 如果您希望降低级别并根据委托进行编程,则更是如此。
在下一期的 Basic Instincts 栏目中,我打算继续有关事件的此讨论。 我将向您说明 Visual Basic .NET 如何通过您熟悉的 WithEvents 关键字语法支持静态事件绑定,并将讨论 Handles 子句。 要真正控制事件,您必须能够轻松驾驭动态事件注册和静态事件注册。
相关文章:

tomcat项目自动发布脚本.脚本运行效果
./update -------------------------------------------------------------- | 确定特定用户执行,否则退出 | -------------------------------------------------------------- Yes,we are the user of testtomcat --------------------------------------------------…
11.11大促来袭,京东如何保障云安全?
2020年4月,国家发改委首次就“新基建”概念作出正式解释。云计算被纳入信息基础设施中的新技术基础设施。据IDC统计,2019年全球云计算基础设施规模超过传统IT基础设施,占全球IT基础设施的50%以上。云计算发展势如破笋,云计算也成为…

分享Kali Linux 2017年第11周镜像文件
分享Kali Linux 2017年第11周镜像文件 Kali?Linux官方于3月12日发布2017年的第11周镜像。这次维持了11个镜像文件的规模。默认的Gnome桌面的4个镜像,E17、KDE、LXDE、MATE、XFCE桌面的各一个,手机版的包括ARMEL和ARMHF。有最近要安装Kali?Linux系统的&…

实现无刷新DropDownList联动效果
在做一个文章添加功能时,想在选择大类后,自动将其所属二级小类显示出来,使用DropDownList的SelectedIndexChanged事件可以很容易实现,但每次选择后页面总要刷新一次,让人感觉很不爽。为实现DropDownList无刷新二级联动…
偷天换日,逼真的天空置换算法
责编 | 晋兆雨来源 | Jack Cui头图 | CSDN付费下载于视觉中国前言天空,是摄像中的一个关键元素。游戏的天空,我们可以随意调节,可以是晴空万里,也可以是风雨交加。现实的天空,我们也可以使用算法进行调整,算…
office2003/2007/2010版本降低宏安全设置方法
如果在公司内部环境中,因为各种系统对Office环境的要求,需要通过降低Office宏安全性的方法来提高系统访问效率的话,可以参考一下方法设置。 Office2003所需的设置如下: 1、打开Office2003,选择"工具"&#x…

【WEB API项目实战干货系列】- API登录与身份验证(三)
上一篇: 【WEB API项目实战干货系列】- 接口文档与在线测试(二) 这篇我们主要来介绍我们如何在API项目中完成API的登录及身份认证. 所以这篇会分为两部分, 登录API, API身份验证. 这一篇的主要原理是: API会提供一个单独的登录API, 通过用户名࿰…

ASP.NET中的页面指示标识
页面指示标识 的功能是用来确定在处理aspx文件的时候,需要系统做一些什么特殊的设定?它的语法是:<% directive attributevalue %> 比如:<%import namespace"System.Data"%> 注意属性之间需要空格&#…

⑨③-不能浪费拥有的年轻资本
⑨③-不能浪费拥有的年轻资本 老男孩语录⑨③-不能浪费拥有年轻资本 万两黄金易得,年轻资本难求! 同学们,我们不能浪费比万两黄金还贵重的年轻资本,因为,转眼间你就不再拥有这个资本了, 趁着年轻࿰…
滴滴AI语音团队获国际顶尖智能对话系统竞赛世界第一
出品 | AI科技大本营头图 | CSDN付费下载于视觉中国近日,在国际顶尖人工智能竞赛第九届对话系统技术挑战赛(DSTC9)中,滴滴语音和NLP技术团队参与端到端多领域面向任务型对话系统任务荣获世界第一,充分彰显滴滴公司在自…

RHEL5+PXE+DHCP+Apache+Kickstart安装RHCE5.1 i386实验室环境
此博文出自:http://andrewyu.blog.51cto.com/1604432/1100567 使用RHEL5PXEDHCPApacheKickstart安装RHCE5.1 i386实验室环境,根据文章内容也可批量安装了红帽RHEL系统或CentOS5.x,现阶段,由于需要大规模应用CentOS5.8 x86_64系统用…

用ASP.Net(C#)连接Oracle数据库的方法
今天看了一下asp.net连接oracle数据库的方法,得到了如下代码。这段代码打开了MyTable表,并把操作员的名字列出。字段类型是OracleString。读取的时候用的是字段编号,我不知道怎么使用字段名来读取某字段的内容。下面是代码://首先…
NLP任务中的文本预处理步骤、工具和示例
作者 | Viet Hoang Tran Duong来源 | DeepHub IMBA头图 | CSDN付费下载于视觉中国数据是新的石油,文本是我们需要更深入钻探的油井。文本数据无处不在,在实际使用之前,我们必须对其进行预处理,以使其适合我们的需求。对于数据也是…

github设置添加SSH
https 和 SSH 的区别: 1、前者可以随意克隆github上的项目,而不管是谁的;而后者则是你必须是你要克隆的项目的拥有者或管理员,且需要先添加 SSH key ,否则无法克隆。 2、https url 在push的时候是需要验证用户名和密码…

在asp.net中使用客户端脚本
我们常常在asp.net中需要使用到客户端脚本,在asp.net中使用客户端脚本很容易,使用到Page的RegisterClientScriptBlock方法就行了。下面是我写的一段C#代码,用来打开新窗口: using System; using System.Web.UI; namespace MyBill { …

一个DIV调用多个CSS样式
1使用以上两个CSS样式表对描述同一个DIV的写法class利用class可以对于同一个标签多重定义样式. 比如用1、2两种样式同时控制一个DIV,可以写成以下格式: <div class"1 2"> </div> 使用以上两个CSS样式表对描述同一个DIV࿰…
最新!百度首发 OCR 自训练平台 EasyDL OCR
今年以来,人工智能愈发火热。在2020年4月,政府已将人工智能基础设施列入新基建范围。在利好政策引导下,人工智能的应用范围越来越广。以 OCR(文字识别技术)为例,随着智能手机与各种端边电子产品的增多&…

性能测试初学_loadrunner base64/md5 编码 解码
参考这3篇文章: MD5: http://bbs.51testing.com/forum.php?modviewthread&tid1111323 base64: http://www.cnblogs.com/preftest/archive/2011/06/12/2079178.html http://www.51testing.com/html/41/15103841-3707341.html 主要思路为&…

ASP.NET管理状态的十种途径
HTTP协议是无状态的,ASP.NET提供了丰富的手段在页面之间管理状态。本文列举ASP.NET管理状态的十种途径。 ASP.NET中,从System.Web.UI.Page继承的类里有以下十种管理页面状态的途径: 1. Application对象: this.Application 2. …

sed学习系列---第3/3部分
为什么80%的码农都做不了架构师?>>> ---简介 在这篇 sed 系列的总结性文章中,Daniel Robbins 带您体验 sed 的真正力量。在介绍完几个重要的 sed 脚本之后,他将通过将一个 Quicken .QIF 文件转换成可读文本格式来演示一些基本 s…

11位科幻作家参与,首次AI人机共创写作实验启动
人工智能会怎样影响人类文学创作?人类智慧与机器智慧如何相互激发创作灵感? 10月27日,由传茂文化和创新工场联手打造的华语科幻AI人机共创写作实验项目《共生纪》启动,人类作家与AI算法将围绕环保、人机关系、性别、文化多样性等…

笔记之远程桌面服务(RDS)
Windows默认只能有2个用户同时通过RDP进行连接,非常不方便,于是借此机会学习了下Win2012R2的远程桌面配置。以下我把学习过程记录一下: 1. 最开始我觉得只需要安装“Remote Desktop Session Host”,事实证明这样没错,可…

用Asp.net 传送大文件
Chris Hynes我们在上传大文件时都遇到过这样或那样的问题。设置很大的maxRequestLength值并不能完全解决问题,因为ASP.NET会block直到把整个文件载入内存后,再加以处理。实际上,如果文件很大的话,我们经常会见到Internet Explorer…

[转]Java Os Properties
2019独角兽企业重金招聘Python工程师标准>>> // File: io/properties/SysPropList.java // Description: Shows system properties. This must be an application. // An applet cant get this information. // Author: Fred Swartz // Date: 2 Feb…

思谋科技A轮融资超1亿美元 ,成为最年轻“准独角兽”AI企业
新一代视觉AI前沿技术公司——思谋科技今天宣布,已完成A轮融资。据知情人士透露,思谋科技此次融资金额超1亿美元,成为业内最年轻的“准独角兽”AI企业,本新投资方包括松禾资本、红杉资本中国基金、基石资本、闻天下投资等。今年6月…

C语言 · 征税程序
算法提高 征税程序 时间限制:1.0s 内存限制:512.0MB问题描述税务局希望你帮他们编写一个征税程序,该程序的功能是:首先输入某公司的年销售额sale和税率rate,然后程序将计算出相应的税额tax,并把它显示在…

在ASP.NET中随意创建图形信息
如果没有一个外部组件的支持,在ASP中是不能动态创建图形的,不管它是一个图表,一个横幅或仅仅是一个图形计数器。可喜的是,这一点在ASP.NET中改变了。现在,我们只需要使用内置功能,就能够很容易动态创建图形…
做动态图表没有数据?用Python就能获取
来源 | 法纳斯特(ID:walker398)刷爆全网的动态条形图,原来 5 行 Python 代码就能实现!这是小F在国庆之前写的一篇文章,既然有了Python这个制作动态条形图工具,缺的那便是数据了。先看一下B站2019年「数据可…

CCTouchDispatcher sharedDispatcher 方法过期
//[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES]; [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:selfpriority:0swallowsTouches:YES];转载于:https://www.cnblogs.com/sell/archive/2013/01/14/2…

哪些听起来像段子一样的故事?
杭州海底世界,一个小走廊两边都是各种爬行动物展览。有两只蜥蜴当时是这个样子人还年轻,还比较猥琐,看到一个趴在另一个身上就觉得在做什么羞羞的事。于是就拍下来,发到群里,然后说了句交配中。然后一天就光拍照&#…