委托、Lambda表达式和事件 - 泡泡熊 阅读原文»1. 引用方法
委托是寻址方法的.NET版本。在C++中,函数指针只不过是一个指向内存位置的指针,它不是类型安全的。我们无法判断这个指针实际指向什么,像参数和返回类型等项就更无从知晓了。而.NET委托完全不同,委托是类型安全的类,它定义了返回类型和参数的类型。委托类不仅包含对方法的引用,也可以包含对多个方法的引用。
Lambda表达式与委托类型直接相关。当参数时委托时,就可以使用Lambda表达式实现委托引用的方法。
2. 委托
当要把方法传递给其他方法时,需要使用委托。我们习惯于把数据作为参数传递给方法,而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作。更麻烦的是,在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到。所以需要把第二个方法作为参数传递给第一个方法。这听起来很令人疑惑,下面用几个例子来说明:
- 启动线程和任务——在C#线程的一个基类System.Threading.Thread的一个实例上使用方法Start(),就可以启动一个线程。如果要告诉计算机启动一个新的执行序列,就必须说明要在哪里启动该序列。必须为计算机提供开始启动的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了线程调用的方法。
- 通用库类——比如Sort(List<T> list,Func<T ,T, comparison>)函数实现快速排序,则需要指定一个方法参数comparison,告诉排序函数如何实现对两个参数的比较。
- 事件——一般是通知代码发生了什么事件。GUI编程主要处理事件。在引发事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法作为一个参数传递给委托。
在C和C++中,只能提取函数的地址,并作为一个参数传递它。C没有类型安全性。可以把任何函数传递给需要函数指针的方法。但是,这种直接方法不仅会导致一些关于类型安全性的问题,而且没有意识到:在进行面向对象编程时,几乎没有方法是孤立存在的,而是在调用方法前通常需要与类实例相关联。所以.NET Framework在语法上不允许使用这种直接方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊类型的对象,其特殊之处在于,我们以前定义的对象都包含数据,而委托包含的只是一个或多个方法的地址。
2.1声明委托
使用委托时,首先需要定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托表示哪种类型的方法。然后,必须创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。
定义为托的语法如下:
delegate void IntMethodInvoker(int x);
在这个示例中,定义了一个委托IntMethodInvoker,并指定该委托的每个实例都可以包含一个方法的引用,该方法带有一个int参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所表示的方法的签名和返回类型等全部细节(理解委托的一种好方式是把委托当做这样一件事情:它给方法的签名和返回类型指定名称)。
假定要定义一个委托TwoLongsOp,该委托表示的方法有两个long型参数,返回类型为double,可以编写如下代码:
delegate double TwoLongsOp(long first, long second);
或者要定义一个委托,它表示的方法不带参数,返回一个string型的值,可以编写如下代码:
delegate string GetAString();
其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托,也就是说,可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性。和委托的作用域,可以在委托的定义上应用任意常见的访问修饰符:public、private、protected等。
实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生自基类System.MulticastDelegate的类,System.MulticastDelegate又派生自其基类System.Delegate。C#编译器能识别这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况。这是C#与基类共同合作,是编程更易完成的另一个范例。
定义好委托后,就可以创建它的一个实例,从而用它存储特定方法的细节。
但是,在术语方面有一个问题。类有两个不同的术语:“类”表示比较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍成为委托。必须从上下文中确定委托的确切含义。
2.2 使用委托
下面的代码说明了如何使用委托。这是在int上调用ToString()方法的一种相当冗长的方式:
private delegate string GetAString();
static void Main(string[] args)
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("string is {0}", firstStringMethod());
//with firstStringMethod initialized to x.ToString(),
//the above statement is equivalent to saying
//Console.WriteLine("string is {0}",x.ToString());
}
在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使用它引用整型变量x的ToString()方法。在C#中,委托在语法上总是接受一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。
为了减少输入量,只要需要委托实例,就可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。下面两个语句是等效的。
GetAString firstStringMethod = new GetAString(x.ToString);
GetAString firstStringMethod = x.ToString;
C#编译器创建的代码是一样的。
委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件是基于委托的。
委托的一个特性是它们的类型是安全的,可以确保被调用的方法的签名是正确的。但是有趣的是。它们不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法,还是实例方法。
给定委托的实例可以引用任何类型的任何对象上的实例方法或静态方法——只要方法的签名匹配于委托的签名即可。
2.3 Action<T>和Func<T>委托
除了为每个参数和返回类型定义一个新类型委托类型之外,还可以使用Action<T>和Func<T>委托。泛型Action<T>委托表示引用一个void返回类型的方法。这个委托类存在不同的变体,可以传递至多16种不同的参数类型。没有泛型参数的Action类可调用没有参数的方法。Action<in T>调用带一个参数的方法,Action<in T2, in T2>调用带两个参数的方法,以此类推。
Func<T>委托可以以类似的方式使用。Func<T>允许调用带返回类型的方法。与Action<T>类似,Func<T>也定义了不同的变体,至多也可以传递16个参数类型和一个返回值类型。Func<out TResult>委托类型可以调用带返回类型且无参数的方法,Func<in T, out TResult>调用带一个参数的方法,以此类推。
2.4 多播委托
前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。但是,委托也可以包含多个方法。这种委托成为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用后最后一个方法的结果。
如果正在使用多播委托,就应知道对同一个委托调用方法链的顺序并未正式定义。因此应避免编写依赖于特定顺序调用方法的代码。
通过一个委托调用多个方法还可能导致一个大问题。多播委托包含一个逐个调用的委托集合,如果通过委托调用的其中一个方法抛出一个异常,整个迭代就会停止。在这种情况下,为了避免这个问题,应自己迭代方法列表。Delegate类定义GetInvocationList()方法,它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。
Delegate[] delegates = firstStringMethod.GetInvocationList();
foreach (Action d in delegates)
{
try
{
d();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
2.5 匿名方法
到目前为止,要想使委托工作,方法必须已经存在(即委托是用它将调用的方法的相同签名定义的)。但还有另外一种使用委托的方式:即通过匿名方法。匿名方法是用作委托的参数的一段代码。用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。
string mid=", middle part,";
Func<string, string> anonDel = delegate(string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(anonDel("Start of string"));
匿名方法的优点是减少了要编写的代码。不必定义仅由委托使用的方法。在为事件定义委托时,这是非常显然的。这有助于降低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行速度并没有加快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。
使用匿名方法时必须遵循两个规则。在匿名方法中不能使用跳转语句(break、goto或continue)跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句也不能跳到该匿名方法的内部。
在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。
如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。此时与复制代码相比,编写一个命名方法比较好。从C#3.0开始,可以使用Lambda表达式替代匿名方法。有关匿名方法和Lambda表达式的区别,参考本分类下的《匿名方法和Lambda表达式》
3. Lambda表达式
自从C#3.0开始,就可以使用一种新语法把实现代码赋予委托:Lambda表达式。只要有委托参数类型的地方,就可以使用Lambda表达式。前面使用匿名方法的例子可以改为使用Lambda表达式:
string mid=", middle part,"
;
Func<string, string> lambda= param=>
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteAxiom3D:Ogre中Mesh网格分解成点线面。 - 天天不在 阅读原文» 这个需求可能比较古怪,一般Mesh我们组装好顶点,索引数据后,直接放入索引缓冲渲染就好了.但是如果有些特殊需要,如需要标注出Mesh的顶点,线,面这些信息,以及特殊显示这些信息。
最开始我想的是自己分析Mesh里的VertexData与IndexData,分析顶点时查找源码发现Ogre里本身有相关的类,这里Axiom3D与Ogre的源码有些区别,不过大致意思相同。
主要用到的类:EdgeListBuilder,CommonVertexList,EdgeData。
流程很简单,EdgeListBuilder添加Mesh,分析Mesh里的顶点与索引缓冲,调用方法Build生成CommonVertexList与EdgeData。下面让我们来看下具体流程。
首先我们要注意到,不管是Mesh或是别的可渲染元素,并不一定是一个VertexData(顶点缓冲)对应一个IndexData(索引缓冲),如Mesh几个SubMesh共用VertexData的情况。所以EdgeListBuilder保存了一个VertexDataList与IndexDataList,中间还有一个关联性列表,在Ogre中是一个Geometry结构,包装顶点索引IndexData,顶点索引列表索引indexSet,顶点缓冲列表索引vertexSet,顶点索引渲染方式OperationType。在Axiom里,是一个和顶点索引IndexDataList长度一样的对应整形数组indexDataVertexDataSetList,其实和Ogre一样,在IndexDataList里的索引直接放入indexDataVertexDataSetList就能得到vertexSet.也还有一个OperationTypeList,同理,长度与IndexDataList一样。
再说CommonVertex,这个结构只有五个字段,分别是vector3类型的position(顶点位置),index(在对应IndexData里的索引),vertexSet(VertexDataList里的索引),indexSet(IndexDataList里的索引),originalIndex(在CommonVertexList里的索引).大家不要搞混了几个带index的索引.假设我要找第二个索引缓冲区里第六个数据对应在顶点索引的值,那么在这个里面,indexSet=2,index=6.大致就是IndexDataList[2][6]这个值.设p=IndexDataList,我们要在对应顶点索引的值,就如下计算VertexDataList[vertexSet][p]就是这个顶点的值了. 还有一种更简单的方法,CommonVertexList包含了顶点位置,而CommonVertex里的originalIndex是指向CommonVertexList的索引,所以可以直接从CommonVertexList[originalIndex]得到顶点位置,如果只考虑顶点位置不考虑法线,纹理坐标,颜色等,这将是一个更好的选择.
EdgeData主要包含二个列表,一个是TriangleList(三角形列表),一个是EdgeGroupList列表,其中一个EdgeGroup对应一个vertexData,多个Edge.Edge好理解,就是我们要的边,二点一线,属性分别是triIndex(TriangleList中的索引),一边可以供二个三角形共享.vertIndex(对应EdgeGroup里的vertexData里的位置).sharedVertIndex(对应CommonVertexList)里的索引.
EdgeListBuilder的Build方法,就是填充上面的CommonVertexList与EdgeData的关系.
调用Build后,首先与VertexDataList里的VertexData对应一一生成EdgeGroup,然后根据索引缓冲集合(IndexDataList)生成三角形网格,索引是告诉GPU如何渲染的,在这里,也就是告诉我们如何生成三角形的.
在这里生成三角形有一个焊接的过程,可能会合并顶点,合并顶点有五种策略,一是合并所有,二是合并在同一索引缓冲区的,三是合并在同一顶点缓冲区的,四是合并相同顶点缓冲与索引缓冲区的,五不合并.在这里,首先会选择一合并所有,然后是二,三四,五.在合并1-4的时候会减少顶点,这样可能会产生一个问题,原来都是三角形二二间共用一边,但是合并后,可能会造成超过二个三角形共用一边,这样就会造成不合法的网格.所以差不多是这样,一如果合并的顶点不造成错误网络,就用第一个策略,否则会采用第二个,一直到最后的第五种策略.
生成完三角形后,根据三角形生成对应的边Edge.
从Ogre与Axiom3D里对这些的引用来看,主要是用于阴影计算,我也是用于偏门,用来展示模型的点,线,面等元素.但是按照EdgeListBuilder实现的功能来看,我们可以把一些相同材质的模型合并成一个模型(有些局限,模型顶点包含的信息最好只有顶点位置,法线),在Ogre与Axiom3D也没看到有此的相关应用,可能这个想法不太完善,我会在后面尝试这个效果.
其中顶,线,面相关分解也只是一个初稿,故相关代码就暂时不放了,等综合考虑相关功能定下相应方法属性后再给出如何分解重组的代码.
本文链接:Axiom3D:Ogre中Mesh网格分解成点线面。,转载请注明。
阅读更多内容
没有评论:
发表评论