装配中的脑袋

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

导航

关于

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

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

标签

每月存档

最新留言

广告

 

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

.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。

打印 | 张贴于 2006-03-18 14:21:00 | Tag:技术随笔

留言反馈

#re: 泛型技巧系列:避免基类及接口约束 编辑
@Ninputer
哦,那我可要期待了,因为我知道一些我不知道如何用.NET的Generics语法表达的表达式(Java的知道)。
2006-03-23 16:35:00 | [匿名:Cavingdeep]
#re: 泛型技巧系列:避免基类及接口约束 编辑
@sp1234

我知道你大概要说什么意思,但是目前.NET以接口作约束的效果就是不太好。这是现状。我没说我这方法就是银弹,但多少能解决问题。

完美解决要靠.NET支持,我刚才提到过了。
2006-03-23 14:46:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
@sp1234

你在回复谁?我半个字也没提到delegate。Comparer不是delegate。当然也可以用delegate来做辅助类,但是效果并不一定比接口来做好。
2006-03-23 14:42:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
@Cavingdeep

我说的是下一次要讲的内容。JAVA5泛型我可是透彻研究过的哦,真的差点劲。
2006-03-23 14:39:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
我认为你应该把运用delegate作为一种启发性的点,与其它方法共同研究。而不需要通过打击接口来说明delegate。
2006-03-23 11:58:00 | [匿名:sp1234]
#re: 泛型技巧系列:避免基类及接口约束 编辑
有这么一种场景,可能强调要求设计师本来就是面向领域模型去设计方法。针对此父类对象开发的扩展方法,本来就是多态地调用这个方法,而特意避免胡乱地调用无关方法。这纯粹是为了设计表达力提高(作为一种功能协议被遵守),需要保持。
2006-03-23 11:40:00 | [匿名:sp1234]
#re: 泛型技巧系列:避免基类及接口约束 编辑
对象自身的接口和运行时delegate作为方法参数,在效率上的比较?

另外,在设计时期,从对象自动取得方法,与委托给调用者提供“任意”方法,这是两种不同的设计方法,恐怕不是“约束”这么简单。 单纯反对约束,就好像单纯反对面向对象所强调的封装性,考虑不周。
2006-03-23 11:36:00 | [匿名:sp1234]
#re: 泛型技巧系列:避免基类及接口约束 编辑
[quote]
# re: 泛型技巧系列:避免基类及接口约束 2006-3-18 23:59 Justin Shen

发现.Net的泛型比我想像的聪明一些。。。。

public class Pair<T>
{
public T a;
public T b;
}

public class IntPair : Pair<int>
{
public IntPair(int i, int j)
{
a = i;
b = j;
}
public int add()
{
return a + b;
}
}

# re: 泛型技巧系列:避免基类及接口约束 2006-3-19 17:17 Ninputer
其实主要就是靠泛型类型静态字段实现的类型字典,否则这种伪Type Traits没有任何意义。像JAVA 5泛型就根本做不到。

[/quote]

为什么说Java做不到,我倒是好奇实验了一下,完全没有问题。

另外,我倒是觉得Java下的Generics语法要比.NET的强,虽然VM级别的实现目前还不够理想,不过这也是出于简化compatibility考虑的,日后这种情况可能会好转。
2006-03-23 11:10:00 | [匿名:Cavingdeep]
#re: 泛型技巧系列:避免基类及接口约束 编辑
漏说一句,不要倾向把所有的责任都丢给类库的实现者。像验证参数的合法之类的,其实就应该由调用者自己来完成。
2006-03-23 00:01:00 | [匿名:Justin Shen]
#re: 泛型技巧系列:避免基类及接口约束 编辑
这没有什么不可取的。不使用约束,我们可以在泛型上获得更大的灵活性,但是这也就意味着我们需要承担更多的责任。
2006-03-22 23:59:00 | [匿名:Justin Shen]
#re: 泛型技巧系列:避免基类及接口约束 编辑
约束问题要想彻底解决,必须由.NET支持By signature的约束。一个有希望的方案是CLR将来会支持Dynamic vtable,这样就可以借助Dynamic Interface约束来实现真正符合泛型需要的约束。
2006-03-21 15:56:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
相对于约束版本,这种把检查推后到运行时也没什么不可取。这两种方法都不完美,但是两者对比,约束的方案更丑陋甚至在有的时候无法做事。

