RSS 2.0 Feed

Friday, June 30, 2006

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 @ | Feedback (18) | Filed Under [ 技术随笔 ]

Friday, June 02, 2006

本文为一些泛型反射技巧的简单罗列,为日后的文章做准备。
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 @ | Feedback (8) | Filed Under [ 技术随笔 ]

Friday, April 14, 2006

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

本文所介绍的技巧,是我在研究泛型开发不久就发现并成功运用的技 巧。这个技巧是突破.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 @ | Feedback (51) | Filed Under [ 技术随笔 ]

Thursday, March 23, 2006

这是我第一次发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