仓酷云

标题: ASP.NET编程:.NET 4.0 中的左券式编程仓酷云 [打印本页]

作者: 若天明    时间: 2015-1-18 11:25
标题: ASP.NET编程:.NET 4.0 中的左券式编程仓酷云
你觉得数据库怎么样?左券式编程不是一门极新的编程办法论。C/C++时期早已有之。Microsoft在.NET4.0中正式引进左券式编程库。博主觉得左券式编程是一种相称不错的编程头脑,每个开辟职员都应当把握。它不仅可使开辟职员的头脑更明晰,并且关于进步程序功能很有匡助。值得一提的是,它关于并路程序计划也有莫年夜的好处。
我们先看一段很复杂的,未利用左券式编程的代码示例。
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}
复制代码
上述代码暗示一个在32位有标记整型局限内的有理数。数学上,有理数是一个整数a和一个非零整数b的比,一般写作a/b,故又称作分数(题外话:有理数这个翻译真是够奇异)。由此,我们晓得,有理数的分母不克不及为0。以是,上述代码示例的机关函数还必要写些进攻性代码。一般.NET开辟职员会如许写:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}
复制代码
上面我们来看一下利用左券式编程的.NET4.0代码示例。为了加倍便利的申明,博主在全部示例上都加了左券,但此示例并不是必定都加这些左券。
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}
复制代码
具体的注释稍后再说。按理,既然左券式编程有那末多优点,那在C/C++天下应当很盛行才对。为何很少看到关于左券式编程的会商呢?看一下C++的左券式编程示例就晓得了。上面是C++代码示例:
  1. //typedeflongint32_t;#include<stdint.h>templateinlinevoidCheckInvariant(T&argument){#ifdefCONTRACT_FULLargument.Invariant();#endif}publicclassRationalNumber{private:int32_tnumberator;int32_tdenominator;public:RationalNumber(int32_tnumberator,int32_tdenominator){#ifdefCONTRACT_FULLASSERT(denominator!=0);CheckInvaraint(*this);#endifthis.numberator=numberator;this.denominator=denominator;#ifdefCONTRACT_FULLCheckInvaraint(*this);#endif}public:int32_tGetDenominator(){#ifdefCONTRACT_FULL//C++Developersliketousestructtype.classContract{int32_tResult;Contract(){}~Contract(){}}#endif#ifdefCONTRACT_FULLContractcontract=newContract();contract.Result=denominator;CheckInvairant(*this);#endifreturnthis.denominator;#ifdefCONTRACT_FULLCheckInvaraint(*this);#endif}protected:#ifdefCONTRACT_FULLvirtualvoidInvariant(){this.denominator!=0;}#endif}
复制代码
Woo...,上述代码充溢了大批的宏和前提编译。关于习气了C#文雅语法的.NET开辟职员来讲,它们是云云丑恶。更主要的是,左券式编程在C++天下并未被尺度化,因而项目之间的界说和修正各纷歧样,给代码形成很年夜凌乱。这恰是很少在实践中看到左券式编程使用的缘故原由。可是在.NET4.0中,左券式编程变得复杂文雅起来。.NET4.0供应了左券式编程库。实践上,.NET4.0仅仅是针对C++宏和前提编译的再次笼统和封装。它完整基于CONTRACTS_FULL,CONTRACTS_PRECONDITIONSSymbol和System.Diagnostics.Debug.Assert办法、System.Environment.FastFail办法的封装。
那末,何谓左券式编程?
何谓左券式编程