而且,这种方案不仅在我的VBF中证明行之有效,而且.NET Framework也倾向于采用。所有支持排序的泛型容器,如List或SortedList等,都没有对容器类型约束。
2006-03-21 15:43:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
感觉这种做法有点不可取
T Add<T>(T a, T b)
{
return Calculator<T>.Default.Add(a, b);
}
方法是没约束,就是什么对象都可以处理.实际上这是个错觉;
因为传入的T没实现Calculator<T>.Default这个约束时就会有运行错误.
方法没有明确地告诉用户约束,用户就有可能编写出不安全的代码.
2006-03-21 14:09:00 | [匿名:bbq]
#re: 泛型技巧系列:避免基类及接口约束 编辑
Comparable里实现了两个,一种是针对有IComparable<T>类型的,一种是针对IComparable类型的。如果两个接口都没实现,就抛异常
2006-03-21 13:39:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
void Sort<T>(T[] arr)
{
Sort(arr, Comparable<T>.Default);
}
有些问题想问下,如果我传入的类型Comparable里没有实现的那你获取DefaultJ是什么,返回一个默认的?如果是那默的会适合使用者的需要吗?

2006-03-21 11:58:00 | [匿名:bbq]
#re: 泛型技巧系列:避免基类及接口约束 编辑
可惜.NET约束没有C++ 0x那么先进,什么“被编译器自动匹配”目前只是美好的梦想。建议你真的去看看接口约束能做什么,不能做什么。很快就会发现,绝大多数情况下约束与你想象的东西完全不同

而缩小程序功能,则是实实在在发生的,这是因为泛型所带来的抽象和OO所带来的抽象不同。绝不可能按同一种思路考虑。

PS. 以后几次我会展示些正确使用约束的例子,印证我在文章中提到的做法。
2006-03-20 08:04:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
约束是为了在定义的时候“分支”——不同类型分别定义不同范型,并且调用代码被编译器自动匹配,而不会为了缩小程序功能。

设计者的目的(意图)决定了是否使用约束,而并不是能力不行。

将分支隐藏得过深,不是好的设计,也并不经济。
2006-03-20 02:35:00 | [匿名:sp1234]
#re: 泛型技巧系列:避免基类及接口约束 编辑
我到是发现,围绕generic,reflection有很多增强,值得去研究一下
2006-03-19 22:16:00 | [匿名:Justin Shen]
#re: 泛型技巧系列:避免基类及接口约束 编辑
其实主要就是靠泛型类型静态字段实现的类型字典,否则这种伪Type Traits没有任何意义。像JAVA 5泛型就根本做不到。
2006-03-19 17:17:00 | [匿名:Ninputer]
#re: 泛型技巧系列:避免基类及接口约束 编辑
发现.Net的泛型比我想像的聪明一些。。。。

public class Pair<T>
{
public T a;
public T b;
}

public class IntPair : Pair<int>
{
public IntPair(int i, int j)
{
a = i;
b = j;
}
public int add()
{
return a + b;
}
}
2006-03-18 23:59:00 | [匿名:Justin Shen]
#re: 泛型技巧系列:避免基类及接口约束 编辑
这个东东似乎不错。Reflector了一下Compare<T>.Default的实现,好象比较复杂。还是等你的下一篇吧,嘿嘿
2006-03-18 23:13:00 | [匿名:Justin Shen]
对不起,目前本随笔不允许发表新评论.

Powered by: Joycode.MVC引擎 0.5.2.0