注意:未经许可,本系列禁止转载。
本文所介绍的技巧,是我在研究泛型开发不久就发现并成功运用的技 巧。这个技巧是突破.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(1, 2)
Dim d As Double = Add(1.5, 888.0)
Return Calculator(Of T).Default.Add(a, b)
End Function
'在另一个方法中
Dim c As Integer = Add(1, 2)
Dim d As Double = Add(1.5, 888.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;
{
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
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
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 Integer, ByVal b As Integer) As Integer
Return a + b
End Function
End Class
Class DoubleCalc
Inherits Calculator(Of Double)
Public Overrides Function Add(ByVal a As Double, ByVal b As Double) As 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
Inherits Calculator(Of Integer)
Public Overrides Function Add(ByVal a As Integer, ByVal b As Integer) As Integer
Return a + b
End Function
End Class
Class DoubleCalc
Inherits Calculator(Of Double)
Public Overrides Function Add(ByVal a As Double, ByVal b As Double) As 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
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
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采用的做法)。第二个缺陷是保存在静态字段 中的对象不会被清理,这样就会越用越多,占据内存。好在这种用于计算或比较类的对象都很小。
下一次,我 们讨论利用反射操纵泛型类型,以达成更巧妙的设计。
打印 | 张贴于 2006-04-14 10:16:00 | Tag:技术随笔
留言反馈
联系电话:(027)56770383
就像sp1234在一开始几个帖子里说的,他的准则是符合微软推荐的编程范式。
而Ninputer你和我一样,对.net的泛型实现有大有微词。所以试着探索能不能在现有特性下,有更好的深化的方式。
大家的出发点不一样,必然会是你做得太多,他都不会觉得好。甚至可以说,你在你的方向上做得越完美,就离他的标准越远 :)
所以说这样的讨论是没有意义的,各做各的事吧
Compose复合函数。如果你有MyConvert和MyCheck两个函数,现在你要找list中的项,符合经过MyConvert转换之后才满足MyCheck的
'省略了Wrap,假设MyCheck等已经是Functor
list.FindAll(Compose(MyCheck, MyConvert))
难道只有Compose?我还有Section切片函数。如果你有F(x, y)As Boolean,你想找list中满足F(x, 10)的所有项,以及F(20, y)的另外所有项,你怎么办,写两个函数包装一下?
用Section同样帮你迅速搞定
list.FindAll(Section(F, 10))
list.FindAll(Section(20, F))
如果你要不满足F(20, y)的呢?
list.FindAll(Not Section(20, F))
如果你还要满足大于10并且不满足F(20, y)的?
list.FindAll(IdInt > 10 And Not Section(20, F))
……
还没看出组合的威力?
没问题,我这组合还可以是动态。现在用户指定F自调用几遍。甚至指定是什么函数,甚至指定Closure参数的来源。
Dim c = FromUserInput()
For i = 1 To c - 1
f = Wrap(Of T, R)(AddressOf F)
f = Compose(f, f)
Next
当然,你每一种都能不用VBF做到,但是VBF的威力也在一点一点体现出来。如果你非要说这些全都没有意义,我还有更多,聚集、折叠等高阶函数,延迟执行函数、累加器Functor……
你总不可能在不了解VBF的情况下,把这些全驳倒。我甚至觉得VBF可以在你的逻辑解题系统里发挥作用,让你的代码更简洁清楚。
而我,如果仅仅在对prolog稍微了解的基础上,不对你的系统进行任何研究,就直接说“没用”,“某种程度的垃圾”或者“你应该放弃”,你会接受或按我说的做吗?
假设存在函数MyCheck做推断
result = list.FindAll(AddressOf MyCheck)
假设现在要找到list中,不 满 足 MyCheck的项目。VBF可以以函数外附类优雅的做到
result = list.FindAll(Not Wrap(Of Integer)(AddressOf MyCheck))
算是通过组合重用已有函数,这种能力还不赖吧?
哎,怎么说呢。你这论断真让我为难,但是我还是得说“我没怒”。要想让我“怒”需得不讲道理,把正确地说成错误,眼见证据不承认等耍赖行为或者以贬低为争论的唯一目的等。
我维护VBF是因为sp1234的质疑没有切中要害,他想反对的东西不是我想用VBF表达的东西,而且他的反对也不是想让我把VBF做得更好,而似乎是想让我放弃这条思路。
既然这样,我能做什么呢?我的确被VBF控制了,我想要让他更有用,更巧妙,我每天都在想能把什么新玩意引进VBF,想怎么才能更好地应用VBF与众不同的特性,让它变成一个真正有用,真正有人爱用的东西。
所以,这是我的激情和创造所在,就是我生命的一部分。我才不会被别人泼冷水之后放弃他。
你不明白我VBF的本质或原理,这是事实,因为你自始至终没能说出一句和这个有关的话。你总是试图用和函数式编程或者函数组合、高阶完全无关的东东来证明“VBF没有意义”,或者我哪里哪里与微软不符什么的。
我就是和微软不符,而且VBF本来就没有你想象中的,并且希望驳倒的意义。泛型和函数式编程思想引入流行编程语言,在C++社区和Java社区已经很多年了。这些都是新思想,许多成功的思想都产生并成熟于技术社区。如果“与微软不符”成为判断想法正确与否的标准,.NET社区将会失去创造力,永远无法与C++和Java社区相媲美。微软的Fx是面向商业应用的,我的VBF不是,所以你不要拿Fx的设计来说事。
我选VB来做VBF不是因为它是微软的,而是因为VB有我所需要的抽象力——面向对象、泛型、运算符重载、静态导入等等。
而你写的这些,说实在没什么做不到的。表达式就那么些类型,一种一种支持就可以做到处理绝大部分表达式,只是没法做到那么优美。
private function MyCheck(x as integer) as boolean
dim z as new myClass(now.getday)
return x>0 or x<=1 or (new Random).next(x) or x=z.anyInt
end function
然后用一大堆“你不懂”之类的话来作为论据。
其实我也懒得跟你的帖子了。
不过,我相信别人都能相信这种时间的先后并不是决定问题答案好坏的唯一标准。
何况,我举的例子说明了这完全可以有一种简单的、符合微软的本意的思路,根本与范型无关。
我一直没有发文章介绍VBF是因为现在还很不完善,而且我也不知道该从何讲起。但是我一定会继续完善并抽空来发表其中的思想。
也期望有机会拜读关于此的文章,呵呵
看你以前CSDN的ID(如果那个sp1234是你的话),你也对泛型感兴趣过。这样不妨把你的想法以某种形式发表出来,让我去学习。只有这样,才能交换思想,取得进步。
private a,b integer
完全可能根据需要变成是
private list1 as ....
private list2 as .....
你是没有可能让我认识到“VBF不好”或任何成程度或弱或强的任何类似意思的。因为你不懂VBF,而且先入为主的看法让阻止了你弄懂,这你明白吗?
你为什么要证明你的做法或者C#更为自然?我就问这个问题“为什么”。他们更自然但是他们做不出VBF.ParserCombinators。我的VBF有我的思想在里面,是函数组合,高阶函数。他们是VB/C#现在没有的,我引入了他,利用泛型和多种VB的抽象手段。
private class MyCheck
private a,b integer
public sub new MyCheck(a as integer,b as integer)
this.a=a
this.b=b
end sub
public function DoCheck(x as integer)
return x>a andalso x<b
end function
end class
dim C as new MyCheck(a,b)
m_result = m_list.FindAll(addressof MyCheck.doCheck)
而c#把一切只写一行上并且还很简单。
你只是要求别人先用你的 functor,否则就不是恰当地表达算法目的的。这不行。
我只想——组合。
我说重载,实际上针对你想用一个“通用、万能”的接口来代替具体的类型的做法而言的。有时候需要抽象地使用接口,有时候应该具体地表达类型使得可以类型重载功能。谁说是用现成的控制机制就不够高级、不够通用呢?发明汽车以前曾经有经济学家非常担心用不了多长时间城市就会被几英寸厚的马粪填满,但是汽车发明了之后经济学家才意识到自己错了。
Public Sub Test(a As Integer, b As Integer)
m_result = m_list.FindAll(IdInt > a And IdInt <= b)
End Sub
那么和C#有何区别?
我的函数可以组合出来,难道组合出来就固定不变了?
Dim f As Functor(Of Integer, Boolean)
f = IdInt > 0
x = list1.FindAll(f)
f = f Or IdInt <= 20
y = list2.TrueForAll(f)
f = Not f And Wrap(AddressOf MyCheck)
z = list3.Exists(f)
看出来了吗,函数是可以组合的。
PS. 如果你认为什么东西有意义,你为什么不自己去做?我的东西已经做出来了,你指明没有意义是为了什么?我难道没有我自己的想法,你认为对所以我就必须如何做,以至于即使我用“错误”的方法已经做出某样东西都是不行的?
这是在“授我以渔”?
我可以保证:我每样开发VBF的经验我都会毫无保留地共享出来,而且VBF本身就是一个供研究的开源项目。
return x>0 or x<=1
end function
result = list.FindAll(addressof MyCheck)
完全不用 VBF。而用c#写则直接写在一行上。
“你的函数重载需要一个,就得定义一个。”这个不是事实。事实是,至少多于你的FindAll,vb.net现在版本(版本8)不支持匿名方法,所以无法像c#那样在“一行”上漂亮地写完筛选表达式(至于使用到外部变量在vb.net中就更需要多写一个繁琐的private class),但是尽管如此,多“枯燥地”写个 private function和end function,也已经不比你的经过层层解释的表达式差吧?
选择一种东西需要平衡。虽然我认为你的工作很有创意,但是如果你开发一个直接解释字符串的 SQL 语句并且在编译时产生高级.net语言目标代码的工程,我觉得更好。
你觉得System.Collections.Generic.List<T>不对T有任何约束,却能够Sort是怎么做到的呢?
所以虽然Calculator的创建好像不会有效率损失,
但是仍然很慢的
这就是思想上的差异。
我知道我的VBF无法表达所有匿名方法所表达的一切事情,但是现在没有匿名方法,我的VBF能做到现在这个样子,都是我努力创建的,我几乎利用到了VB所有的抽象能力。
这是正在开发的VBF.Query,查询库。Q函数启动查询。
result = Q(Process.GetProcesses).Where(p!ProcessName.Satisfies(Initial(IdStr, "A"))).Take(10).Select(p!ProcessName.AsType(Of String), p!WorkingSet.AsType(Of Long)).OrderBy(1)
还有VBF.ParserCombinators也正在开发,是一个基于函数式组合子的LL语法分析器的“原料库"
private function MyCheck(x as integer) as boolean
return x>0 or x<=1
end function
private function MyCheck(x as string) as boolean
return len(x)>15
end function
对于一种list:
result = list.FindAll(addressof MyCheck)
另一种list:
result = list.FindAll(addressof MyCheck)
上边可能大字太快,没有说清楚。这就是我第一句话的意思。
至于你说你的东西远不止 FindAll,那好呀!欢迎拿出来T讨论呀!
PS.你以为VBF只能做Query吗?就算作Query你以为只能写到FindAll这种程度吗?太小看VBF了。
PS. 那个特性叫匿名方法,不叫匿名代理,因为作为Delegate,它是有名字的。匿名方法显然要比VBF容易做事,但这是靠编译器特别提供的语法,我是靠类库做出来,这就是一种尝试。且不说这个,我的每个Functor之间可以发生关系,可以组合,可以运算。匿名方法之间没有任何运算关系。
在VBF for VB9中,我会让Lambda函数发挥目前C#匿名方法所无法想象的力量。
-->
result = list.FindAll(addressof StrCheck)
private function IntCheck(x as integer) as boolean
return x>0 or x<=1
end function
private function StrCheck(x as string) as boolean
return len(x)>15
end function
result = list.FindAll(addressof IntCheck)
另一种list:
result = list.FindAll(Len(IdStr) > 15)
当list中的参数不是Integer或者String而是自定义类型的时候,编译器会自动尝试查找有没有 ctype。
只要一钻进牛角尖,就要拼命去钻别的牛角尖去找出路。
更多地考虑运用微软提供的现成的东西
对于你的语法,虽然vb.net现在不支持匿名代理,我看不出来你的写法比使用代理函数要好。代理函数可以用同名,但是参数重载。
反过来说,IdInt、IdStr,你认为把重载“避免”了吗?
result = list.FindAll(IdInt > 0 Or IdInt <= 1)
result = list.FindAll(Len(IdStr) > 15)
result = list.FindAll(IdEmp!Age.Satisfies(IdInt 〉25))
我的IdInt, IdStr, IdEmp都是同一个类型Identity<T>的实例。
你能不能既实现这种语法,又把你那些规则都用上?不行。你说的规则都是微软希望泛型用户用的,都是在当前.NET泛型限制下最符合微软意愿的规则。但是你须得明白,最“正确”的规则不能做事情的时候,就得用别的手段。
而且,我没有利用泛型高效化后期绑定。实际上我在利用后期绑定。后期绑定是泛型的,但非强类型的。泛型就一定强类型?又是一个“最符合微软意愿”的想法。
范型不是用来含混类型的。范型是明确的类型,不是用来对后期绑定进行高效化处理的。避免后期绑定,一定要在编译期做类型检查,这个原则不会因为范型而改变,而是会更强化。