在动态编程时,我们常常需要运行时确定调用对象的哪个属性或哪个方法。这个任务通常可以用反射来解决。但众所周知,反射的性能要比静态指定的方式低很多,因为反射要通过运行时复杂的机制完成。能否获得性能和灵活性兼备的动态调用?我在开发VBF的最新功能时反复考虑了这个问题。我们通常动态调用一个对象的属性是采用这样的手法,假设对象a有一个属性叫做MyProp:
Type t = a.GetType();
PropertyInfo pi = t.GetProperty("MyProp");
string value = (string)pi.GetValue(a, null);
注意到什么问题了吗?我们知道这个属性的类型是string,也知道它没有参数。当然也有不知道即将调用的属性类型及参数的时候,但这个场合我们知道,却没有利用,还是当成什么信息都不知道一样使用纯动态的手法获取。这样我们就错失了能利用强类型特性加速这一过程的良机。同样还有方法调用,我们有时候只是方法或属性的名字在编译时不知道(比如需要用户指定),但方法或属性的类型及签名我们是知道的,这种情况下就可以用泛型和委托技术高性能地调用。
泛型技术为处理类型提供了方便,除此之外,.NET的委托还具有一些额外的良好特性。委托可以担当类似接口的任务,但与接口最大的不同就在于,方法无须声明自己满足某个委托,而只要签名符合,即可赋给委托变量。这样我们就可以利用一组事先声明的委托,处理千变万化类型的属性与方法。
C#不允许属性带有参数,除非是索引器。VB允许属性带有参数但很少有人真的大量使用。于是在真实世界中属性的getter和setter的形式就被限定了,绝大部分属性的getter和setter可以用以下两个委托表示:
public delegate void PropertySetter<T>(T value);
public delegate T PropertyGetter<T>();
有了这两个委托,我们就可以对已知类型但名字需要动态化的属性进行高速的强类型动态访问了。方法是使用反射获取属性Gettet或Setter的MethodInfo,再使用MethodInfo创建委托:
Type t = a.GetType();
PropertyInfo pi = t.GetProperty("MyProp");
MethodInfo getter = pi.GetGetMethod();
PropertyGetter<string> strPropGetter =
(PropertyGetter<string>)Delegate.CreateDelegate(
typeof(PropertyGetter<string>), a, getter);
string value = strPropGetter();
注意,这个方法在调用前进行了更多反射操作,因此,如果你只想一两次地获取属性的值,这种方法还不如直接用放射来的快。但是,当你需要对同一属性进行成千上万次访问时,绝对值得多写这点代码,在string类型的简单属性上,速度可比直接反射获取最多快达1000倍,这是我实测的结果。
接下来我们讨论有index的属性和方法的调用。C#尽允许在索引器的语法上使用属性参数,而在VB看来,索引器不过是类所有带参数的属性中比较特殊的一个,他得到了在对象上使用数组语法访问的特权。不管怎么说,无论是索引器还是普通带参数的属性,他们的getter和setter过程都不像典型属性那样简单。同样还有对方法的调用,方法的签名千变万化,似乎我们很难用预先定义的委托统一进行调用。事实的确如此,不过与针对每一种属性访问器或方法的签名定义一种委托的做法相比,泛型还是给出了一种稍微舒服一点的做法:
public delegate R Func<R>();
public delegate R Func<T0, R>(T0 a0);
public delegate R Func<T0, T1, R>(T0 a0, T1 a1);
public delegate R Func<T0, T1, T2, R>(T0 a0, T1 a1, T2 a2);
这样一组泛型委托,可以涵盖参数数目从0-3,有返回值并且没有参数是out或ref的所有方法签名。你还可以定义一组用于无返回值的。有了这样一组泛型委托,就可以在想要某种函数的签名时直接创建出来,而无须声明新的类型。再结合刚才的手法,就可以用统一的手法实现大部分带有参数的属性或方法的动态调用——同时获得动态名称和强类型性能的双重好处。
也许你早已经利用了类似的手法,并用于除了动态调用属性或方法以外的其他任务。我只是在开发VBF时想到了他们,希望能对部分需要的人有所帮助。
除了J#外,所有微软支持的.NET开发语言现在均支持运算符重载,因此纯粹为C#简化写法一样特性现已成为一种.NET开发中值得研究的一项重要语言特性。有人认为运算符重载其实就是简化写法,满足模拟基本类型操作的小功能,没有必要深究。但我觉得要多思考一层,为什么我们总希望模拟基本类型的操作?因为运算符重载能够将操作中缀化,能够自动推测静态过程的主体。
首先是操作中缀化。函数调用其实是一种前缀操作,函数(代表操作)总是在参数(代表操作数)之前写出。这样执行序列操作时运行的顺序其实和书写的顺序相反:
H(x,y)
G(H(x, y), z)
F(G(H(x, y), z), w)
序列运行的顺序是H->G->F但是却要反过来写,二元参数距离函数名越来越远。我们按照计算机执行的顺序思考,却要反过来写,多少有些不爽。成员函数与扩展方法的写法则是将操作数(对象)写在前面:
x.H(y)
x.H(y).G(z)
x.H(y).G(z).F(w)
这样就将书写的顺序正过来了。这是一个甚好的方案,但是在不具备扩展方法的今天,有些事情是成员函数做不了的。比如在我的VBF里,我希望Functor<T, bool>可以进行And, Or等逻辑运算,而Functor<T, int>之间只能进行算术运算,Functor<T, string>之间只能进行连接运算,而且规则还不一样……但是成员函数没有根据类型参数选取不同重载的能力,也就是说.NET泛型无法进行特化操作。在.NET中具有编译期类型判定的机制只有两个:函数根据参数类型的重载和用户自定义隐式转换(相当于根据返回类型重载)。我们可以用Functor<,>类型的静态方法来实现根据类型参数不同的不同重载。但是静态方法不但要写全类型的名字,还是前缀操作,使用起来让人甚为不爽,这时就会发现,运算符重载是我们梦寐以求的东西。
Type.op_Operator(x, y) '静态方法
x op y '运算符写法
以上两种是等价的,可以看到运算符重载不仅可以通过x,y的类型推测静态方法的调用主体Type,还可以将操作转化为中缀写法——比后缀更适合表现二元运算。既然这么完美,我们能不能这样写呢?
Class Functor(Of T, U)
Public Shared Operator And(x As Functor(Of T, Boolean), y As Functor(Of T, Boolean)) _
As Functor(Of T, Boolean)
End Operator
End Class
很遗憾,这样会编译错误。作为运算符重载过程,其参数至少有一个必须是定义运算符的类型。在编译器看来,必须是Functor(Of T, U),两个类型参数都必须是该泛型类定义的。就在我对此大感抱怨时,我偶然在C#编译器的源代码(见Rotor)中看到了它识别运算符的规则,其中并没有这些限制,只有两条规则——方法必须是静态的,特定名称的方法;方法必须带有specialname属性。那么我们完全可以骗过编译器,不用它提供的Operator关键字来声明运算符重载过程,而是使用自己编写特定名称的方法,并加以specialname的手法来打造运算符重载过程:
Imports System.Runtime.CompilerServices
Class Functor(Of T, U)
<SpecialName()> _
Public Shared Function op_BitwiseAnd(x As Functor(Of T, Boolean), y As Functor(Of T, Boolean)) _
As Functor(Of T, Boolean)
End Function
End Class
System.Runtime.CompilerServices.SpecialNameAttribute是一个指示编译器为声明成员添加specialname的特殊属性,C#和VB编译器都支持。op_BitwiseAnd是VB和C#等语言所识别的与操作运算符过程名称。这样写完以后编译成类库,再以引用DLL的方式引用它,你就会看到编译器将他识别成了我们要的运算符重载过程。当你在Functor<T, int>这样的类型上使用And操作时,编译器会告诉你不支持该运算符,仅在Functor<T, bool>上才能进行这一操作,编译错误信息准确无误,真是太棒了。
在我们结束前,我们还可以看看如此手工打造还能突破哪些编译器人为的限制:
可重载Protected和Private的运算符(尽管这样做几乎没有意义)
可不成对重载比较运算符(=, >, >=, <=, <, <>)
可以让移位运算符的第二个操作数不是int(>>和<<样子很好看,但是有了这个限制我们就不能拿它来干别的事情,现在好了)
可以在C#中重载仅VB支持的运算符,也可以在VB中重载仅C#支持的运算符(当然要到对方语言中才能生效)
可以让用户自定义显式转换支持泛型类型参数之间更加神奇的写法
用了这种手法,似乎还可以重载诸如operator+(int, int)之类的运算符,但它们并不能生效。
.NET语言编译器中每一项特性,都可能有隐藏在其表面之下的深层次用途。善加研究后常能发现原来所认识不到的功能。我当然不是在推荐大家乱用运算符重载,只是一种思考,一种新的灵感。
我在介绍Visual Basic 9.0的时候,曾经多次提到Tuple这个概念,当时是作为匿名类型的实例出现的。现在我们单独来讨论一下这个概念。Tuple常常译为“组元”,在大部分支持Tuple的语言中,常常表示成员数目确定,每个成员类型也确定的结构。常常用于表示函数的多个返回值或者查询的结果等。Tuple应当是强类型的,即所有成员的类型在编译时确定。比如,假想语法下
Dim t = New Tuple(Of String, Integer, Double)
那么t将具有三个成员,该数目无法改变;同时三个成员的类型分别为String, Integer和Double,也无法改变。如你所见,Tuple可以看作不用事先声明的结构体,可以根据所使用的场合灵活地创建。那么VB9和C#3的匿名类型当然是Tuple很好的实现方案。但是这都是N年后的东西了,我们在.NET 2.0中能否实现Tuple?最关键的难点在于,我们要在希望使用的地方创建Tuple的结构,而不是事先声明,因此就必须有个灵活的机制来完成。
方法一:TypeList
我是某一天在公共汽车上想到这个办法,后来看到和Loki的TypeList有相似之处。当然.NET没有特化和记录类型的能力,所以无法实现TypeList。但我们把静态类型运算的思路移到运行时,就可以做Typed Variable List——那就是Tuple。
public abstract class TypeNode { internal TypeNode {} }
public sealed class Tail : TypeNode { }
public sealed class Tuple<T, TNode> : TypeNode where TNode : TypeNode, new()
{
public T Field = default(T);
public TNode Next = new TNode();
}
我充分利用了.NET泛型的约束特性来达成我的设计。TypeNode被设计为abstract,因此约束了new()的泛型参数TNode将无法取值TypeNode本身的类型。而其internal的构造函数又限制了用户继承于它。这个手法就将TNode的取值范围限定在Tail和Tuple两个类型上。这个用法是我认为约束用法中相当巧妙的一种。
这个类型的原理很简单,就是利用泛型,在创建TypeList的实例时自动生成相同结构的链表。比如我们要创建一个String, Integer, Double的Tuple,就是这样写:
Tuple<string, Tuple<int, Tuple<double, Tail>>> t;
如你所见,这种Tuple的类型参数第一个是某节点的类型,第二个要么是另一个Tuple,要么是Tail(表示终结列表)。这个对象创建出来以后就会自动生成一个“各个节点类型都不相同”的链表。
t = new Tuple<string, Tuple<int, Tuple<double, Tail>>>();
t.Field = " a string ";
t.Next.Field = 123;
t.Next.Next.Field = 13.56;
Tail没有Next字段,因此遇到Tail就代表Tuple终结了,这可以由编译器检查,因此没有越界的危险。而且这种Tuple可以达到无限长。不过这种方法也是有缺陷的,首先使用的语法方面非常不便,如果要用第7个字段,要写成myTuple.Next.Next.Next.Next.Next.Next.Field,稍不注意就会写错。无论VB还是C#都没有足够的抽象能力简化这一操作。第二个缺陷是建立Tuple时的一连串new操作开销很大,因为这里的new是通过反射进行的。所以受限于语言特性的缺乏,这种方法无法达到很完美的地步,不过这个思路也许在其他场合可以用上。
方法二:重载原型
模仿泛型委托的思路,我们可以用完全泛型化的一系列同名结构来模拟即时创建的Tuple:
struct Tuple<T0>
{
public T0 Field0;
}
struct Tuple<T0, T1>
{
public T0 Field0;
public T1 Field1;
}
struct Tuple<T0, T1, T2>
{
public T0 Field0;
public T1 Field1;
public T2 Field2;
}
......
struct Tuple<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9> {...}
这样就创建了一组Tuple结构。因为名称相同,在使用中不会察觉到存在10个类型,而是“要什么有什么”:
Tuple<string, int, double> t1;
Tuple<string, string> t2;
Tuple<int, int, int, int, float> t3;
......
这和我们一开始假想的语法一样!而且没有任何额外开销,相当完美。但是它的元素数目有限,一开始定义了几个就只能有几个,好在一般不需要太多,10个够用了。不过这样生成的Tuple有点死板,似乎没有什么可以智能化的地方。
我将在我的VBF中采用第二种Tuple方案,斟酌后还是觉得它比较实际。唯一改动的地方就是为每个Tuple结构增加了一个初始化所有成员的构造函数。
大家已经很熟悉C# 2005将加入内置的重构支持,方便用户快速对代码实施常用的重构。重构是十分重要的维护代码的手段,不仅C#的用户需要,VB的用户更加需要。因为从VB6升级来的代码,经过升级向导的“蹂躏”后已经面目全非,而且许多旧的代码风格与新的.NET特性格格不入。因此,重构成了升级旧代码,改善旧代码的重要手段。可惜的是,除了重命名之外,VB2005并没有加入重构特性,这让VB的程序员十分扫兴。好在微软承诺下一个版本支持重构的同时,还委托第三方软件开发商专门为VB2005提供了重构插件,VB2005的用户可直接免费下载安装,这就是
Refactor! for VB2005。Refactor! Pro是由
Developer Express Inc开发的VS插件,可用于所有VS.NET版本和编程语言,而Refactor! for VB2005则只支持Visual Studio 2005和Visual Basic语言,功能也比完全版的Refactor! Pro来的少。但是,它的功能却已经超过了VC# 2005所内置的重构功能。从今天起,我就开始介绍Refactor! for VB2005所提供的重构功能。
Refactor!支持Visual Studio 2005独特的智能标记,即将重构功能置于上下文的智能标记中,而不是在一个“重构”菜单选项中提供选择。这样用户更容易知道在当前的代码上可进行什么样的操作,而不是从一堆不知所措的重构名字中选取。这类智能标记通常要等用户将输入光标置于可重构的对象上才会出现,如子程序或函数名称、变量名称等等。
重新排列参数是Refactor!多项重构功能之一,它可以将函数或子程序的参数重新排列。更神奇的是,他可以将按引用传递的参数转化为函数的返回值,或进行相反的操作。比如有这样一个子程序:
Public Sub Sub1(ByVal a As Integer, ByVal b As String)
'调用方
Call Sub1(10, "str")
通过“重新排列参数”可以将上述程序重构成:
Public Sub Sub1(ByVal b As String, ByVal a As Integer)
'调用方
Call Sub1("str", 10)
再比如有这样的子程序:
Public Sub Sub2(ByRef x As Integer)
'调用方
Dim i As Integer
Call Sub2(i)
同过“重新排列参数”可以将按引用传递的参数移出而变成返回值:
Public Function Sub2()As Integer
'调用方
Dim i As Integer
i = Sub2()
还可以进行相反的重构,将函数的返回值变成按引用传递的参数。有许多VB程序员习惯用参数传出所需要的参数。现在你可以用这个重构功能项,将合适的子程序转变成函数,或进行相反的操作。
进行这项操作的步骤是:1、先保存和编译项目。2、将光标放在函数的定义上,点击出现的智能标记,并选择“Reorder Parameters”。3、按Tab键,选择要移动的参数或返回值,然后按左右方向键移动这个参数,到合适的位置。4、按Enter键确定修改,接下来根据提示将函数所有的调用处更改为新的签名。5、根据需要,进行Rename Local重构,将此重构引入的变量名更改为适当的名字。下面的动画演示了“重新排列参数”重构进行的过程,交换参数的视觉效果确实很酷!(速度较慢请包涵)
重命名本地变量是另一项非常有用的功能。实际上VB2005已经内含安全的重命名功能,但重命名本地变量功能可以将重命名的范围限定在当前的函数或子程序中,同时明显的视觉效果可以让你对被重命名的变量一目了然。操作步骤是:1、选择要重命名的符号,单击智能标记并选择“Rename Local”。2、在绿色的可替换区域内输入新的名称,按Tab键可在所有出现该名称的区域间跳转,可借此观察此项重构的影响范围。3、确定后,按下Enter键。很简单。
前一段时间我介绍了泛型,其中有个很重要的特性叫“约束”。使用约束,可以减小泛型类型参数的取值范围,同时能够允许在泛型类型上应用所约束类型的功能。许多人为了在泛型类型上进行所需的操作,约束大量的类型。事实上,我觉得没有特别要求,泛型的类型参数应该尽量不约束任何类型,因为它会严重缩小你的泛型类的使用范围。而在类型参数上进行操作着一需求,并不是只能通过约束来达成。
规则:当你希望对类型参数实施的操作只用于你的泛型类一部分功能时,用辅助对象代替约束。
比如你希望编写一个容器类,它有很多功能,其中一个是排序。排序需要对象之间进行比较。如果容器类的其他功能并不依赖于排序,那么对整个类的类型参数使用约束就不合适。因为我们有可能根本不用排序,为什么还要有此约束呢。比如你将类写成:
Public Class MyContainer(Of T As IComparable(Of T))
Public Sub Sort()
Dim a, b As T
If a.CompareTo(b) Then
'code
End If
End Sub
End Class
那么T所能取值的范围就大大减少了。我们不妨让用户在需要排序的时候,自行指定比较的依据,而不是强迫类型自己必须能够比较大小。改动成这样:
Public Class MyContainer(Of T)
Public Sub Sort(ByVal compareHelper As Comparison(Of T))
Dim a, b As T
If compareHelper(a, b) Then
'code
End If
End Sub
End Class
这里,compareHelper就充当了辅助对象的功能。辅助对象是独立于类型T之外的,因此T不需要进行任何约束。而用户则必须为该功能提供正确的辅助对象。将辅助对象设置为委托是相当好的,这样用户可以只编写一个方法来规定辅助对性的功能(而不用编写一个完整的类)。C#用户甚至可以用匿名方法就地指定辅助对象的功能。.NET Framework为我们头提供了几个常用的泛型委托,都可以用作辅助对象的类型:
System.Action(Of T) '是一个无返回值的方法,用于进行作用于一个T对象的动作
System.Comparison(Of T) '用于比较了两个T类型的对象
System.Converter(Of T, U) '用于指定将一个T对象转成U类型的规则
System.Predicate(Of T) '用于指定T类型的一个对象是否符合某项判断
当然,我们很容易就可以指定自己的辅助对象类型。不仅仅是委托,还可以是更加复杂的功能。用辅助对象代替约束,可以确保你的泛型类型有最大的应用范围,同时能让用户对特定的功能做深层次的定制。那什么时候用约束?当你的类确实只对某一类对象起作用,换句话说,你想要类型参数具有的功能贯穿整个泛型类的设计,那么约束就是更好的选择了。
最近灵感之源大哥常常在MSN中向我倾诉移植VB6代码的痛苦过程。我一看他的代码——好家伙,这都是原先在VB6种最高深的用法。什么不安全的指针啊,复制内存啊,一应俱全。调用平台的时候指针是少不了的,而VB.NET又不支持,所以麻烦常来,确实让人感到痛苦。我建议他用C#或者MC++,可是他愿意更纯的VB,那只好我来做了。为了能够让指针操作得以在VB中进行,必须对他们进行封装,泛型在这里是个很好的选择。而C#根本不允许对泛型的类型参数使用指针,那我们只有C++/CLI了。
C++/CLI支持两种托管的指针:内部指针和顶指针。内部指针(stdcli::language::interior_ptr)是一个指向托管堆中对象的动态指针。当托管堆中对象发生移动时,内部指针可以同步得到更新。内部指针具有很到的灵活性,他可以和C++本地指针进行类型转换,还可以进行指针运算。定指针(stdcli::language::pin_ptr)也是在一种指向托管堆对象的指针,被定指针所指的对象将被“定”在内存中,CLR不能移动它在内存中的位置,因此定指针可以用来向非托管的代码传递托管对象的指针,它能确保非托管代码操作的时候对象不会移动。最令人欣喜的就是,无论是定指针还是内部指针,他们都能对泛型的类型参数进行操作。所以我们可以封装一段代码来操作指针:
ref class PointerHelper sealed
{
private:
PointerHelper(void)
{
}
public:
generic<typename T> where T : System::ValueType
static T GetValue(IntPtr address)
{
return *(interior_ptr<T>)(void *)address;
}
generic<typename T> where T : System::ValueType
static void UnsafeSetValue(IntPtr address, T value)
{
interior_ptr<T> ptr = (interior_ptr<T>)(void*)address;
*ptr = value;
}
generic<typename T>where T : System::ValueType
static IntPtr UnsafeGetAddress(T %value)
{
pin_ptr<T> pp = &value;
return IntPtr((void*)pp);
}
};
我也不太确定这段代码会是什么效果,来试验一下。这是一段VB的代码:
Dim x As Integer
Dim p As IntPtr = PointerHelper.UnsafeGetAddress(x)
PointerHelper.UnsafeSetValue(p, 123)
MsgBox(x.ToString)
运行下看x的值,真的变了。相信有了这个,灵感之源所遇到的痛苦应该可以减弱一点了吧。至少从指针里获取指的操作不需要复制内存来做了。以前无法获取变量的地址现在也可以做到了。灵感之源只需要耐心等到VS2005发布,就可以轻松使用这段代码,逃离苦海了。
不过……这样使用终归是不安全的。VB对指针的类型没有任何检查和约束,我也没办法把这个东西做成强类型的。如果有人愿意,他完全可以这样写
Dim x As Long
Dim p As IntPtr = PointerHelper.UnsafeGetAddress(x)
PointerHelper.UnsafeSetValue(p, 123.0F)
MsgBox(PointerHelper.GetValue(Of Guid)(p).ToString)
这完全是可以运行的,把123.0F的浮点数塞到整型变量的地址里,然后按照Guid取出……不难想象若这种东西滥用起来,就会再次恢复到VB6那种“大师级”无法阅读、维护的代码了。
用Enum的时候,可能会有要遍历Enum中所有已定义值的功能。而当前各种.NET语言都没有提供对此需求的语法支持。我们只能用Enum.GetValues来实现这个任务。但是,Enum.GetValues一来是要用到运行时类型信息,让人不爽;二来得到的数组不直接是所需枚举类型的,需要转换。最好有一种强类型的语法可以帮助我们做这件事。所以,我就写了这个简单的小程序。其实内部还是得用Enum.GetValues,但是只在第一次使用时调用,后面就将这个取得的值集合保存起来,即使多次使用也不会担心性能受损。这个程序用C++/CLI BETA1写成,不适用于C++/CLI CTP或Tools Update。用C++的原因是,C#和VB.NET都对泛型约束语法做了过分的限制,让我写不了这个程序……
首先是ValueCollection类:
generic<typename T> where T : System::Enum
[DefaultMember("Item")] //让Item属性成为默认属性(C#中的索引器)
public ref class EnumValueCollection : public IEnumerable<T>
{
private:
initonly array<T>^ values;
IEnumerator<T>^ enumerator;
protected:
virtual IEnumerator<T>^ GetValuesEnumerator() = IEnumerable<T>::GetEnumerator
{
if (!enumerator)
{
enumerator = ((IEnumerable<T>^)values)->GetEnumerator();
}
return enumerator;
}
private public: // internal: in future version
EnumValueCollection(void)
{
values = (array<T>^)Enum::GetValues(typeid<T>);
}
public:
property T Item[] //参数化属性
{
T get(int index)
{
try
{
return values[index];
}
catch(Exception^ e)
{
throw gcnew ArgumentOutOfRangeException("index",
"This enum does not contain a value of this index");
}
}
}
};
接下来要有一个维护ValueCollection实例Singleton的帮助类:
generic<typename T> where T : System::Enum
public ref class EnumHelper sealed
{
private:
EnumHelper()
{
//不能创建此类的实例
}
static EnumValueCollection<T>^ valueCol;
initonly static Object^ syncRoot = gcnew Object();
public:
static property EnumValueCollection<T>^ ValueCollection
{
EnumValueCollection<T>^ get()
{
if (!valueCol)
{
try
{
Monitor::Enter(syncRoot);
if (!valueCol)
{
valueCol = gcnew EnumValueCollection<T>();
}
}
finally
{
Monitor::Exit(syncRoot);
}
}
return valueCol;
}
}
};
很简单,不用多解释了。这是用法:[Visual Basic]
For Each o As MessageBoxButtons In EnumHelper(Of MessageBoxButtons).ValueCollection
TextBox1.AppendText(o.ToString & vbCrLf)
Next
Dim x As MergeAction = EnumHelper(Of MergeAction).ValueCollection(1)
MsgBox(x.ToString)
用过VB6的人都会对控件数组念念不忘,因为控件数组在处理多个控件统一事件上确实很方便。.NET Framework没有引入控件数组这一概念,这是因为.NET Framework的类型系统很完善,可以实现控件数组原来的功能。只是这样凭空增加了一些麻烦,我们不是需要在Handls字句后面写一长串控件名称,就是要用写数遍AddHandler或C#的+=语句给多个控件绑定同一个事件。为了减轻没有控件数组日子的痛苦,我写了这个替代方案:共享事件容器。在编写它的过程中,我用了泛型,但其实不用泛型也可以做到,只是泛型能够约束容器内控件的类型,减少可能的运行时错误。
在看实现方法之前,先来看看共享事件容器是怎么操作的。首先你先要放一些相同类型控件到窗体上,这些就是你的控件数组元素了,假设是button1,button2和button3。然后在窗体的构造函数或Load事件中添加如下代码:
ba = New ShareEventsContainer(Of Button) '创建一个共享事件容器
ba.AddRange(New Button() {Button1, Button2}) '用AddRange可以添加多个控件
ba.Add(Button3) '当然Add方法也是支持的了
这就是共享事件容器了,就像是一般的集合一样,你可以用数组的语法操纵其中的对象。接下来就是重头戏了,我们要将容器内的全部控件的Click事件都绑定到同一个方法上,比如这个方法
Public Sub Buttons_Click(ByVal sender As Object, ByVal e As EventArgs)
MsgBox("Hello! I'm " & CType(sender, Button).Name)
End Sub
则接着在刚才初始化共享事件容器语句的后面写事件绑定代码:
ba.AddHandler("Click", New EventHandler(AddressOf Buttons_Click))
有点像VB的语法,其实我就是取了个神似。这里事件名称是要用字符串表示的,可别写错了。而事件处理程序的委托也与一般的事件绑定语句不一样,这时必须将委托类型写出来,还得写正确了。VB和C#2.0的委托类型推定在这时是不起作用的。你还可以写更多的AddHandler,绑定其他事件。
好了,就这么简单,事件已经绑定好了。更进一步的是,将来继续往这个容器内添加新的按钮,他们的事件也会自动绑定到这时预定的所有处理程序上,而从容器中移除控件,其事件会自动解除绑定。你还可以在任意地方用AddHandler和RemoveHandler方法添加和删除新的事件处理程序,支持多播事件方法。
如果你觉得这个方法值得用用,那就可以看下面的实现代码了:VB2005 BETA1
Imports System.Collections.Generic
Imports System.Reflection
'包含委托的列表类型
Imports DelegateList = System.Collections.Generic.List(Of System.MulticastDelegate)
''' <summary>
''' 提供一个共享事件容器,容器内的控件可以共享事件处理过程
''' </summary>
''' <typeparam name="T">控件的类型,不支持Menu</typeparam>
''' <remarks>
''' 共享事件容器在事件处理上类似于Visual Basic 6.0或更早版本的控件数组,
''' 它可以将多个控件的事件绑定到同一个处理过程上,方便进行功能一致的操作。
''' </remarks>
Public Class ShareEventsContainer(Of T)
Inherits Collection(Of T)
'保存当前共享事件容器所预定的全部事件
'为了支持多播事件,必须采用这种结构的存储方式
Private events As Dictionary(Of String, DelegateList)
Private controlType As Type
''' <summary>
''' 初始化一个默认的共享事件容器
''' </summary>
Public Sub New()
MyBase.New()
controlType = GetType(T)
events = New Dictionary(Of String, DelegateList)
End Sub
''' <summary>
''' 初始化一个共享事件容器,并添加初始的控件
''' </summary>
''' <param name="controls"></param>
Public Sub New(ByVal ParamArray controls() As T)
MyClass.New()
If controls IsNot Nothing Then
AddRange(controls)
End If
End Sub
''' <summary>
''' 为容器内所有控件绑定统一的事件处理过程
''' </summary>
''' <param name="eventName">要绑定的事件名称,应当是T类型所支持的事件</param>
''' <param name="handler">对应于事件,并且类型正确的委托变量</param>
Public Sub [AddHandler](ByVal eventName As String, _
ByVal handler As MulticastDelegate)
If events.ContainsKey(eventName) Then
events(eventName).Add(handler)
Else
events.Add(eventName, New DelegateList)
events(eventName).Add(handler)
End If
Dim ev As EventInfo = controlType.GetEvent(eventName)
Try
For Each item As T In Me
ev.AddEventHandler(item, handler)
Next
Catch ex As NullReferenceException When ev Is Nothing
Throw New ArgumentException("所请求的事件不存在", "eventName")
Catch ex As ArgumentException
Throw New ArgumentException("事件处理的程序与事件的委托类型签名不符", "handler")
End Try
End Sub
''' <summary>
''' 为容器内所有控件解除绑定统一的事件处理过程
''' </summary>
''' <param name="eventName">要绑定的事件名称,应当是T类型所支持的事件</param>
''' <param name="handler">对应于事件,并且类型正确的委托变量</param>
Public Sub [RemoveHandler](ByVal eventName As String, _
ByVal handler As MulticastDelegate)
If events.ContainsKey(eventName) Then
events(eventName).Remove(handler)
Dim ev As EventInfo = controlType.GetEvent(eventName)
Try
For Each item As T In Me
ev.RemoveEventHandler(item, handler)
Next
Catch ex As NullReferenceException When ev Is Nothing
Throw New ArgumentException("所请求的事件不存在", "eventName")
Catch ex As ArgumentException
Throw New ArgumentException("事件处理程序与事件的委托类型签名不符", "handler")
End Try
End If
End Sub
'在删除项目时,解除该项目的事件绑定
Private Sub WhenRemoveItem(ByVal index As Integer)
Dim ev As EventInfo
For Each eventName As String In events.Keys
ev = controlType.GetEvent(eventName)
For Each handler As MulticastDelegate In events(eventName)
ev.RemoveEventHandler(Me(index), handler)
Next
Next
End Sub
'在插入新项目时,为该项目绑定所有事件
Private Sub WhenInsertItem(ByVal index As Integer, ByVal item As T)
Dim ev As EventInfo
For Each eventName As String In events.Keys
ev = controlType.GetEvent(eventName)
For Each handler As MulticastDelegate In events(eventName)
ev.AddEventHandler(item, handler)
Next
Next
End Sub
Protected Overrides Sub RemoveItem(ByVal index As Integer)
WhenRemoveItem(index)
MyBase.RemoveItem(index)
End Sub
Protected Overrides Sub ClearItems()
For i As Integer = 0 To Me.Count - 1
WhenRemoveItem(i)
Next
MyBase.ClearItems()
End Sub
Protected Overrides Sub InsertItem(ByVal index As Integer, ByVal item As T)
WhenInsertItem(index, item)
MyBase.InsertItem(index, item)
End Sub
Protected Overrides Sub SetItem(ByVal index As Integer, ByVal item As T)
WhenRemoveItem(index)
WhenInsertItem(index, item)
MyBase.SetItem(index, item)
End Sub
''' <summary>
''' 添加多个项目到共享事件容器中
''' </summary>
''' <param name="items">包含要添加项的可列举容器</param>
Public Sub AddRange(ByVal items As IEnumerable(Of T))
For Each item As T In items
Add(item)
Next
End Sub
End Class
其实我写的也有些仓促,还没有仔细测试,估计还有不少毛病。不过这个思路的确可以简化不少使用控件数组方面的麻烦,我今后也会不断改进和完善它的。
记得在Framework 2.0还叫1.2的时候,我曾经进行过几个简单的实验,以便测试对带有约束的泛型类中,类型参数对象的方法调用的性能。其结果令我非常失望——类型参数对象的方法调用比用接口调用甚至通过Object传递的方式都慢得多,泛型的优势完全没有体现出来。到了BETA1,我又可以重复这个简单的实验,结果却大大不同了。这是我的四个实验方法:
Private Sub Test1(Of T As ITest)(ByVal obj As T)
obj.Test()
End Sub
Private Sub Test2(ByVal obj As ITest)
obj.Test()
End Sub
Private Sub Test3(ByVal obj As Object)
DirectCast(obj, TTest).Test()
End Sub
Private Sub Test4(ByVal obj As TTest)
obj.Test()
End Sub
其中用到的结构及接口定义如下:
Private Interface ITest
Sub Test()
End Interface
Private Structure TTest
Implements ITest
Public i As Integer
Public Sub Test() Implements ITest.Test
'no code
End Sub
End Structure
很简单,第一个方法用了约束ITest的类型参数T作为参数;第二个是用ITest接口作为类型参数;第三个方法则是用Object作为参数,并在调用前直接拆箱;第四个直接用结构作为类型参数,想象中应该是它最快……测试用的代码已经是老生常谈了:
Dim w As New Stopwatch
'Static 让所有测试共享一个预先创建结构的实例
Static testObj As New TTest
w.Start()
For iLoop As Integer = 0 To 10000000
Test1(testObj) 'Or Test2, Test3, etc
Next
w.Stop()
TextBox1.AppendText(w.ElapsedMilliseconds & vbCrLf)
结果为Release下,不开特殊优化,预运行10次得到的(毫秒):
泛型:208
接口:394
Object:329
直接结构:209
我也很震惊,早先性能很差的泛型版本竟然超过了直接结构调用!而接口参数的速度反而成了最慢。我对这几个月来,.NET Framework组在泛型性能上的工作十分满意,但愿在正式版还有上佳表现。
我们在编写程序的时候,常常要使用Try Catch来进行异常的捕获。有时,将暂时无法处理的异常继续向外抛出也是十分常见的用法。完成这项操作可以通过两种方式,第一种是判断后继续抛出异常;第二种则是通过filter进行条件过滤,只捕获条件符合的异常。我们分别用两个子程序演示他们:
'检查后重新抛出异常
Private Sub Test1()
Dim a As Integer = 0
Try
a = 2 \ a
Catch ex As Exception
If a <= 0 Then Throw
End Try
End Sub
'用过滤器根据条件捕获异常
Private Sub Test2()
Dim a As Integer = 0
Try
a = 2 \ a
Catch ex As Exception When a > 0
End Try
End Sub
注意到区别了吗,一个是通过Throw语句完成,而另一个则是通过When语句。我用的抛出异常的语句是a = 2 \ a,它产生的被零除异常速度非常快,要比直接抛出Exception的实例还要快的多,只有这样我们才能将比较的焦点放在捕获上。
这是测试的代码:
Dim w As New Stopwatch()
For i As Integer = 1 To 50
w.Start()
Try
Test1() 'Or Test2()
Catch ex As Exception
End Try
w.Stop()
Next
TextBox1.AppendText(w.ElapsedMilliseconds & vbCrLf)
测试进行在Release,不开优化(以免编译器改变机理)的情况下,预运行10次的条件下,这是两者的对比(毫秒):
Throw版:341
Filter(When)版:169
性能差距极大!Throw是一个非常慢的操作。相比之下Filter则十分快速。此处只循环了50次,就能有数百毫秒的差距,可见此性能差距是日常程序里肉眼就能看出的严重差距。建议大家需要按条件捕获异常时,尽量用When语句,以获得Filter的性能好处。
在.NET开发中,保持线程安全的默认手段是采用System.Threading.Monitor加锁以确保对共享数据访问的同步性。我们已经很习惯于用SyncLock语句来实现这个功能。但是锁住什么变量也是有讲究的。如果一个线程过程所用的同步变量被别的过程锁住而又不放开的话,这个线程就会陷入永久的沉睡。普遍方案是:
1、锁住被访问的数据本身。对于只存在一种线程过程的情况可以使用,但若有很多种不同的线程过程都要访问这数据,就很危险了。
2、锁住容器的实例。常见是锁住thMe。也有锁住Me.GetType()的,都是相似的原理。这种方法的最大缺陷就是Me尤其是Me.GetType()都是可以被对象外部访问到的对象,如果从外部锁住他们而且不放开,对象内的线程就被锁死了。这是相当危险的。
3、锁住特殊的同步根。常常是类型的字段,没有任何功能,只是用来维持同步的。这种变量从外部是无法锁住的,但类内部的其他过程仍可锁住他们,导致死锁。
所以我想出一种方案:锁住局部静态变量。如下所示:
Static syncRoot As New Object()
LyncLock syncRoot
'Code
End SyncLock
局部静态是无法被其他过程访问的,因此这个同步根就成了本线程过程专用的。不用担心任何其他人可以锁住它。而局部静态变量的生命周期与类的字段一样,完全可以胜任这个工作。
通过长时间分析Microsoft.VisualBasic.dll我发现了一个重要的问题:首先,Visual Basic语言运行时是要依靠Microsoft.VisualBasic.dll这个运行库的,其中类型转换、字符串比较和异常处理更是直接依靠该运行库中的类型。所以,Visual Basic开发应用程序无法不引用这个运行库。但是,大量的迹象表明,Microsoft.VisualBasic.dll是用Visual Basic开发的,不但Visual Basic的总设计师承认了这一点,运行库中的很多代码也体现了这一点,比如大量运用可选参数,以及使用了C#无法生成的try filter特性。那么是怎么用Visual Basic开发运行库自身的呢?它总不能引用还没有开发出来的自己吧?
另外一件事,我发现C#不允许将普通函数声明为类型的名字,比如Form1中不允许出现
public void Form1()
这样的声明。为什么呢?怕和构造函数弄混吗?但是构造函数没有返回类型,这是不可能弄混的啊。我用Visual Basic生成了与类型名一样的函数,然后拿到C#中调用,发现他能够很好地处理这种情况。那为什么不允许自己声明成这样呢?Visual Basic就没有这样的问题,虽然Visual Basic将New指定为构造函数的名字,但是却可以声明叫做New但不是构造函数的函数,只要这样写就行了。
Public Sub [New]()
语言设计者的想法,有时候很难捉摸。
在讨论My名字空间的时候,IceShark让我看了一个他写的类库,这是一个封装了大量Windows常见操作的类,和My的思想颇有异曲同工之妙。其中一个命名空间让我倍感兴趣:DesignPatterns.CreationalPatterns。竟然能将设计模式做成类库,这是我前所未见的。我特地看了一下他的Singleton类,只有一个Instance()静态方法,当然是为了获取唯一的实例用的。但Singleton类没有其他任何功能,我们要使用显然要继承它。但问题是,继承一个Singleton类还是Singleton吗?我们看看Instance()的实现方法(在此对作者表示歉意,原库是没有源代码的,我进行了一点反向工程):
public static Singleton Instance()
{
if(__Singleton == null)
__Singleton = new Singleton();
return __Singleton;
}
清楚地看到,new是作用在Singleton上的,只能返回Singleton的实例,而不能返回他子类的实例。此外,子类的变量也不能引用父类的实例,所以继承这个Singleton类根本不能做成能用的Singleton。那我们究竟有没有办法实现继承后的类仍是Singleton呢,我觉得可以尝试一下泛型:
Public Class Singleton(Of T As {Singleton(Of T), New})
Private Shared _instance As T
Private Shared _syncRoot As New Object
Public Shared Function GetInstnce() As T
If _instance Is Nothing Then
SyncLock _syncRoot
If _instance Is Nothing Then
_instance = New T
End If
End SyncLock
End If
Return _instance
End Function
End Class
现在我们可以试试继承这个类了:
Public Class SingSub
Inherits Singleton(Of SingSub)
End Class
注意,我们这里为什么要约束基类自身,主要是保证T的取值范围,否则就会做成工厂类了。这里的SingSub类的确像是一个Singleton了,可以作用这种语法使用它:
Dim s As SingSub = SingSub.GetInstnce()
但是,又一个问题出现了,这里约束了New,就必须让子类有一个可公共访问的构造函数,这可与Singleton大相径庭。私有他的构造函数又不能采用这个约束。不知大家对这种情况有没有什么好的解决方法,或者说Singleton模式还是无法写成类库的。
.NET Framework 2.0 BETA的Console类加了很多新的属性和方法,现在可以改变背景颜色,字符颜色等等,还能移动控制台的缓冲区。如
Imports System.Console
Imports System.Threading
Module Module1
Sub Main()
BackgroundColor = ConsoleColor.Green
ForegroundColor = ConsoleColor.Magenta
Clear()
WriteLine("Hello World")
For i As Integer = 1 To 20
MoveBufferArea(i - 1, i - 1, 1, 1, i, i)
Thread.Sleep(100)
Next
Read()
End Sub
End Module
效果如图:
