装配中的脑袋

用程序装配大脑,再用大脑装配程序
随笔 - 118, 评论 - 1214, 引用 - 11

导航

关于

如果想发较大的信件,请用Ninputer @ gmail.com

不要在我的Blog评论中张贴广告,除非同意向我付款。

标签

每月存档

最新留言

广告

【第1页/共9页,121条】
首页
前页
1
2006年06月30日
CER是.NET 2.0 CLR方面的重要改进,旨在帮助那些对稳定性高度苛刻的程序对付.NET Framework的不稳定因素。因为普通的程序很少会用到,所以一直没有对这个特性加以足够重视。现在碰巧在翻译书籍的过程中用到,就一起来学习一下。
首先,需要提到异步异常的问题。异步异常就是指OutOfMemoryException、StackOverflowException和ThreadAbortException等系统异常。说他们“异步”是因为他们可以在线程代码执行到任何地方的时候发生。一般的异常,比如FileNotFoundException是由代码自己产生的,因此可以用Try语句正常捕捉和处理。而异步异常则是CLR产生的。而且,这些异常都预示着非常严重的错误,代码自己通常都会手足无措。比方说内存耗尽了,代码自己即使Catch了也无济于事,都不知道刚刚哪一步出的问题,也不知道该怎么继续执行。ThreadAbortException通常是由Thread.Abort方法引发,如果要Abort的线程正在进行很关键的人物,比如修改一个全局对象的状态,那么发生ThreadAbortException可能会让整个程序的状态受损,进而产生错误的行为。因此,需要有种机制告诉CLR,我们要进行的事情很关键,不容打断,这就是CER——Constrained Execution Region。
声明CER很简单,先调用System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions()方法,再紧接一个Try...Catch...Finally块即可。注意,这个Try必须紧接着PrepareConstrainedRegions()方法,而且,只有Catch和Finally块的内容成为CER。如下所示
RuntimeHelpers.PrepareConstrainedRegions()
Try
Catch
    
'注意,这里是CER
Finally
    
'这里也是CER
End Try
一般情况下都用Finally块来做CER。CER与普通代码不同,在CER执行期间CLR不能发出异步异常。因此CLR就必须采取一些措施。首先CER会将ThreadAbortException推迟到CER结束之后才发生,这比较容易做到。第二,为了避免OutOfMemoryException,CLR会将CER中用到的所有方法(注意,这里是从代码静态观察,而不是实际调用的方法)以及这些方法所调用到的所有方法全都编译成本地代码,然后根据情况预测可能的内存不足并提前到CER之前引发。然而,这个方法并不能对付堆栈益处错误,所以这个方法会事先保留48K的栈空间以防万一。然而根据MSDN文档,StackOverflowException还是可能会发生的。
为了确保CER这种原理能够工作,首先CER之内不能在堆上进行任何分配操作,包括后台进行的分配操作。除了不能用New分配引用类型的对象之外,也不能进行装箱、线程同步锁操作或者访问多维数组。
刚才介绍到,CLR会事先编译CER中所有用到的方法以及它们各自调用的所有方法。那么聪明的人一定能看出一个问题,那就是通过委托和虚函数机制调用的方法无法事先准确判断,因而就无从准备。因此,RuntimeHelpers还提供了两个方法——PrepareMethod和PrepareDelegate。调用之前务必用这两个方法准备所有虚函数的实际版本和委托变量。使用CER是需要极其小心准备的,因此不是随随便便使用的特性。RuntimeHelpers还有许多其它方法对应各种有变数的情况。总之,CER的宗旨就是在执行之前将所有可以知道的情况尽数分析透彻以便提前判断CER中的操作到底有没有可能顺利完成。
下面用ThreadAbortException来做一个试验,因为这个异常是最容易引发的:
Imports System.Runtime.CompilerServices
Imports System.Threading

Module Module1

    
Dim globalArray() As Integer

    
Sub Main()

        globalArray 
= New Integer(50000000) {}


        
Dim t As New Thread(AddressOf Thread1)
        t.Start()
        t.Abort()
        t.Join()

        Console.WriteLine(AllEquals(globalArray, 
100))
    
End Sub


    
Function AllEquals(Of T)(ByVal arr() As T, ByVal value As T) As Boolean
        
For i As Integer = 0 To arr.Length - 1
            
If Not arr(i).Equals(value) Then Return False
        
Next
        
Return True
    
End Function


    
Sub Thread1()
        RuntimeHelpers.PrepareConstrainedRegions()
        
Try
        
Finally

            
For i As Integer = 0 To 50000000
                globalArray(i) 
= 100
            
Next

        
End Try
    
End Sub


End Module
 先把准备CER的代码注释掉,可以发现这个方法不是总能执行成功的,ThreadAbortException可能会将数组的操作打断,以至于留下不正常的状态。如果在你的计算机上该方法不会失败,可以尝试改变数组的大小。接下来应用CER,会发现出现异常时程序执行的速度剧烈下降,但是最终方法总能够成功地完成。这就是CER所带来的好处。

posted on 2006-06-30 15:57:00 by Ninputer  评论(18) 阅读(7132)

 
2006年06月02日
本文为一些泛型反射技巧的简单罗列,为日后的文章做准备。
1、如何获得一个封闭构造类型(closed constructed type)的Type对象?
假设有如下的类型:
class TestType<T>
class TestType<T, U>
如果要获得封闭构造类型的Type对象,那么只需要用C#的typeof运算符,或者VB的GetType运算符作用于具体类型即可:
//C#
Type t1 = typeof(TestType<int>);
'VB
Dim t2 As Type = GetType(TestType(Of String))