左券是削减年夜型项目本钱的冲破性手艺。它一样平常由Precondition(前置前提),Postcondition(后置前提)和Invariant(稳定量)等观点构成。.NET4.0除上述观点以外,还增添了Assert(断言),Assume(假定)观点。这能够由列举ContractFailureKind范例一窥眉目。
左券的头脑很复杂。它只是一组了局为真的表达式。如若否则,左券就被违背。那依照界说,程序中就存在忽略。左券组成了程序规格申明的一部分,只不外该申明从文档挪到了代码中。开辟职员都晓得,文档一般不完全、过期,乃至不存在。将左券移动到代码中,就使得程序能够被考证。
正如前所述,.NET4.0对宏和前提编译举行笼统封装。这些功效年夜多会合在System.Diagnostics.Contracts.Contract静态类中。该类中的年夜多半成员都是前提编译。如许,我们就不必再利用#ifdef和界说CONTRACTS_FULL之类的标志。更主要的是,这些举动被尺度化,能够在多个项目中一致利用,并依据情形是不是天生带有左券的程序集。
1.Assert

Assert(断言)是最基础的左券。.NET4.0利用Contract.Assert()办法来特指断言。它用来暗示程序点必需坚持的一个左券。
  1. Contract.Assert(this.privateField>0);Contract.Assert(this.x==3,"Whyisn’tthevalueofx3?");
复制代码
断言有两个重载办法,首参数都是一个布尔表达式,第二个办法的第二个参数暗示违背左券时的非常信息。
当断言运转时失利,.NETCLR仅仅挪用Debug.Assert办法。乐成时则甚么也不做。
2.Assume

.NET4.0利用Contract.Assume()办法暗示Assume(假定)左券。
  1. Contract.Assume(this.privateField>0);Contract.Assume(this.x==3,"Staticcheckerassumedthis");
复制代码
Assume左券在运转时检测的举动与Assert(断言)左券完整分歧。但关于静态考证来讲,Assume左券仅仅考证已增加的现实。因为诸多限定,静态考证其实不能包管该左券。也许最好先利用Assert左券,然后在考证代码时按需修正。
当Assume左券运转时失利时,.NETCLR会挪用Debug.Assert(false)。一样,乐成时甚么也不做。
3.Preconditions

.NET4.0利用Contract.Requires()办法暗示Preconditions(前置前提)左券。它暗示办法被挪用时办法形态的左券,一般被用来做参数考证。一切Preconditions左券相干成员,最少办法自己能够会见。
  1. Contract.Requires(x!=null);
复制代码
Preconditions左券的运转时举动依附于几个要素。假如只隐式界说了CONTRACTSPRECONDITIONS标志,而没有界说CONTRACTS_FULL标志,那末只会举行检测Preconditions左券,而不会检测任何Postconditions和Invariants左券。假设违背了Preconditions左券,那末CLR会挪用Debug.Assert(false)和Environment.FastFail办法。
假设想包管Preconditions左券在任何编译中都发扬感化,可使用上面这个办法:
  1. Contract.RequiresAlways(x!=null);
复制代码
为了坚持向后兼容性,当已存在的代码不同意被修正时,我们必要抛出指定的准确非常。可是在Preconditions左券中,有一些格局上的限制。以下代码所示:
  1. if(x==null)thrownewArgumentException("Theargumentcannotbenull.");Contract.EndContractBlock();//后面一切的if检测语句皆是Preconditions左券
复制代码
这类Preconditions左券的格局严厉受限:它必需严厉依照上述代码示例格局。并且不克不及有else从句。别的,then从句也只能有单个throw语句。最初必需利用Contract.EndContractBlock()办法来标志Preconditions左券停止。
看到这里,是否是以为年夜多半参数考证都能够被Preconditions左券替换?没有错,现实切实其实云云。如许这些进攻性代码完整能够在Release被往失落,从而不必做那些冗余的代码检测,从而进步程序功能。但在面向考证客户输出此类情境下,进攻性代码仍有需要。再就是,Microsoft为了坚持兼容性,并没有效Preconditions左券取代非常。
4.Postconditions

