




已阅读5页,还剩15页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
面向对象为什么要写这篇文档?我写这篇文档只是想把我所认识的面向对象理论给表达出来,并希望可以给一些开发者以启示作用。而我目前并不打算开放它们,因为还没有写完呢。所以现在请勿评论。什么是面向对象?面向对象是一种程序设计的范式。面向对象使用对象,类,封装,继承,多态,消息等概念来进行程序设计。为什么面向对象?面向对象符合人类对世界的认知规律。简单说,人类个体认识世界是从对周围事物的观察开始的,而且是从具体到抽象的,并且认识到事物的属性,状态,行为等等,以及事物之间的相互关系。而面向对象就是对这种思维方式的抽象表达。面向对象与面向过程并不矛盾。面向对象着重刻画系统中对象的关系和相互作用,已经对象本身的属性,状态,和行为。面向过程则是侧重于描述过程。而面向对象的程序设计,是无法离开面向过程的范式。什么是对象?万事万物皆对象。我以为这句话很精彩,因为它带有佛偈一样的色彩。于是,不同的人理解这句话,有着不同的看法。我们并不想在这句话本身做更多的讨论,那太宗教哲学了。你们看那天上的飞鸟,不稼不穑,我尚且养活它们,你们不比那飞鸟贵重得多吗?-马太福音我们在这里至少看到了飞鸟就是一种对象,而在我们的世界中,充满了对象,人,飞鸟,百合花。齿轮,轴承,阀。桌椅,鼠标,液晶显示器。我们认知世界是从具体到抽象的。如果只是把看得见,摸得着的东西定义为对象,那么这未免太简单了。一个过程,一个办法,一个概念也可以是对象。至于,一个对象是具体的,还是抽象的,我想这是一个哲学问题,也许取决于每一个人对一个事物的认识程度罢。对象有其类型我们不得不诠释类型的概念。inta=1;这里,变量a的类型是int,那么它的类型就是int。当我们要表示年龄,数量,这样的信息的时候,int类型的量就可以完成描述了。而一个矩形呢?它的类型是什么呢?我们知道在C语言里面采用struct关键字来描述这样的类型。struct Pointintx;inty;struct Point p1;struct Point p2;p1.x = 0;p1.y = 0;p2.x = 0;p2.y = 1;Point就是我们自定义的类型了,那么这个类型的变量,我们就可以称为是一个Point类型的对象了。在上面的代码中,我们说Point是类型,而p1和p2是Point类型的两个实例。对象和类初学者常常对语言中的static成员感到疑惑,于是我们讲解一下static关键字的面向对象的意义。这个static已经不是C语言中的static了。看如下代码:classPointstaticintcount;intx;inty;这里,我们有三个成员,其中,count被冠以static关键字。那么它与成员x,y就有了非常大的差别了,我们称count是类成员,而x和y是对象成员。count成员不属于某一个对象,不是某个对象的属性,而是属于整个类。在一个二维平面上,有若干个点,那么每一个点就是一个Point类的实例对象,它们有自己的坐标,一个点的x值(横坐标),和另外一个点的x值是两个不同的量(尽管它们的值可能相等),而这个平面上点的数量只有一个,是Point实例的个数,并且,这个数量不属于某一个Point实例的属性。对于C+的程序员来说,类成员往往被直接翻译为静态成员了,但是这似乎不能直观地反映它的意义。 在这个系列的专题中,请大家注意对象这个词,有时候,它可能强调对象的类型的概念,有时候则表达对象实例的含义,而有时候则含有这两重意思,读者要仔细辨析和体会。但是初学者,还是要注意区分类型和实例之间的关系。在这个系列的专题中,我尽量采用C+和Java代码作为例子,而且会忽略其中的一些语法要素,比如是private还是public,是int,还是Integer。并且还有可能用C#,JavaScript,Ruby的代码作为例子。但是大家能体会到代码所表达的意义即可,不必深究其中的语法细节。面向对象初步我们开始讲述和程序设计相关的面向对象理论,主要涉及到消息,封装,继承,多态等内容。消息其实我并不喜欢消息这个概念,因为当时我学习面向对象导论的时候,已经有过Windows编程的经验了,而面向对象的消息和Windows的消息机制并无关系。因为面向对象强调对象的行为和对象之间的相互作用,而这种行为是以消息体现出来的。当某一个对象收到消息的时候,则对象对此做出反应。而实际体现到具体的语言上,则往往是以方法的调用来体现这一概念的。obj.function();我们给对象obj发送某消息,就是用调用对象obj的方法来表明这一概念的。如果有Objective-C的基础,那么理解这个概念将更加容易。一般有C语言基础的人, 更习惯于称方法为函数, 这里我统一称其为方法, 因为我以为方法更多地带有行为的意味, 而函数更多地强调了数学上多输入, 单输出的数学函数的概念. 面向对象的几个重要概念封装(Encapsulation),继承(Inheritance),多态(Polymorphism)是面向对象三个重要的特性。除此以外,还有许多重要的概念,比如说数据的封装和隐藏,方法的重写,等等。在后面我们将一一进行讲解。 封装(encapsulation)封装,就是将数据和数据的操作进行封装,这涵盖了数据的封装,数据的隐藏,对象的抽象和描述,封装方法对数据进行操作,等等。 数据的封装我们依然以Point为例。如果说一个Point的实例就表示了在笛卡尔二维坐标系上的一个点。那么我们说,我们用Point对象刻画了坐标系上的点。而一个Point对象封装了两个int数据,很显然,二维平面上点,用一个横坐标和一个纵坐标就能唯一确定了。所以我们的代码是这样的。classPoint. intx; inty;类Point封装了两个整型的数据,x和y。数据的封装,是刻画对象的最基本的一种方式,它反映了has-a的关系。我们说,对象Point有一个x(横坐标),也有一个y(纵坐标)。Point对象,和x,y对象就构成了has-a的关系。据此,我们也可以刻画Rect对象了。classRect.Pointp1;/Top-leftpoint;Pointp2;/Bottom-rightpoint于是,我们有了一个Rect类,它用两个顶点描述了这个矩形的性质。 数据的隐藏(Data Hiding)通常,Data Hiding被翻译为信息隐藏,而不是数据隐藏。但是我习惯于使用数据隐藏这个词汇。 在讲述数据隐藏之前,我们要先讲述为什么要实现数据隐藏。如果,我们把Point限制为表达屏幕上一点的坐标,那么有如下的规则:( x = 0, y = 0, x = ScreenWidth, y = 0&xOnDraw(); /CodewillgointoImageButton:OnDraw(),notButton:OnDraw().那么上面的代码完整地展示了多态的工作情况,我们用基类的指针变量调用了派生类的方法,那么这种行为就叫做多态。这是多态在具体编程语言上的表述和体现。通常,比较完整的说法是:“用基类的指针或者引用变量去调用派生类对象的实例方法。”在这里,我们需要强调的是,“用基类的指针或者引用”,因为这是多态所以关键的因素。比如下面的代码,于运行之前,我们无从知道基类的指针到底是指向了哪一个派生类的对象,也许是一个ImageButton类的对象,也许是一个CheckButton类的对象实例。voiddrawButton(Button*pButton)Button-OnDraw();/Whoknows,whatsthetypeoftheinstancewhichpButtonpointsto.但是,尽管我们不知道pButton指针指向的对象的实际类型(我指的是在编译期),但是这段代码却是可以通过编译,并且顺利运行,而具体会呈现出什么行为,完全是由运行时实际指向对象的类型决定的,这就是动态绑定,或者称为延迟绑定。理解多态至关重要,它的运用非常广泛,也非常灵活,它是面向对象中最核心和最基础的概念。我们在这节中只讲最基本的概念和语法。但是在后续的章节中,我们会更加深入的讨论多态。多态将贯通整个专题。我们所要讲的是多态的理论,而不是实现,我们暂时不会涉及到C+的虚表,或者JVM的invokevirtual指令,这些细节似乎并不能帮助我们理解多态理论的运用和对象设计。但是需要的时候,我会以ASM的角度来阐述面向对象多态的实现细节。面向接口编程(上)前面的章节对于本篇来说,只是基础和铺垫,而且讲的很简单,因为那些很容易理解。我们从这个章节开始,用大量的代码的配合,来阐述面向接口编程。 接口的演化形式现在我们回顾一下继承相关的知识。我们现在给出一组新的继承体系。它们是和图形相关的,我们可以假设这样的一种需求,就是我们要实现一个Windows的画图板,至少要能在上面绘制几个圆形和矩形。于是,我们很自然底定义了如下的classes。classShapevoiddraw();classCircle:Shapevoiddraw()/*drawacircleonsomedevice*/classRect:Shapevoiddraw()/*drawaRectonsomedevice*/首先要说的是,我们确实是无法给出Shape类的方法draw()的实现。因为它是一个抽象的类型(不要和语言中的抽象类或abstract关键字混淆,但是它们确实有莫大的联系,但是我所谓的抽象是一种真实的抽象),我们不知道一个所谓的“图形”应该如何被画出来。这就像你让我画一个图形出来一样,我感到很为难。我画个圆圈或者是多边形,五角星,似乎都不是一个具有抽象意义的图形。我们再回顾一下多态,下面的代码更好的诠释了多态的作用。Arrayshapes=newCircle(),newRect(),newCircle(),newCircle(),newRect();voiddrawShapes()foreach(Shapeshapeinshapes)shape.draw();这是一段伪码,但是很自然地表现除了多态的从容,foreach从容器中循环枚举出Shape类的各种不同的派生类对象,它们都多态性地调用了它们各自类型的draw()方法。而这个过程又绝没有很明显的显露出某一个具体的派生类的类型的参与。于是,我们还可以很自然地再派生出圆角矩形,或者是五角星,都让它们继承自Shape类即可,它们也可以很自然地被放进shapes容器中,而且又不会修改循环处的分毫代码,这就是我们所追求的可扩展性和“新增代码不会影响已有的代码”。嗯,这一切都很完美,不是嘛? 然而,这真的很完美嘛?表格(Table)是一个Shape(对象)嘛?文本(Text)是一个Shape嘛?如果我们要有若干个图层(Layer),每一个图层是一个Shape嘛?如果是,这些出现在画图板上的元素,它们很好的诠释了is-a的信念嘛?没有,因为一个Table不是一个Shape,Text也不是Shape,但是它们也都可以被画在上面,于是我们需要进行一次重要的演化。我们在C+的语法教材中不强调接口的概念,而是用纯抽象类来表达这一个含义,但是我更愿意用Java的interface关键字作为表达。但是我们要清楚,无论是接口,还是基类,抽象基类,多态性都是存在于这样的语法关系中的。interfaceIDraw.voiddraw();classCircleimplementsShape.voiddraw()./*/*drawacircleonsomedevice*/classRectimplementsIDraw.voiddraw()./*/*drawaRectonsomedevice*/这次演化,似乎是没有本质改变的,特别是对于C+的编译器来说,编译出来的代码都可能没有丝毫的不同。也许很多人开始叹气了,但是我想说的是,对于设计来说,这种变化是本质的变化。因为对象的关系变化了,之前,我们说一个Circle对象也是一个Shape,它满足is-a的经典关系,但是现在,这种关系被打破了。 can-do & Constraint我们在前面的章节中,已经提到了has-a和is-a的对象关系了,现在,我们的重点是can-do的关系,这种关系表示约束(Constraint)。于是,在上面的代码中,我们说Circle类实现了IDraw接口,或者说Circle对象能够完成IDraw接口所要求的行为。voiddraw(IDrawd)d.draw();而这个函数就更直观地表达出约束的概念了,“画可画之对象”,准确地说,这个函数更直接地完成 IDraw接口的约束语义表达:只有实现了约束的对象才可以被传入,并调用其draw()方法(注意,而并不是有draw()方法的对象都可以被传入和调用,相关问题可以对比C+09的concept)。ArraydrawList=newCircle(),newRect(),newCircle(),newTable(),newRect(),newTextArea();voiddraw()foreach(IDrawdindrawList)d.draw();我用这段伪码,明确地表示了这样的一个新情况,Circle被画到画图板中去了,Table和TextArea也被画到上面去了。依然是存有怀疑,难道Table派生自Shape就有问题嘛?难道一定要弄出一个IDraw,让它看起来有道理才是合理的?不是的。首先,如果IDraw接口没有取代Shape类,我们也要承认这样几个事情,Shape类还是带着IDraw接口的含义,尽管我们不把它抽象出来。而且,设计没有对错之分,是不是适应需求才是最实际的评判标准。我们当前的例子还很简单,我们只涉及到了元素的绘制(draw),但是也许还有其他的问题(新需求总是很多的),比如ALPHA混合,图层的遮挡,元素的选择,蚂蚁线的绘制,等等。例如,当我们选择了一个Rect的时候,他的四周有蚂蚁线,而我们选择了一个TextAread的时候,它的四周是带有8个调节点的边框。可是,我们不打算让蚂蚁线和边框参与其他元素的遮挡计算和ALPHA混合计算,而且它们也不参与序列化,于是,它们既不是Shape,也不应该实现IDraw接口。经验告诉我们,Shape类是不足以成为所有元素的基类的,IDraw接口也不是万能的。具体的解决方案要看需求,如何应对这些需求,我们会在后面的内容中有所涉及。面向接口编程(下)约束的理解是一个很辩证的问题,我觉得我们需要一个好的例子才能把这个事情论述清楚。我很喜欢鸟儿在天上飞这个例子,因为它和大自然紧紧的联系了起来。classBirdfly()/*somecodemakebirds fly onthesky*/classDove:Birdfly()/*somecodemakeadove fly onthesky*/classPenguin:Birdfly()/* fly, fly, Oh, couldIfly?*/在这段代码中,有一个基类,class Bird,鸟类,然后派生类是Dove(鸽子),和Penguin(企鹅)。简而言之,这三个类描述了这样一个世界,“但凡是个鸟就能飞”。哦,世界还是我们所认识的世界嘛?企鹅都能飞了。问题就在于:是鸟就一定能飞嘛?显然不是,这个世界除了企鹅外,还有许多鸟类没有飞行的能力,也就是说鸟类(Bird)与飞行(fly()并没有一种必然的关系的。或许,我们不该把fly()方法设计到Bird类中去。于是,我们采用一个叫做IFly的接口,根据can-do的对象关系。我们可以称这个接口的实现类为能飞的事物。interface IFlyfly()/*somecodemake something fly onthesky*/classDove: Bird, IFlyfly()/*somecodemakeadoveonthesky*/classPenguin: Bird/* And wound not implements IFly */然后我们发现,这样的设计很好,我们可以很简单地让一些不是鸟类的动物飞了起来。classButterfly:Insect,IFlyfly()/*abutterflyflies*/嗯,这次演化,和Shape类演化到IDraw接口,具有相似的原因。一些Java,或者C#的教科书,往往会这样写:“Java不支持多继承,但是支持实现多个接口. .”, 我以为这样的话本身没有问题,但是许多初学者会误以为实现多个接口也能完成多继承的功能。而从设计的角度来说,用聚合来代替继承才是一种选择,这个是我们前面说过的,而实现多接口并不是完成这种对象关系表达的合理途径。因为接口的语义往往是can-do,而且需要强调的是,这是一种约束的语义。而继承表达的是is-a的关系。继承的对象关系使得派生类传承了一种(行为)能力。而,接口没有。我们想想,是IFly使得一只鸟儿飞上了天嘛?不,它没有赋予鸟儿这种行为能力。而真正完成飞行能力的是实现代码本身,而并不是“实现接口”这一举动。想飞?这很容易。许多动态的教本语言,比如Ruby,Python,JavaScript,它们只要拥有了fly这样的方法,就可以被调用了,于是,无论有没有实现IFly接口,对象都起飞了。所以说,实现接口并没有赋予对象以能力,而是约束对象,或者说要求对象具有某种能力。一些语言,如果只支持单继承,比如Ruby,如果想聚合某种功能,可以通过Mixins来完成这种功能的扩展。classBirdendmoduleActiondefflyendendclassDoveBirdincludeActionenddove=Dove.newdove.fly接口与应用前面我们所讲的东西足够理论了,但是实际的软件中,大量用到面向接口编程,而没有我们这么多推演和辩证的过程。因为接口有许多重要的性质。例如,COM的出现,可以解决Dll Hell的问题。结果,COM的开发者总是要定义出许多的COM接口。而它们的实现反而并不重要了。接 口的使用,会有许多种原因,有时候仅仅就是为了做到“接口和实现分离”。而这给软件工程带来了许多新话题。而相当多的设计模式都是以接口编程为基础的,经典的设计模式的实现,往往就是一个精彩的接口展示。而又不仅仅是模式本身,还有些原则,比如DIP,往往是采用接口编程的方式。当然了DIP还有很多争 论,我们就不讨论它本身的问题了。对象的粒度粒度的变化对象关系的演化 我们依然用Button作为例子来说明对象关系中粒度的变化。我们在ImageButton的OnDraw方法中加入了许多的代码,这些代码完成了一个按钮的绘制工作,可能要绘制按钮的3D边框,或者表明的渐变色,以及上面的文本,比如OK或者Cancel,此外还有一个Icon,这里我们就认定它为一个Image。classImageButton:publicButtonpublic:voidOnDraw()/*/*codefordrawbuttonframe*/./*/*codefordrawbuttontext*/./*/*codefordrawbuttonimage*/.; 这段代码展示了一个ImageButton是如何显示自己的。但是事情还没有结束呢,现在我们要设计一个新的控件,一个ImageLabel,也就是不仅仅有文本的标签,它也想要一张图片来装点自己。classImageLabel:publicLabel.public:voidOnDraw()./*/*/*/*codefordrawLabelframe*/./*/*/*/*codefordrawLabeltext*/./*/*/*/*codefordrawLabelimage*/.;OK,我们观察每一处变化,并假定Draw一个Image是一个复杂的实现。但是无论是在Button上画一个Image,还是在Lable上画一个Image,它们都(应该)是相同的逻辑(比如以Win32的程序为例,在Button的DC画,与在Label的DC上画,它们的逻辑几乎是完全是一回事情)。那么,为什么不把这样的绘制行为进行一下封装呢?classImageDrawingpublic:voidDrawImage()/*drawanimage*/classImageButton:publicButton,protectedImageDrawingpublic:voidOnDraw()/*codeforpaintingbuttonandframe*/.DrawImage();/callImageDrawing:DrawImage();classImageLabel:publicLabel,protectedImageDrawingpublic:voidOnDraw()/*codeforpaintinglebeltextandframe*/.DrawImage();/callImageDrawing:DrawImage();于是,我们把绘制Image的逻辑封装到了ImageDrawing类中,而用继承的方式,把这种功能集成了进来。需要有两点说明:第一是这段代码限于在C+内,Java和C#很难用类似的代码形式表达这种关系,但是可以求助于聚合。第二,所以封装为类,而不是函数(方法),是因为类对象更容易维护状态。保护继承和Implement-with在上面的代码中,我们重点描述了对象关系的演化,而演化的结果就是这种关系,implement-with,以.实现(用.实现)。那么这种关系更易于用C+的代码进行描述,或者采用Ruby的Mixins等语法特性。那么这是在讲述了has-a,is-a,can-do对象关系后的又一种关系。为什么要用保护继承呢?那么我们来看下面的代码。下面的代码用最直接的方式说明了is-a的关系问题。To be, or not to be: that is the question.-ShakespeareButton*button=newImageButton();/OKImageDrawing* d=newImageButton();/CompileError.OK,这就是我们想要的结果,我们不想接受那样的事实,一个ImageButton也是一个“Image绘制”?我觉得用自然的语言表达得如此尴尬。然而,我们还可以将ImageDrawing类的构造函数标记为Protected的,这样,这个类就真的只能用于保护继承了。于是,这种关系又一次被稳固了。 对象的正交关系对象之间的关系,在功能上可能存在正交性,而我们要尽量地把这种正交性体现出来,于是我们就要重新界定对象的粒度。我们借用Java的Thread类,和Runnable接口作为例子的原型,记住,只是以其为原型,我所讲述的并非是Java的线程。classThreadvoidstart(Runnablerun)interfaceRunnablevoidrun();我再次请大家注意的是,我的Thread类并没用实现Runnable类,因为我认为那并不必要,事实上,我们也不鼓励Java的开发人员派生Thread,而是鼓励他们实现Runnable接口。(newThread(newRunnable()publicvoidrun().start();这样的使用方式(除了代码风格外),才是我们鼓励的。但是由于我们只讲必要性,而不涉及到具体使用的方便性,所以我的Thread类没有实现Runnable接口。那么我们思考线程对象和Runnable实现类的真实含义,一个线程对象,也许维护了OS的线程内核对象(可能是模拟的,比如Ruby的Fake Thread)。但是线程对象本身可以加载什么方法,或者说在一个线程上做什么事情,那是Runnable实现类要如何描述的问题,这两个对象有着关系,但是在功能上确实正交的。于是,我们把它们分成两个对象的粒度。到此,如果我们有一个Timer类,或者一个叫做Fiber的类。我们只是想把我们要做的事情加载到Timer事件中,或者是Fiber纤程上,我们就会更好地意识到Runnable接口和Thread类的正交性。抽象粒度我们考虑流的概念,这是一个抽象的概念,比如文件流,Socket的通信,管道中的数据,都被抽象为了流。但是我们先想想 MFC的CFile类,这个类主要完成了哪些事情?打开和关闭文件,读写文件。但是我们发现,文件被打开后,对其进行读写的操作,可以正交于打开和关闭这 些操作,更关键的是,如果我们把Socket,管道,串口设备等都看作是文件的时候。我们依然要读写它们。而这个操作尽管在实现上有着千差万别,但是对象 的方法,也就是其表现行为是一致的(准确说是相似的,一致只是一种理想的情况),都是Read和Write。于是我们就有把这种操作抽象出来的理由了,那 么今天的流的概念,就源于此处的抽象。Filefile=newFile(fileName);FileInputStreamfis=newFileInputStream(file);迭代器也是一种抽象的概念,它的对象用于遍历容器,并可以通过它进行对容器中的元素的各种操作。而这种抽象,也隔离了具体容器的实现,无论是链表,数据,平衡二叉树,我们都可以用一致的迭代器接口访问那些处于容器中的元素。这些是最经典的抽象了,在实际的开发中,我们也需要建立一个抽象的概念,然后去实现它,并从中发现对象的抽象粒度之美。对象的粒度是一个难以界定的量,在今天的面向对象开发中,提倡单一职责的原则,尽量要对象的粒度变小。比如C+设计新思维就提倡这种观点。但是,书中没有刻意地让一个类保护继承自若干个Policy Classes
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 情商口才课件
- 兴义市第八中学2026届高一化学第一学期期中质量跟踪监视试题含解析
- 活动方案策划稿
- 2026届浙江省杭州市江南实验学校高三化学第一学期期末达标测试试题含解析
- 怎么写食品安全活动方案
- 泡沫雕塑面试题及答案
- 小学汉字之谜社团活动方案
- 军事投弹考试题及答案
- 化学考核面试题及答案
- 家电公司配送管理规定
- 煤矿安全规程新旧版本对照表格版
- 私募薪酬管理办法
- 2025年急诊三基考试题库及答案
- 2025贵州航空产业城集团股份有限公司旗下子公司贵州安立航空材料有限公司招聘61人笔试历年参考题库附带答案详解
- 军人休假规定管理办法
- 2025秋人教版英语八年级上Unit 2 全单元听力材料文本及翻译
- DB11-T 1455-2025 电动汽车充电基础设施规划设计标准
- 2025北京初二(上)期末英语汇编:阅读单选CD篇
- 2025年公招教师特岗教师招聘考试教育公共基础知识真题(带答案)
- 2025年贵州省中考英语真题含答案
- 社矫业务培训课件
评论
0/150
提交评论