2、如何获取一个泛型类型(generic type)的Type对象?
所谓泛型类型,就是有类型参数,但类型参数还未指定的原始定义。我们不能用TestType<T>这样的语法,因为T在我们的上下文中不存在。这时,可以用空的尖括号(C#)或空的Of语句(VB)来获取。
Type t1 = typeof(TestType<>);
Type t2 
= typeof(TestType<,>);
Dim t1, t2 As Type
t1 
= GetType(TestType(Of ))
t2 
= GetType(TestType(Of ,))
注意,我们可以用逗号来区别类型参数的个数。这就表明,泛型类型只能按类型参数的多少来重载,而不管有何种约束之类。这里获得的Type,就是类型参数未指定的泛型类型。

3、如何从构造类型的Type对象生成泛型类型的Type对象?
Type类的新增方法可以做到。
//C#
Type ct = typeof(List<int>);

//Get generic type definition
Type gt = ct.GetGenericTypeDefinition();

4、如何获取类型参数的Type对象?
泛型类型的T, U等类型参数,以及运行中的实际取值,都是可以从Type对象获取的。
'VB
Dim t As Type = GetType(List(Of Integer))

'Get the generic arguments, an array
Dim typeArgs As Type() = t.GetGenericArguments()
'Get the first argument: Integer in this case
Dim tArg0 As Type = typeArgs(0)

5、从泛型类型Type对象生成构造类型的Type对象。
通常可以用来从一种构造类型生成另一种构造类型
//C#
Type ct = typeof(List<int>);
Type gt 
= ct.GetGenericTypeDefinition();

//Make another constructed type
//The List<string> in this case
Type ct2 = gt.MakeGenericType(typeof(string));

6、如何取一个开放构造类型(open constructed type)的Type对象?
开放构造类型是最难处理的一个,因为他们的类型参数已经指定,但没有指定为具体类型,而是指定为其他泛型类型的类型参数。这种类型在进行反射重载选取以及反射发出(Reflection Emit)操作的时候尤为重要。我们的手法就是,先从宿主泛型类型的定义中获取类型参数的类型,然后再建造出开放构造类型。这里,我们获得List<T>的构造函数的参数,IEnumerable<T>的类型,注意这里的T是List<T>所定义的,而不是泛型IEnumerable<T>自己的类型参数
'The generic type of List(Of T)
Dim tlist As Type = GetType(List(Of ))

'Get the "T" of List(Of T)
Dim typeParam As Type = tlist.GetGenericArguments()(0)

'the generic type of IEnumerable(Of T)
Dim tienum As Type = GetType(IEnumerable(Of ))

'make the open constructed type
Dim tienumOpen As Type = tienum.MakeGenericType(typeParam)

'只有用这种方法获得开放构造类型
'
你才能用这个语法获得真正想要的构造函数定义
'
因为构造函数定义里IEnumerable(Of T)是一个开放构造类型
Dim c As ConstructorInfo = _
    tlist.GetConstructor(
New Type() {tienumOpen})

大家可以回去结合试验理解这些用法。

posted on 2006-06-02 08:49:00 by Ninputer  评论(8) 阅读(8047)

 
2006年04月14日

注意:未经许可,本系列禁止转载。

本文所介绍的技巧,是我在研究泛型开发不久就发现并成功运用的技 巧。这个技巧是突破.NET泛型限制,达到“看上去很美”境界的法宝。当然本方法也存在重大缺陷,后面我会逐一介绍。 本文同时使用VB和C#语法,以下是泛型方面VB和C#的语法小小对照:

VB C#
Identifier(Of T) Identifier<T>
Identifier(Of T As C) Identifier<T> where T : C

上次我们介绍了约束模型的缺陷和使用外部辅助类代替约束的手法。现在我们继续研究该手法。如上次所说,基本 数值类型Integer, Long和Double等并没有实现什么公共的接口以实现普通加减乘除等运算;在泛型类和方法中,类型 参数的变量之间也不允许使用+、-等运算符,所以我们别无选择,只能使用外部辅助类。以下就是我编写的辅助 类Calculator。他是这样工作的:

Function Add(Of T) (a As T, b As T)As T
    
Return Calculator(Of T).Default.Add(a, b)
End Function

'在另一个方法中
Dim c As Integer = Add(12)
Dim d As Double = Add(1.5888.0)

我们看到,Calculator(Of T).Default能够在T确定为Integer或Double的时候分别给出正确的整数相加及浮点数相 加运算。.NET泛型本身是缺乏这种编译时类型选择功能的,也就是说,无法根据类型参数T的不同自动选择不同的代码 执行。那么是不是只能用运行时If语句加类型判断了呢?如果是那样,运行时效率必然难以保证,同时也失去了使用泛 型的意义。所以我们得求助一个工具——类型字典。

“类型字典”这个词是我根据其特性杜撰的,它其实利用 到.NET泛型很重要的一个特性:泛型类的静态字段。如果有一个泛型类型A(Of T),那么它的每一个封闭构造 类型(Closed Constructed Type)独享一组静态字段的取值。也就是说,假设X是A(Of T)的静态字段(VB中 的Shared或C#中的static),那么A(Of Integer).X与A(Of String).X是互相独立的两个字段,改变一个的值不会影响 到另一个。这样就给我们提供了一个极为便利的条件,我们可以利用泛型类型的静态字段,为类型参数的每一组封闭取 值(封闭的意思是所有取值为非泛型类型或泛型封闭构造类型)保存一个字段的值。简单地说,这就相当于以类型作为 关键字,建立了一个字典,其值则是一个对象。以下为C#语法的一个小小的例子:
class TypeDict<T>
{
    
public static int Value;
}

//在代码中,我们可以为每个封闭类型保存一个int
//这相当于一个 类型到int 的字典
TypeDict<int>.Value = 1;
TypeDict
<string>.Value = 2;
TypeDict
<List<int>>.Value = 3;
TypeDict
<List<double>>.Value = 4;

从类型查询到其相应的值,只有访问一个静态字段的代价,可谓极低。下面我们就利用这个优势,实现根据类型参 数的不同,自动给出适合该类型的算术运算功能。我们统称这种类型参数取不同取值,类的功能就自动变化的功能 为type traits,意为“类型特征”。.NET泛型不支持特化,因此不能做编译期选择的type trait,只能将这个过程推迟 到运行期。为了减少运行期的消耗,我们采用首次运行进行类型判断,然后用类型字典保存判断结果的手法,我称之为 首次缓存的拟type traits。下面用四则运算为例介绍做法:为了简化,我们现在只考虑四则运算功 能中的加法。以下是在同类型上实现加法的接口:
Public Interface ICalculator(Of T)
    
Function Add(a As T, b As T)As T
    
End Interface

我们要给ICalculator接口提供一个具有type trait功能的默认实现,首先声明一个类型:
Public MustInherit Class Calculator(Of T)
    
Implements ICalculator(Of T)

    
Public MustOverride Function Add(a As T, b As T) As T _
        
Implements ICalculator(Of T).Add
    
End Class

注意这是一个抽象类,我们还没有写完他,但当前要做的事情是,为Integer,Double和T(代表未知类型)编写三 个特化子类,分别封装Integer, Double和未知类型的加法。
Class IntegerCalc
    
Inherits Calculator(Of Integer)

    
Public Overrides Function Add(ByVal a As IntegerByVal b As IntegerAs Integer
        
Return a + b
    
End Function
End Class

Class DoubleCalc
    
Inherits Calculator(Of Double)

    
Public Overrides Function Add(ByVal a As DoubleByVal b As DoubleAs Double
        
Return a + b
    
End Function
End Class

Class ObjectCalc(Of T)
    
Inherits Calculator(Of T)

    
Public Overrides Function Add(ByVal a As T, ByVal b As T) As T
        
'后期绑定的加法运算
        'VB的后期绑定支持运算符重载
        Return DirectCast(CObj(a) + CObj(b), T)
    
End Function
End Class

给特殊类型的子类编写加法出奇的容易,因为类型变成了已知类型,因此运算符也就能够允许使用。注意我我们给 ObjectCalc编写的加法过程使用了后期绑定加法,因此可以支持任何重载+号的类型,但是他的速度要比IntegerCalc和 DoubleCalc中的做法慢很多倍。因此在实际代码中,我为.NET中具有算术加法概念的许多类型,如整型,浮点 型,Decimal和Nullable类型都做了特殊子类。而ObjectCalc则为那些没有考虑到的类型提供低性能,但仍起作用的最 低保证。 下面我们用If语句模拟编译期的类型选择机制,并保存在Calculator(Of T)的静态字段里,做成类型字典。 修改Calculator如下
Public MustInherit Class Calculator(Of T)
    
Implements ICalculator(Of T)

    
Public MustOverride Function Add(ByVal a As T, ByVal b As T) As T _
        
Implements ICalculator(Of T).Add

    
'保存类型T的字典值
    Private Shared defaultCalc As Calculator(Of T)

    
Public Shared ReadOnly Property [Default]() As Calculator(Of T)
        
Get
            
If defaultCalc Is Nothing Then CreateCalculator()
           & nbsp;
Return defaultCalc
        
End Get
    
End Property

    
Private Shared Sub CreateCalculator()
        
Dim calcObj As Object
        
'根据类型进行特殊子类的指派
        If GetType(T).Equals(GetType(Integer)) Then
            calcObj 
= New IntegerCalc
        
ElseIf GetType(T).Equals(GetType(Double)) Then
            calcObj 
= New DoubleCalc
        
Else
            
'未知类型
            calcObj = New ObjectCalc(Of T)
        
End If
        
'强行让编译器认为我们选择类型就是T
        defaultCalc = DirectCast(calcObj, Calculator(Of T))
    
End Sub
End Class

下面我们来看看这是如何工作的,假设我们要进行Integer的计算,那么首先写下Calculator(Of Integer).Default,这是第一次运行,所以它调用CreateCalculator方法。这个方法利用GetType运算符(C#的typeof )判断T是否是我们写过的特殊子类实现的类型,并且给静态字段defaultCalc分配一个特殊子类。这时候Default的值 其实就是IntegerCalc的实例。当你第二次运行Calculator(Of Integer).Default的时候,缓存在Calculator(Of Integer)类型字典中的IntegerCalc实例会被直接调用,不再会进行第一次的类型判断。也就是说从第二次起,我 们Integer运算的加法就可以好像编译期选择的那样,全速执行了。

类型字典+首次缓存的拟type traits可以 漂亮地做出自动根据类型进行选择的功能,但是他也不是没有缺陷的。首先是这种类型选择不能轻易的扩展。假如 将Calculator的代码做到类库里,那么从外部想给他加入一个特殊类型子类是无法轻易做到的,因为基于If的判断语句 不易扩展,改进的做法可能通过一个(普通)字典作登记。或者结合面向对象,不采用这种方法扩充,而由用户自行实 现ICalculator,然后取代默认Calculator(这正是.NET Framework和VBF采用的做法)。第二个缺陷是保存在静态字段 中的对象不会被清理,这样就会越用越多,占据内存。好在这种用于计算或比较类的对象都很小。

下一次,我 们讨论利用反射操纵泛型类型,以达成更巧妙的设计。

posted on 2006-04-14 10:16:00 by Ninputer  评论(51) 阅读(8215)

 
2006年03月23日

这是我第一次发Office开发相关的帖子。说到Office开发,我只能算新手。这次是碰巧开发了一个Excel智能文档项目,其中用到了这个小小的技巧,就发出来让大家看看。
在Excel开发中,工作表上最基本也是最常用的元素就是Range,Range可以表达一个获任意多个单元格或者矩形区域的组合,其复杂程度相当高。如果我们的智能文档程序要与用户打交道的话,势必要编程控制文档中的单元格,或与用户选择的单元格交互。而Range的对象模型并不符合.NET开发人员的习惯,要想获取用户选中区域的形状或者操作特定形状的区域都十分繁琐。而MSDN和VSTO的推销人员们只关心诸如怎么把单元格和数据源或者XML绑定之类,这种“小事”只能靠我们自己动手了。我的任务就是编写一个Range的封装类,将Range中所有单元格和矩形区域转化为易于访问的对象模型。首先我们看看Range的组成,一个普通的Range可以是一个或多个矩形区域的集合,每个矩形区域都由一组连续的列和连续的行组成。其中行使用阿拉伯数字索引,而列采用字母索引。如图所示:

注意,多个矩形区域可以不连续,还可以交叠。每个Range都有一个描述其位置的字符串,称为Range的地址字符串。地址字符串不但包含所有位置信息,还可以被Excel用来直接快速定位,所以我们就以地址字符串为桥梁,编写我们的包装类。
ColumnWrapper类:主要用于吧表示列的字符串“A”,“B”,“AA”等转化为1开始的整数序列,或者相反。我这里用到的算法可以支持无限大的整数与列名字符串互转,但其实Excel只支持到256列。

ColumnWrapper

有了ColumnWrapper,下面就是CellWrapper,表示单个单元格。
CellWrapper

接下来我们要表示矩形区域RectRangeWrapper。我们用来表示矩形区域。当我们需要计算矩形区域的大小时,只要使用着两个角单元格的位置信息即可算出。
RectRangeWrapper

最后是多个矩形区域组成的完整Range,我们用MultiRectRangeWrapper类来描述。它其实就是一个矩形区域的集合。
MultiRectRangeWrapper

好了,现在四个包装类已经全部写完了。用法就简单多了。用Range.Address属性初始化MultiRectRangeWrapper类,就可以得到一个该Range中所有矩形区域的集合,进而轻松访问该举行区域中每个单元格。比如下面这个例子,展示了一些简单的区域操作:
'取得用户选定的区域
Dim myrange As New MultiRectRangeWrapper(Application.Selection.Address)
For Each rectRange As RectRangeWrapper In myrange.Areas
    
If Not rectRange.IsWideRange Then
        
'非扩展选择的区域,比如整行或整列
        '修改左上角的数字格式
        Me.Range(rectRange.LeftTopCell.Address).NumberFormat = "0.00"

        
'将最后一个选中区域复制到剪贴版
        Me.Range(rectRange.Address).Cut()
    
End If
Next

我写这个只是为了我自己写程序方便,并没有考虑太多因素,所以可能写得比较粗糙,功能有限。有兴趣的可以在我这程序的基础上继续改进。

posted on 2006-03-23 11:49:00 by Ninputer  评论(9) 阅读(7938)

 
2006年03月18日

本系列未经许可,禁止转载(包括网络媒体刊载)

.NET泛型的一大特点是在编译阶段对类型参数不做任何假设。也就是说,面对类型参数T和他的变量,你没有什么能做的——不能调用除Object成员之外的任何方法,不能进行大多数运算符的运算等等。它提供了一个叫约束的机制,能在编译期对类型实参的取值进行一些检查。许多人都将约束视为在类型参数上提供操作支持的唯一方法,并大量使用——你有没有约束过IComparable呢?但是,这种做法是不对的,因为约束仅仅能检测声明类型是否实现了某接口或继承自某类,但通过接口和基类实现的多态机制是一个运行时检查的机制,约束没有从任何方面帮助接口和基类工作。实际上,绝大多数情况下,你用一个约束了IComparable的类型参数T来编程序,和直接用IComparable作为你的操作类型没有什么不同。

约束的真正目的是减少类型实参的取值范围,也就是降低抽象性,是一个泛型具体化的手法。只有当你清楚,你的目的就是“约束”本身,而不是想给类型参数增加些操作的时候,才应该使用约束。那么只想给类型参数增加操作应该怎么办呢?我推荐的方法是:将实现该操作的功能封装到一个外部的辅助类中。比如我们常常想约束IComparable,因为我们想比较类型参数对象的大小。有很多人都用这种方法:

[VB]
Sub
Sort(Of T As IComparable(Of T))(arr As T())

[C#]
void Sort<T>(T[] arr) where T : IComparable<T>

显而易见,Sort需要数组元素能够比较大小,那么为什么约束不好呢?注意我们约束的是T实现IComparable(Of T),如果T实现的是IComparable呢,就不能比较大小了吗?约束无法表达“实现了IComparable(Of T)或IComparable”这种情况。更进一步,仅有这样的对象才能比较大小吗?有许多类型并没有实现这两个接口中的任何一个,但是仍有比较大小的可能,我们一旦约束就等于将他们拒之门外。因此约束是不适合解决此类问题的。记住:我们唯一需要的就是比较,因此正确的方法是这样:

[VB]
Sub
Sort(Of T)(arr As T(), comparer As IComparer(Of T))

[C#]
void Sort<T>(T[] arr, IComparer<T> comparer)

 注意我们对T现在没有任何限制,但是要求提供一个IComparer(Of T)的实例。这个IComparer(Of T)的实现和T可以没有关系,因此不仅T可以不知晓它的存在,还可以提供不只一种的比较方法。但是这样做,无形要求用户必须提供一个额外的辅助对象,对于一些显而易见可比较大小的对象(如Int32),这种要求显得有些多余。那我们的解决方法就是提供一个默认的比较器:

[VB]
Sub
Sort(Of T)(arr As T())
    Sort(arr, Comparable(Of T).Default)
End Sub

[C#]
void Sort<T>(T[] arr)
{
    Sort(arr, Comparable<T>.Default);
}

代码中的Comparer(Of T)是System.Collection.Generic下的一个类型,专门用于提供类型默认的比较器。注意到什么了吗?我们现在没有约束了,但是对用户来说,和有约束时的语法一样简单而清晰。比较那些实现了IComparable和IComparable(Of T)的类型时,Comparer(Of T)都提供了支持,而在比较没有实现这类接口的自定义类型时,可自行实现IComparer(Of T)提供比较机制。完美解决。

这种方法还能进行一些约束都实现不了的做法:支持运算符。我们知道在类型参数上实现哪怕最简单的加法都是不允许的,而且没有任何接口可以帮你做到这一点。这时如果能够使用外部辅助类的做法,就能够突破这一恼人的限制,比如VBF就用了下面一个机制来计算类型参数的加法。

[VB]
Function
 Add(Of T)(a As T, b As T, calc As ICalculator(Of T))As T
    Return calc.Add(a, b)
End Function

Function Add(Of T)(a As T, b As T)As T
    Return Calculator(Of T).Default.Add(a, b)
End Function

[C#]
T Add<T>(T a, T b, ICalculator<T> calc)
{
    return calc.Add(a, b);
}

T Add<T>(T a, T b)
{
    return Calculator<T>.Default.Add(a, b);

}

现在剩下的问题就是,诸如Comparer(Of T)和Calculator(Of T)这类默认的比较器和计算器是如何实现的?如何保证所实现操作的高效率?这就是我们下一次的任务——类型字典和Type Traits。

posted on 2006-03-18 14:21:00 by Ninputer  评论(26) 阅读(7813)

 
2006年02月24日
昨天忙了一个晚上,终于发布了新的BETA 1.5。叫BETA 1.5而不是BETA2的原因是我觉得这次发布比较仓卒,品质恐怕还到不了叫BETA2的程度。但是这次更新带来的新特性还是比较多的。下载页面:
http://sourceforge.net/project/showfiles.php?group_id=148613

首先给不熟悉VBF的人简单介绍一下VBF:VBF是一个.NET下的函数式编程类库,提供了创建函数式算法逻辑的基本框架,还提供了一个使用组合子和高阶函数来创建新函数的体系。使用VBF可以实现对象查询、词法解析以及延迟执行逻辑的许多程序。这个版本提供的新功能主要包括:

1、Nullable的支持和IsNull判定

这次提供了对.NET Framework 2.0中Nullable Type的支持,即支持返回Nullalbe<T>类型的函数子(Functor)使用定义在T上的四则运算或比较运算符。对C#而言,这是一个非常方便的特性。比如我们有个一System.Collections.Generic.List<int?>类型的列表list1,下面的代码演示了如何从中查询到所有小于0或者为空的项:
//C#
//定义一个Identity对象用于查询
Identity<int?> IdNInt = new Identity<int?>();

List
<int?> result = list1.FindAll(IdNInt.IsNull() || IdNInt < 0);

如你所见,现在可以使用捷径运算符||和&&了,这也是本版本一个新增功能。另一个新增功能:IsNull()返回一个函数用于判断当前项是否为空。

2、元组(Tuple)和多元函数支持

现在VBF通过对Tuple的支持,提供了处理二元函数及三元函数的能力。Tuple是一种只读结构体,其每个字段的类型均由类型参数指定。因此使用Tuple,你可以快速创建出满足你要求的小型结构。
'VB
'
演示生成一个Tuple

Dim t1 As Tuple(Of IntegerString= Values(12"Hello World")
Dim t2 As Tuple(Of LongSingleDate= _
    Values(100L, 
-58.8, Now())


Values函数自动生成一个相应类型的Tuple,当前Tuple最多支持5个类型参数。Tuple用于模拟多元函数的参数表,还可以作为查询的临时返回量。

3、属性访问语法

这是本版本极其重要的一个功能,有了他,就可以在Id型查询中访问被查询对象的属性。我们先看一个例子。假设Employee类型有两个属性——整数型的Age和字符串型的Name。如果我们有一个System.Collections.Generic.List<Employee>类型的列表emplist,现在要找到所有Age大于25的Employee对象,该如何写呢?VBF提供了独特的!语法(仅支持VB)和Satisfies满足条件方法:

'VB
'
Employee是一个有Age和Name属性的类型
'
emplist是一个装有Employee实例的List(Of Employee)

'定义Identity对象用于查询
Dim IdEmp As New Identity(Of Employee)

result 
= emplist.FindAll(IdEmp!Age.Satisfies(IdInt > 25))

注意!Age语法,它提供了对Age的访问,但无法得知Age的类型。因此需要在Satisfies函数后面用强类型的IdInt指明要查询的Age是Integer类型。这条查询语句可以读作“寻找Employee,它的Age属性满足整数大于25”。这已经是我找到的最具可读性的写法。除了Satisfies满足条件方法,还支持直接取得属性值的功能。比如我们要的到另一个List(Of String),是刚才那个Employee集合中所有项目Name属性组成的集合,可以这样写:
'VB
'
接上段代码

Dim names As List(Of String= _
    emplist.ConvertAll(Of 
String)(IdEmp!Name.AsType(Of String))

AsType方法表明了你要访问的属性的真实类型,这里不支持任何协变。比如上述语法如果你是用Of Object就会运行错误。C#不支持!运算符,我在C#中采用了indexer的语法,即使用方括号来访问想要的属性:
//C#
Identity<Employee> IdEmp = new Identity<Employee>();

result 
= emplist.FindAll(IdEmp["Age"].Satisfies(Identities.IdInt > 25));

4、延迟执行

现在VBF的每一个函数子对象(Functor)都支持一个DelayInoke的函数,它能返回一个IDelayInvoke(Of TReturn)的接口。使用DelayInvoke可以延迟函数的真实求值过程,直到真正使用函数返回值的时候才开始计算。延迟执行可以用来实现Command模式以及Undo/Redo功能。

VBF 1.5还提供了许多其它的新功能,比如用于String型函数的IndexOf方法支持、一元正负运算符支持、类型转换函数支持等等。有兴趣的可以直接下载源代码观看。很抱歉的是这次时间仍然较紧张,我还是没有提供文档,如果发现很难看懂我建议从Functor(Of TArg, TReturn)这个类的虚函数开始看起。希望大家多多提出建议,以便在VBF中提供更多新酷功能。

posted on 2006-02-24 09:42:00 by Ninputer  评论(5) 阅读(6744)

 
2006年02月16日
注意:本系列未经许可,不得转载。

在泛型编程当中,我们对类型的关注大大提高了。有时需要这样的功能:“当类型是A的时候执行这段代码;当类型是B的时候执行另一段代码”。就是说,需要针对类型进行分支选择。当前,我们有三种机制可以实现这种分支选择:1、根据编译期声明类型选择的函数重载机制。2、根据运行时类型的虚函数-重写机制。3、If语句+类型判断。我们使用面向对象和泛型中的某些机制都是为了消除If语句带来的影响,所以这个时候使用If语句可能令人不太兴奋。但是有时候If语句是唯一的实现方式,我们可以将If封装到基础逻辑中以消除它的不利影响。
本来在泛型世界中,我们应该有特化或偏特化手段,用以实现类型方面的匹配式选择。但是.NET泛型没有提供这一机制。那么我们就只能利用运行期类型比较加以模拟。但问题就出来了:运行期类型与声明类型可以是不同的,比如一个对象可以声明成Object类型,但运行时可以是String类型。这种情况下,使用TypeOf运算符(C#的is运算符)的行为将和编译期选择的行为不同。如何让运行期也能识别编译期声明类型呢?上次讲到抽象工厂的实现时,我们提到了一个工具——TypeToken,我们还可以利用它!不过这次我要改个名字:TypeMate,因为我发现.NET Framework中也有一个东东叫TypeToken,起名字真难啊。。这就是TypeMate的定义:
Public NotInheritable Class TypeMate(Of T)
    
Private Sub New()

    
End Sub

    
Public Shared ReadOnly Value As TypeMate(Of T) = New TypeMate(Of T)
End Class
这个和上次的TypeToken几乎一样,唯一不同就是把属性换成了静态只读字段。因为TypeToken的运行时作用很小,所以我们不能让他在运行时带来太多负担。TypeMate(Of T)与T一一对应,但不管T之间有什么继承关系,各个TypeMate(Of T)之间都没有任何关系,更进一步的是,TypeMate(Of T)的实例将会把T的编译期类型带到运行期,这个正是我们需要的。
接下来我们定义TypeSwitch,作为应用TypeMate的第一个例子,他实现了我们开始的想法,能够根据声明类型进行分派选择。
Public MustInherit Class TypeSwitch(Of T1, T2)

    
Public MustOverride Sub Fire(ByVal m As TypeMate(Of T1))

    
Public MustOverride Sub Fire(ByVal m As TypeMate(Of T2))

    
Public Sub Fire(Of T)(ByVal m As TypeMate(Of T))
        
Dim typeless = m
        
If TypeOf typeless Is TypeMate(Of T1) Then
            Fire(TypeMate(Of T1).Value)
        
ElseIf TypeOf typeless Is TypeMate(Of T2) Then
            Fire(TypeMate(Of T2).Value)
        
Else
            
Throw New ArgumentException("No matched type")
        
End If
    
End Sub
End Class
注意,为什么除了两个基本的Fire虚方法,还需要带有第三个类型参数的Fire方法呢?这全怪.NET泛型的局限性,编译器在编译的时候无法推测类型参数实际表示的类型,只能把这件事推迟到运行时来做。
现在我们用一个例子看看这种方法与普通的函数重载有什么不同。先定义一个字符串和整数的选择器:
Class Test
    
Inherits TypeSwitch(Of IntegerString)

    
Public Overloads Overrides Sub Fire(ByVal m As TypeMate(Of Integer))
        Console.WriteLine(
"Interger!")
    
End Sub

    
Public Overloads Overrides Sub Fire(ByVal m As TypeMate(Of String))
        Console.WriteLine(
"String!")
    
End Sub
End Class
下面我们编写一个利用该选择器进行类型选择的代码例子:
Module Module1

    
Sub Main()
        
Dim o As Object = "abc"
        
Dim s As String = "abc"

        SwitchByDeclarationType(o)
        SwitchByDeclarationType(s)
    
End Sub

    
Sub SwitchByDeclarationType(Of T)(ByVal arg As T)
        
Dim switch As TypeSwitch(Of IntegerString= New Test
        switch.Fire(TypeMate(Of T).Value)
    
End Sub

End Module
注意泛型算法SwitchByDeclarationType,他利用TypeSwitch功能实现了针对类型参数T(而不是一个具体类型)的判断与分派功能。虽然判断的过程在运行时,效果却是根据参数的声明类型,而不是运行时类型来选择的。例子中两个变量运行时都是字符串,但是object那一次调用的时候会失败。

posted on 2006-02-16 09:34:00 by Ninputer  评论(8) 阅读(6869)

 
2006年01月12日

注意:本文版权所有,不得擅自转载

泛型不仅能用来做容器,还能够提供代码复用的手段。在泛型的参与下,许多设计就可能更精妙,更具扩展性。今天我就来演示一个利用泛型增强的抽象工厂模式。我们知道,抽象工厂(Abstract Factory)模式是将工厂和产品全部抽象化,一个抽象工厂生成一组抽象产品,而一个具体工厂则生成具体产品的一个特定组合。它能够维持这种相关对象组合的一致性,并使得用户不需要了解工厂和产品的具体实现。传统的Abstract Factory主要弱点是类型依赖性强,可复用性弱。一个抽象工厂通常是为一个特定需要而设计,通常不能被其他需要抽象工厂的场合使用。而且尽管抽象工厂可以将实际生产任务委派给特定类型的工厂,但这种委派是需要通过自己纯代码实现的,没能利用语言所提供的抽象特性。我们今天的任务就是编写一个不针对特定产品类型和数目的泛型抽象工厂,当你需要特定的抽象工厂时,可随时复用无需再定义专门的抽象工厂实现。

我们首先从只生成一个产品的工厂方法开始,以这作为抽象工厂的原料。很明显,可以设计这样一个接口作为工厂方法的模板:

public interface IFactory<T>
{
    T Create();
}

这个工厂生产一个T类型的对象。当你实现此工厂时,应该让T为抽象产品的类型——即产品通用的基类。比如我们可以实现一个采用无参数构造函数来创建对象的OpNewFactory实现:

public class OpNewFactory<TAbstractProduct, TProduct> : IFactory<TAbstractProduct>
    where TProduct : TAbstractProduct, 
new()
{
    
public TAbstractProduct Create()
    {
        
return new TProduct();
    }
}

从此例子可以看出,你应该仅实现抽象类型的IFactory接口,并生成具体类型。现在我们做完了单一产品的工厂方法模板,就要开始定义生产多个产品的抽象工厂接口了。.NET泛型支持按类型参数个数进行重载,就是说,我们可以定义生产一个、两个、三个……等多种数目的抽象工厂接口,而使用同一个名字。(汗吧,这就是所谓支持“任意数目”的手法)这里免不了要拷贝代码,不过别担心,纯拷贝而已,使用的时候可是非常舒心的哦。我们以生产两种产品类型的抽象工厂为例介绍。能不能定义成这样呢?

public interface IAbstractFactory<T1, T2>
{
    T1 Create();
    T2 Create(); 
//编译错误!!!
}

哦不!方法不能以返回类型区分重载,只能靠参数类型重载。然而这里我们显然不能用T1和T2作为参数,因为这些方法就是为了生产T1和T2准备的,怎么能接受它们作为参数呢?难道要命名为Create1和Create2吗?这很难接受,我们希望生产方法能够体现产品的类型,怎么能叫1和2呢。为了解决这个问题,我们引入了TypeToken<T>类型,它的定义如下:

public sealed class TypeToken<T>
{
    
static private TypeToken<T> instanceValue = new TypeToken<T>();
    
static public TypeToken<T> Instance
    {
        
get { return instanceValue; }
    }

    
private TypeToken() { }
}

这个类没有成员,并且每个类型实参只能创建一个实例,因此代价极小。但就是这小小的实例上带有其类型实参的类型信息,因此可以作为判断函数重载的依据。我们用TypeToken<T>作为区分生产函数重载的依据,实现如下:

public interface IAbstractFactory<T1, T2>
{
    T1 Create(TypeToken
<T1> token);
    T2 Create(TypeToken
<T2> token);
}

现在我们针对抽象工厂实现具体工厂。具体工厂就是利用生产每种产品的单一工厂来组合实现。因此你只要有每种类型的单一工厂就可以直接组合生成抽象工厂,而无需定义一个类来做这件事。注意,对每种数目的抽象工厂接口都需要对应生成一个具体工厂的实现,这里我仅针对生成两个产品的演示:

public class ConcreteFactory<T1, T2> : IAbstractFactory<T1, T2>
{
    
private IFactory<T1> factory1;
    
private IFactory<T2> factory2;

    
public ConcreteFactory(IFactory<T1> f1, IFactory<T2> f2)
    {
        factory1 
= f1;
        factory2 
= f2;
    }

    
public T1 Create(TypeToken<T1> token)
    {
        
return factory1.Create();
    }

    
public T2 Create(TypeToken<T2> token)
    {
        
return factory2.Create();
    }
}

public static class ConcretFactory
{
    
public static ConcreteFactory<T1, T2> NewFactory<T1, T2>(IFactory<T1> f1, IFactory<T2> f2)
    {
        
return new ConcreteFactory<T1, T2>(f1, f2);
    }
}

注意,我又声明了一个没有类型参数的ConcretFactory类,用一个静态方法来生成泛型ConcretFactory的实例,这是因为使用泛型方法可以推测类型参数,使得我们可以不必输入尖括号或Of语句,而泛型类则没有这个功能。现在大功告成!我们用一个例子来演示这个泛型抽象工厂的工作情况。现在假设我们需要一个生产PC的抽象工厂,需要生产两种抽象产品:处理器和内存。处理器和内存的抽象和具体实现如下:

Processor 和 Ram

下面的代码演示了如何随心所欲生成想要的抽象工厂接口以及快速从现有单一产品工厂组合成特定的具体工厂实现。

class Program
{
    
static IAbstractFactory<Processor, Ram> ComputerFactory(string type)
    {
        
if (type == "Intel")
        {
            
return ConcretFactory.NewFactory(
                
new OpNewFactory<Processor, PentiumProcessor>(), 
                
new OpNewFactory<Ram, DDR2Ram>());
        }
        
else if (type == "AMD")
        {
            
return ConcretFactory.NewFactory(
                
new OpNewFactory<Processor, AthlonProcessor>(),
                
new OpNewFactory<Ram, DDRRam>());
        }

        
//unknown type
        return null;
    }

    
static void Main(string[] args)
    {
        
//Yield a computer of Intel
        IAbstractFactory<Processor, Ram> factory1 = ComputerFactory("Intel");

        Ram ram1 
= factory1.Create(TypeToken<Ram>.Instance);
        Processor cup1 
= factory1.Create(TypeToken<Processor>.Instance);

        Console.WriteLine(
"An Intel Computer");
        Console.WriteLine(
"CPU Model: {0}", cup1.Model);
        Console.WriteLine(
"Memory Frequency: {0} MHz", ram1.Frequency);

        Console.WriteLine();

        
//Yield a computer of AMD
        IAbstractFactory<Processor, Ram> factory2 = ComputerFactory("AMD");

        Ram ram2 
= factory2.Create(TypeToken<Ram>.Instance);
        Processor cup2 
= factory2.Create(TypeToken<Processor>.Instance);

        Console.WriteLine(
"An AMD Computer");
        Console.WriteLine(
"CPU Model: {0}", cup2.Model);
        Console.WriteLine(
"Memory Frequency: {0} MHz", ram2.Frequency);
    }
}

总结:我们用泛型技术成功地增强了原本重用性较低的抽象工厂,演示了泛型在提高抽象性和代码重用方面卓越的价值。

posted on 2006-01-12 15:59:00 by Ninputer  评论(13) 阅读(6648)

 
2006年01月08日

从今天起,我将分享一些我在.NET泛型方面的研究心得。这些心得大都是我在设计VBF中思考发现的,还受了很多C++模板与JAVA泛型的启发。其中相当多的技巧和概念都是大部分“深入C# 2.0”类文章也见不到的。我考虑把其中概念性的部分系统地整理成书,而技巧性的东西则通过我这个系列不定期地分享到Blog上。希望用到.NET泛型的人能从我这些技巧中受益。

首先我要介绍的技巧是如何提供类型参数之间的转换。我们知道,.NET泛型将每个类型参数理解为一个独立的类型。如果不通过约束指定,编译器不会对类型参数所代表的类型做任何假设。也就是说,如果在某个上下文中有两个不同的类型参数U和V,编译器不会知道运行时他们代表的真实类型能否进行类型转换,因此会拒绝编译如下代码:

Public Function GenericCast(Of U, V)(ByVal obj As U) As V
    Return CType(obj, V)
End Function

除非我们加以约束U是V的子类。这显然是为了类型安全做出的考虑,因为.NET这种跨程序集的泛型无法在编译时做出足够的检查来确保类型安全。但是限制了这种操作,就妨碍了我们做事,很多代码因此写不出来。于是,我们可以利用.NET所有类型的基类Object来绕过这一限制:

Return DirectCast(DirectCast(obj, Object), V)

VB的DirectCast运算符在泛型的类型参数上作用与C#的括号运算符相同。也就是说,这段代码用C#写起来是这样:

return (V)(object)obj;

这样,即使不约束U和V之间的关系,这段代码也能编译了。但是他的功能却不能令我们满意。这样写出的类型转换实际上还是仅当U是V本身或其子类的时候才能转换成功。而其他一切情况都会转换失败。不管U和V的运行时类型之间是否定义有其他类型转换规则。这显然不合我们意愿,我们希望int与double之间的转换等语言内置的类型转换都能够自动进行,否则就和约束没什么两样了。对VB用户来说有一个极为简单的解决方案——把第二次DirectCast变成CType:

Public Function GenericCast(Of U, V)(ByVal obj As U) As V
    Return CType(DirectCast(obj, Object), V)
End Function

现在,GenericCast泛型方法就能执行int与double等内置规则的转换了。很神奇?因为CType运算符在编译时自动调用了VB运行库的转换函数,该函数在运行期间对泛型类型参数的真实类型做了检查。而且这个过程的性能完全可以接受。

现在VB的用户已经轻松享受这一功能了。但是C#的事情还没完,因为C#没有如此智能的类型转换运算符,因此就需要手工实现VB运行库所代办的那些任务。其实就是利用了一下IConvertible:

static V GenericCast<U, V>(U obj)
{
    IConvertible convertibleObj = obj as IConvertible;
    if (convertibleObj != null)
    {
        Type t = typeof(V);

        switch (Type.GetTypeCode(t))
        {
            case TypeCode.Boolean:
                return (V)(object)convertibleObj.ToBoolean(null);
            case TypeCode.Byte:
                return (V)(object)convertibleObj.ToByte(null);
            case TypeCode.Char:
                return (V)(object)convertibleObj.ToChar(null);
           
            //.........
            default:
                //None of them, use the following default way..
                break;
        }
    }

    return (V)(object)obj;
}

不过这个方法看起来不但很丑、很麻烦,功能上还达不到VB版。所以建议您用到这种类型参数之间的转换,就用VB封装这一功能,然后做成dll供C#调用吧

————Update 1.8, 19:40————

经速马提醒,发现Convert.ChangeType方法已经封装了IConvertible判断的那些代码,因此C#版可采用这种写法:

static V GenericCast<U, V>(U obj)
{
    return (V)Convert.ChangeType(obj, typeof(V));
}

不过它仍然没有VB版的CType功能丰富。此外我还发现了VB运行库的ChangeType还有支持用户自定义类型转换运算符的功能。所以终极版本如下:

Public Function GenericCast(Of U, V)(ByVal obj As U) As V
    Try

        Return CType(DirectCast(obj, Object), V)
    Catch ex As InvalidCastException
        Return DirectCast( _
            CompilerServices.Conversions.ChangeType(obj, GetType(V)), V)
   
End Try

End Function

posted on 2006-01-08 17:07:00 by Ninputer  评论(11) 阅读(7789)

 

在动态编程时,我们常常需要运行时确定调用对象的哪个属性或哪个方法。这个任务通常可以用反射来解决。但众所周知,反射的性能要比静态指定的方式低很多,因为反射要通过运行时复杂的机制完成。能否获得性能和灵活性兼备的动态调用?我在开发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时想到了他们,希望能对部分需要的人有所帮助。

posted on 2006-01-08 15:45:00 by Ninputer  评论(7) 阅读(7402)

 
2005年11月26日

除了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语言编译器中每一项特性,都可能有隐藏在其表面之下的深层次用途。善加研究后常能发现原来所认识不到的功能。我当然不是在推荐大家乱用运算符重载,只是一种思考,一种新的灵感。

posted on 2005-11-26 10:52:00 by Ninputer  评论(8) 阅读(8983)

 
2005年11月11日

很早就开始VBF的设计,在经历开发和调整了两个月的时间后,VBF的第一个BETA版终于和大家见面了!我是从前一阶段博客园上较为流行的动态语言讨论上获得灵感。我主要不是想写一个动态语言,而是发现很多动态语言共有的特征——不同程度地支持函数式编程(Functional Programming,FP)。这是一种显著不同于命令式编程(目前的VB,和C#均为命令式编程语言,面向过程和面向对象也是命令式编程范式的扩展)的编程范式。它讲究组合、高阶函数和延迟计算。在纯粹的FP语言(Lisp, SML)可以不使用变量,函数执行没有副作用,表达式的意义更接近数学式的意义而不是计算机的执行方式。函数式语言具有比命令式语言更高的数学抽象力,因此在描述某些算法的时候比命令式语言更佳。现在人们发现,在命令式语言中引入局部的函数式编程思想,能将两种范式的好处合二为一。VB9和C#3都将不同程度地引入函数式编程的特性。

我开发VBF主要就是希望利用Visual Basic 2005和C# 2.0自身所具有的语言抽象力将函数式编程的思想引入。我不对编译器进行改造,也不编写任何插件,只要引用我的类库,就可以实现一些非常精妙的语法。

[VB]
Dim list As List(Of Integer) = GetList() '假设用一些数填充list
Dim result As List(Of Integer)
'找到所有大于25或者小于等于17的数,存入result
result = list.FindAll(IdInt > 25 Or IdInt <=17)

[C#]
// using I = VBF.Functional.Identities
List<int> list = GetList(); //假设用一些数填充list
//找到所有大于25或者小于等于17的数,存入result
List<int> result = list.FindAll(I.IdInt > 25 | I.IdInt <= 17);

如你所见,“IdInt”就如同表达式中变量的占位符,由它生成的表达式并不求值,而是将表达式的运算规则保存下来传递给FindAll,而FindAll作为一个高阶函数将表达式作用于列表中的每一项上。这就是延迟计算。这里IdInt为“恒等组合子(Identity Combinator),是VBF初期所支持的几个组合子之一。通过组合子之间的组合就可以重用函数的功能,比如IdInt和+(对结果进行加法的组合子)和7(常函数)三者组合,就生成了对一个数加7的函数。这就是我VBF BETA1的最基本功能。我在VBF 1.0 BETA1中支持了以下特性:

Functor 函数体基类,支持大量组合运算符和延迟求值
Combinator 组合子基类
Identity和Constant组合子
Predicate 谓词
ComparisonPredicate 比较谓词(大于、小于等)
进行通用四则运算的组合子
进行字符串处理的组合子

我的VBF代码中非常频繁地使用泛型和运算符重载,大量手法均为主流类库所见不到的。我也因此获得了非常丰富的在.NET中使用泛型的经验。大家均可下载我的代码参考。项目主页:http://www.sf.net/projects/vbf

希望大家对我的VBF提出建议,同时敬请关注VBF的后续版本,有更多精彩的内容。

 

posted on 2005-11-11 08:48:00 by Ninputer  评论(16) 阅读(8856)

 
2005年11月02日

.NET 2.0的泛型,是在何时进行建造(Construct)?编译器在何时知道泛型的类型参数,何时不知道?不妨先来思考一下这几个Quiz:

Quiz 1:写出以下代码的输出

class Program { static void Main(string[] args) { A<int> a = new A<int>(); a.InternalCall(12); } } class A<T> { public void InternalCall(T obj) { Test(obj); } public void Test<U>(U obj) { Console.WriteLine("U"); } public void Test(int obj) { Console.WriteLine("int"); } }

到底哪个重载会被调用呢?仅从类的里面看,似乎只有一个选择;而在调用方看,却存在两个选择……

Quiz 2:写出以下代码的输出

class Program
{ static void Main(string[] args) { A<int> a = new A<int>(); a.Test(12); } } class A<T> { public void Test(T obj) { Console.WriteLine("T"); } public void Test(int obj) { Console.WriteLine("int"); } }

恩……这是个最奇特的例子,当你输入代码时,IDE会告诉你有两个一模一样的重载函数,那么会调用哪一个呢?

Quiz 3:写出以下代码的输出

class Program { static void Main(string[] args) { A<int> a = new A<int>(); a.Test(12); a.Test(12.0); } } class A<T> { public void Test<U>(U obj) { Console.WriteLine("U"); } public void Test(T obj) { Console.WriteLine("T"); } }
当你做出这些题后,你能想到这些特性可以用来编写什么样的程序吗?

 

posted on 2005-11-02 21:23:00 by Ninputer  评论(19) 阅读(8409)

 
2005年10月19日

我在介绍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结构增加了一个初始化所有成员的构造函数。

posted on 2005-10-19 08:56:00 by Ninputer  评论(7) 阅读(5414)

 
2005年10月12日

本期是Visual Basic 9.0前沿播报动态篇的第三篇,也是这个系列的完结篇。按照惯例,我将Visual Basic 9.0最激动人心的部分放在了这一篇。在这一篇,我们将看到所有新特性作用在XML上之后带来的革新性变化。

Visual Basic 9.0专门为XML集成设置了一个开发组,力求将XML语言与Visual Basic语言完全结合,使Visual Basic成为全行业对XML支持最佳的开发工具。这些新特性都基于微软的XLinq——新一代支持语言集成查询的XML框架。使用XLinq可以按照XML逻辑树状对象模型来建立和访问。本文不打算介绍XLink,有兴趣可以下载这篇文档http://msdn.microsoft.com/VBasic/Future/XLinq%20Overview.doc。我要介绍的是,VB9为XLinq所做的惊人语法。

XML字面量

你有没有想过在一种编程语言里创建XML可以达到多么简单?VB9支持直接XML文档的原始结构作为字面量,也就是说,VB9里可以直接写:

Dim x = <Books><Book author="A. S. Tanenbaum">Mordern Operating Systems</Book></Books>

这么写,VB不仅仅吧它当作一个字符串,而是可以帮你做XML结构的检查,而且编译器了解这一XML的结构,因此会有智能感知的帮助。你可以将XML字面量分成多行编写,直到遇到根元素的终结(例子中的</Books>)才判断XML字面量写完,因此无须使用续行符。这也就是说,VB的XML可以表达一个以单元素为根的XML结构片断。如果XML字面量仅仅是这样,就没有多大意思了。我们可以让这个XML字面量中的数据动态化:

Dim rootName = "Books"
Dim b As New Book {Author := "A. S. Tanenbaum", Title := "Mordern Operating Systems", ID := 20 }

Dim x = <(rootName)><Book author=(b.Author)><%= b.Title %></Book></>

我们观察一下上面的代码,元素名称Books可以用变量来指定,只要在括号组成的“洞”填入即可,相应的关闭标记则变成</>这很重要,因为元素名称已经是动态决定的了,所以引入特定写法根据位置来关闭相应标记。我们看到,attribute的值也可以用洞来填入(attribute的名称也可以用相同语法),但是标记的Inner Text部分则不能使用洞,而是要使用<%= %>这种类似于ASP.NET数据绑定的语法,这称为代码嵌入。 这已经很强大并且激动人心了,不是吗?但还不够,我们可以把查询包含的语法嵌入到XML字面量中。假设我们有一组Book要生成:

Dim books = GetBooks() '假设用别的代码生成了Book类型的集合

Dim x = _
<(rootName)>
  <%= Select
    <Book author=(b.Author)><%= b.Title %></Book>
  From b In books Where b.ID > 5 %>
</>

我们用代码嵌入的手段,将类似于SQL的查询包含直接插入到XML字面量中去了。其结果很显然,是生成了<Books>下<Book>的列表。使用XML字面量,我们可以非常灵活地组合和生成我们想要得XML;也可以把XML直接拷贝粘贴到VB的IDE中,稍加修改而成为你自己的XML生成器。XML字面量还完善支持XML的namespace等特性。有了VB9,还需要XSLT吗?难说……

XML后期绑定

现在我们有了生成XML的手段,那么访问XML的手段呢?Visual Basic 9.0提供了XML后期绑定,我们可以用最简单的语法来访问XML。假设我们有刚才生成的Books文档,则可以用下列语法直接访问到它的某一个Book

Dim title = x.Books.Book(0).InnerText
Dim author = x.Books.Book.@author

这语法很类似于XPath,我们还可以做到XPath的//的功效:

For Each Dim book In x.Books...Book
    '遍历每一个Book元素
Next

只要3个点,就可以把Books下边所有层次的Book都找到,然后直接就可以遍历。当然我们还可以用查询包含,使用SQL语法来查询XML文档。总之,在Visual Basic 9.0中使用XML会是一种前所未有的体验。

现在Visual Basic 9.0的新特性已经介绍完了,这确实是一个有着巨大进步的版本,同时特别注重用户体验的改善,保持着简单易用的特点。

posted on 2005-10-12 09:23:00 by Ninputer  评论(13) 阅读(10226)

 
【第1页/共9页,121条】
首页
前页
1

Powered by: Joycode.MVC引擎 0.5.1.0