Postconditions左券暗示办法停止时的形态。它跟Preconditions左券的运转时举动完整分歧。但与Preconditions左券分歧,Postconditions左券相干的成员有着更少的可见性。客户程序也许不会了解或利用Postconditions左券暗示的信息,但这其实不影响客户程序准确利用API。
关于Preconditions左券来讲,它则对客户程序有反作用:不克不及包管客户程序不违背Preconditions左券。
A.尺度Postconditions左券用法

.NET4.0利用Contract.Ensures()办法暗示尺度Postconditions左券用法。它暗示办法一般停止时必需坚持的左券。
  1. Contract.Ensures(this.F>0);
复制代码
B.特别Postconditions左券用法

当从办法体内抛出一个特定非常时,一般情形下.NETCLR会从办法体内抛出非常的地位间接跳出,从而展转仓库举行非常处置。假设我们必要在非常抛出时还要举行Postconditions左券考证,我们能够以下利用:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}0
复制代码
个中小括号内的参数暗示当非常从办法内抛出时必需坚持的左券,而泛型参数暗示非常产生时抛出的非常范例。举例来讲,当我们把T用Exception暗示时,不管甚么范例的非常被抛出,都能包管Postconditions左券。哪怕这个非常是仓库溢出或任何不克不及把持的非常。激烈保举当非常是被挪用API一部分时,利用Contract.EnsuresOnThrows<T>()办法。
C.Postconditions左券内的特别办法

以下要讲的这几个特别办法仅限利用在Postconditions左券内。
办法前往值在Postconditions左券内,能够经由过程Contract.Result<T>()办法暗示,个中T暗示办法前往范例。当编译器不克不及推导出T范例时,我们必需显式指出。好比,C#编译器就不克不及推导出办法参数范例。
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}1
复制代码
假设办法前往void,则不用在Postconditions左券内利用Contract.Result<T>()。
前值(旧值)在Postconditions左券内,经由过程Contract.OldValue<T>(e)暗示旧有值,个中T是e的范例。当编译器可以推导T范例时,能够疏忽。别的e和旧有表达式呈现高低文有一些限定。旧有表达式只能呈现在Postconditions左券内。旧有表达式不克不及包括另外一个旧有表达式。一个很主要的准绳就是旧有表达式只能援用办法已存在的那些旧值。好比,只需办法Preconditions左券持有,它一定能被盘算。上面是这个准绳的一些示例:

  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}2
复制代码
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}3
复制代码
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}4
复制代码
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}5
复制代码
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}6
复制代码
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}7
复制代码
D.out参数

由于左券呈现在办法体后面,以是年夜多半编译器不同意在Postconditions左券内援用out参数。为了绕开这个成绩,.NET左券库供应了Contract.ValueAtReturn<T>(outTt)办法。
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}8
复制代码
跟OldValue一样,当编译器能推导出范例时,泛型参数能够被疏忽。该办法只能呈现在Postconditions左券。办法参数必需是out参数,且不同意利用表达式。
必要注重的是,.NET今朝的工具不克不及检测确保out参数是不是准确初始化,而不论它是不是在Postconditions左券内。因而,x=3语句假设被付与其他值时,编译器其实不能发明毛病。可是,当编译Release版本时,编译器将发明该成绩。
5.ObjectInvariants

对象稳定量暗示不管对象是不是对客户程序可见,类的每个实例都应当坚持的左券。它暗示对象处于一个“优秀”形态。
在.NET4.0中,对象的一切稳定量都应该放进一个受回护的前往void的实例办法中。同时用[ContractInvariantMethod]特征标志该办法。别的,该办法体内涵挪用一系列Contract.Invariant()办法后不克不及再有其他代码。一般我们会把该办法定名为ObjectInvariant。
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){if(denominator==0)thrownewArgumentException("Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{returnthis.denominator;}}}9
复制代码
一样,ObjectInvariants左券的运转时举动和Preconditions左券、Postconditions左券举动分歧。CLR运转时会在每一个大众办法末了检测ObjectInvariants左券,但不会检测对象闭幕器或任何完成System.IDisposable接口的办法。
6.Contract静态类中的其他特别办法

.NET4.0左券库中的Contract静态类还供应了几个特别的办法。它们分离是:
A.ForAll

Contract.ForAll()办法有两个重载。第一个重载有两个参数:一个汇合和一个谓词。谓词暗示前往布尔值的一元办法,且该谓词使用于汇合中的每个元素。任何一个元素让谓词前往false,ForAll中断迭代并前往false。不然,ForAll前往true。上面是一个数组内一切元素都不克不及为null的左券示例:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}0
复制代码
B.Exists

它和ForAll办法差未几。
7.接口左券

由于C#/VB编译器不同意接口内的办法带有完成代码,以是我们假如想在接口中完成左券,必要创立一个匡助类。接口和左券匡助类经由过程一对特征来链接。以下所示:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}1
复制代码
.NET必要显式如上述声明从而把接口和接口办法相干联起来。注重,我们不能不发生一个哑元前往值。最复杂的体例就是前往default(T),不要利用Contract.Result<T>。
因为.NET请求显式完成接口办法,以是在左券内援用不异接口的其他办法就显得很愚笨。由此,.NET同意在左券办法之前,利用一个部分变量援用接口范例。以下所示:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}2
复制代码
8.笼统办法左券

同接口相似,.NET中笼统类中的笼统办法也不克不及包括办法体。以是同接口左券一样,必要匡助类来完成左券。代码示例不再给出。
9.左券办法重载

一切的左券办法都有一个带有string范例参数的重载版本。以下所示:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}3
复制代码
如许当左券被违背时,.NET能够在运转时供应一个信息提醒。今朝,该字符串只能是编译经常量。可是,未来.NET大概会改动,字符串能够运转时被盘算。可是,假如是字符串常量,静态诊断工具能够选择显现它。
10.左券特征

A.ContractClass和ContractClassFor

这两个特征,我们已在接口左券和笼统办法左券里看到了。ContractClass特征用于增加到接口或笼统范例上,可是指向的倒是完成该范例的匡助类。ContractClassFor特征用来增加到匡助类上,指向我们必要左券考证的接口或笼统范例。
B.ContractInvariantMethod

这个特征用来标志暗示对象稳定量的办法。
C.Pure

Pure特征只声明在那些没有反作用的办法挪用者上。.NET现存的一些托付能够被以为云云,好比System.Predicate<T>和System.Comparison<T>。
D.RuntimeContracts

这是个程序集级其余特征(详细怎样,俺也不太分明)。
E.ContractPublicPropertyName

这个特征用在字段上。它被用在办法左券中,且该办法相对字段来讲,更具可见性。好比公有字段和大众办法。以下所示:
  1. //.NET代码示例publicclassRationalNumber{privateintnumberator;privateintdenominator;publicRationalNumber(intnumberator,intdenominator){Contract.Requires(denominator!=0,"Thesecondargumentcannotbezero.");this.numberator=numberator;this.denominator=denominator;}publicintDenominator{get{Contract.Ensures(Contract.Result<int>()!=0);returnthis.denominator;}}[ContractInvariantMethod]protectedvoidObjectInvariant(){Contract.Invariant(this.denominator!=0);}}4
复制代码
F.ContractVerification

这个特征用来假定程序集、范例、成员是不是可被考证实行。我们可使用[ContractVerification(false)]来显式标志程序集、范例、成员不被考证实行。
.NET左券库今朝的缺点

接上去,讲一讲.NET左券库今朝所存在的一些成绩。

Well,.NET左券式编程到这里就停止了。嗯,就到这里了。
PS:.NET左券库固然已相称文雅。但博主觉得,其跟D言语完成的左券式编程仍有一段间隔。
PS:有谁乐意当俺的Mentor。您可以享用如许的权力和任务:天堂般可怕的发问和骚扰。非诲人不倦者勿扰。
本文来自:http://www.ckuyun.com/lucifer1982/archive/2009/03/21/1418642.html
相干浏览:
.NET代码左券组件今朝已供应下载
一个很大的类库。应用程序之所以难以跨平台,在于直接调用了特定平台的接口,而一个巨大的类库,就能极大地减少应用程序对平台的依赖。
作者: 莫相离    时间: 2015-1-18 21:42
CGI程序在运行的时候,首先是客户向服务器上的CGI程序发送一个请求,服务器接收到客户的请求后,就会打开一个新的Process(进程)来执行CGI程序,处理客户的请求。CGI程序最后将执行的结果(HTML页面代码)传回给客户。
作者: 蒙在股里    时间: 2015-1-21 06:54
ASP.net的服务器,要求安装一个.net环境,当然我这里指的是windows系统,顺便点一下,.net只能放在windows环境里来运行。Asp.net1.1的就装Framework1.1,Asp.net2.0的就装Framework2.0。
作者: 小女巫    时间: 2015-1-22 20:33
市场决定一切,我个人从经历上觉得两者至少在很长时间内还是要共存下去,包括C和C++,至少从找工作就看得出来,总不可能大家都像所谓的时尚一样,追捧一门语言并应用它。
作者: 谁可相欹    时间: 2015-1-31 11:28
JSP/Servlet虽然在国内目前的应用并不广泛,但是其前途不可限量。
作者: 精灵巫婆    时间: 2015-1-31 18:56
使用普通的文本编辑器编写,如记事本就可以完成。由脚本在服务器上而不是客户端运行,ASP所使用的脚本语言都在服务端上运行,用户端的浏览器不需要提供任何别的支持,这样大提高了用户与服务器之间的交互的速度。
作者: 若相依    时间: 2015-2-6 16:35
PHP的源代码完全公开,在OpenSource意识抬头的今天,它更是这方面的中流砥柱。不断地有新的函数库加入,以及不停地更新,使得PHP无论在UNIX或是Win32的平台上都可以有更多新的功能。它提供丰富的函数,使得在程式设计方面有着更好的资源。目前PHP的最新版本为4.1.1,它可以在Win32以及UNIX/Linux等几乎所有的平台上良好工作。PHP在4.0版后使用了全新的Zend引擎,其在最佳化之后的效率,比较传统CGI或者ASP等技术有了更好的表现。
作者: 柔情似水    时间: 2015-2-17 07:51
弱类型造成潜在的出错可能:尽管弱数据类型的编程语言使用起来回方便一些,但相对于它所造成的出错几率是远远得不偿失的。
作者: 变相怪杰    时间: 2015-3-5 17:27
市场决定一切,我个人从经历上觉得两者至少在很长时间内还是要共存下去,包括C和C++,至少从找工作就看得出来,总不可能大家都像所谓的时尚一样,追捧一门语言并应用它。
作者: 若天明    时间: 2015-3-7 07:58
Asp.net:首先来说,Asp.net和Asp没什么关系,看着像是升级版本什么的,其实没什么联系。Asp是脚本编程,用的是ASP语言,而ASP.net用的是C#语言,完全不同的东西。
作者: 金色的骷髅    时间: 2015-3-12 07:37
Servlet的形式和前面讲的CGI差不多,它是HTML代码和后台程序分开的。它们的启动原理也差不多,都是服务器接到客户端的请求后,进行应答。不同的是,CGI对每个客户请求都打开一个进程(Process)。
作者: 愤怒的大鸟    时间: 2015-3-19 18:44
是指转换后的Servlet程序代码的行数。这给调试代码带来一定困难。所以,在排除错误时,可以采取分段排除的方法(在可能出错的代码前后输出一些字符串,用字符串是否被输出来确定代码段从哪里开始出错)。




欢迎光临 仓酷云 (http://ckuyun.com/) Powered by Discuz! X3.2