C#和java是号称90%的相同加上10%的不同。因此当时我学习C#,阅读两种代码完全没有什么阻碍。
对C#了解得深入以后,来发表下对这两种语言各自特性的一些看法
比较起java和C#大相径庭的那10%,会发现C#五花八门的特性要多很多。比较知名的有:委托,属性,真正的泛型,索引器,类初始化器,分部类,操作符重载,struct,unsafe代码,IDisposable等,另外.net framework 3.5还加了一大串纯粹由编译器提供的特性。
委托,这个可以算是C#之于java的最大优势。虽然java可以依靠接口,匿名内部类这些特性实现委托一样的功能,但却要麻烦许多,如果涉及到N个委托实例相加的情况,那么一个C#里面简单的"+"号,在java里就只能用FilterChain,InterceptorStack这种概念了。
属性,据说属性信息是带到运行时的,后查看反射的确有PropertyInfo类,之前说它是编译器语法糖是受了某篇分析IL代码的文章误导,不过这个是不是语法糖都没什么关系,它和java普遍做法的get,set各有优劣。属性写起来简捷,但却比较难看出哪些属性是只读,哪些是只写不读。
真正的泛型,这个又是一大C#的优势,同时伪泛型又是java的一大败笔。但是java年代比较长,为兼容性考虑不得不使用假的泛型实现。如果泛型不是在jdk1.5时推出,而是在jdk1.4,和collection framework一起推出,我认为它也会采用真正的泛型实现。
索引器,有了这个很多容器类都可以直接用[]取元素,感觉还不错,比没有好,编译器的小把戏。
类初始化器,典型的懒汉特性。每次用顶多能省下一两行代码,又是编译器的小把戏。
分部类(partial class),纯粹为了vs.net的那一大堆图形化设计器老和人的代码冲突而搞出来得玩意,又是编译器的小把戏。
操作符重载,属于用到得不多,要用时却显得特别有用的东西,很奇怪java为什么不提供这个功能。
struct,在堆栈上的东西,释放内存那是超级的快,只不过需要用到这个的场合,大概都在使用C++编程。
unsafe代码,纯粹增加语言复杂性的东西。就好像一个人搬家,看这个不舍得扔,那个也不舍得扔,搞到最后把瓶瓶罐罐都搬走了。
IDisposable,实现这个接口,配合using块,非常的强大,终于可以像C++那样掌握对象的销毁了。
接着说说java比C#多的特性:
匿名内部类:真是极端方便的一个东西,还和JAVA的好多设计模式有关系,不过C#把java匿名内部类的写法拿去用作类初始化器了,糟蹋了...
动态代理: java里面要实现AOP,易如反掌;C#要实现AOP,难如登天,不得不借助Assamble命名空间下的那些动态IL生成工具。这就是动态代理的作用。
希望有一天C#可以拥有匿名内部类和动态代理,变得更完善。也希望java也可以有委托等。
这是一个控制Java动态代理执行的类(附C#实现),target域是他的代理目标,动态代理会生成一个可以替代target的类,在调用target类的某方法时,实际上是调用被改造过的方法(会多执行代码块中标记custom code的那些代码)。
不过这些不是重点。
重点是,ignorePrefixList中包含了一些字符串,而要求是以这些字符串开头的方法,不执行//custom code中省略的逻辑,而是绕到else块直接执行原方法。
下面这段代码,看似在对方法名和ignorePrefixList中的字符串进行逐个比较的时候,没什么问题,但还是希望各位别看后面,推测下isMethodIgnored方法会发生什么问题。
代码:
1 public class MgtInvocation implements InvocationHandler{
2
3 private static List ignorePrefixList;
4
5 static {
6 ignorePrefixList = new ArrayList();
7 ignorePrefixList.add("get");
8 ignorePrefixList.add("list");
9 }
10
11 private Object target;
12
13 public Object invoke(Object proxy, Method method, Object[] args)
14 throws Throwable {
15 if(!isMethodIgnore(method)){
16 //custom code
17 //
18 return method.invoke(target, args);
19 //custom code
20 //
21 }else {
22 return method.invoke(target, args);
23 }
24 }
25
26 private boolean isMethodIgnore(Method mtd){
27 boolean res = false;
28 String mtdName = mtd.getName();
29 for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
30 res = res||mtdName.startsWith((String)it.next());
31 }
32 return res;
33 }
34 }
翻译成C#大致是这样的(C#没有动态代理,所以这个类是做做样子的,又因为Iterator和IEnumrator稍有不同,此处使用并不会导致错误,所以isMethodIgnore方法内部也稍有不同):
1 public class FakeInvocation
2 {
3 private static IList ignorePrefixList;
4 private object target;
5
6 static FakeInvocation(){
7 ignorePrefixList = new ArrayList();
8 ignorePrefixList.Add("get");
9 ignorePrefixList.Add("list");
10 }
11
12 public object invoke(object proxy, MethodInfo method, object[] args)
13 {
14 if (!isMethodIgnore(method))
15 {
16 //custom code
17 //
18 return method.Invoke(target, args);
19 //custom code
20 //
21 }
22 else
23 {
24 return method.Invoke(target, args);
25 }
26 }
27 public bool isMethodIgnore(MethodInfo mtd) {
28 bool res = false;
29 String mtdName = mtd.Name;
30 for (int i = 0; i < ignorePrefixList.Count; )
31 {
32 res = res || mtdName.StartsWith((String)ignorePrefixList[i++]);
33 }
34 return res;
35 }
36
37 }
38
上面代码的问题是,isMethodIgnored方法极易陷入死循环。
当ignorePrefixList中,除去最后一个字符串元素外,任何一个若为mtdName的起始字符串,整个方法就会陷入死循环。
印象当中,死循环是菜鸟中的菜鸟才犯,因此当时根本想不到自己的代码会死循环,还以为是线程间死锁了。看来看去,也没发现哪里同步块不对,最后靠单步debug才找出错误。
原因很简单,就是||运算符,在发现左边为true时,就直接返回true而不执行右边了。
所以当这个代理去代理以get开头的目标方法时,就出现了这种景象:第一轮比较了"get"后,res被设成了true,迭代子看到后面还有个"list"的字符串元素,于是执行
1 res = res||mtdName.startsWith((String)it.next());
而||发现res本来就为true,结果直接返回,没有执行关键的it.next();接着就去看循环条件了。但因为next()方法没执行,因此迭代子位置都没动,下一元素还是"list"字符串。结果又回到开头一幕。于是就死掉了...
修正得方法很多,交换||两端位置可以:
1 private boolean isMethodIgnore(Method mtd){
2 boolean res = false;
3 String mtdName = mtd.getName();
4 for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
5 res = mtdName.startsWith((String)it.next())||res;
6 }
7 return res;
8 }
把||改成|也可以:
1 private boolean isMethodIgnore(Method mtd){
2 boolean res = false;
3 String mtdName = mtd.getName();
4 for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
5 res = res|mtdName.startsWith((String)it.next());
6 }
7 return res;
8 }
遇到res为true直接跳出方法也可以:
1 private boolean isMethodIgnore(Method mtd){
2 String mtdName = mtd.getName();
3 for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
4 if(mtdName.startsWith((String)it.next()))return true;
5 }
6 return false;
7 }
另外,我在把java换成C#过程中,发现要在C#中重现这错误还不容易。
因为如果'直译'isMethodIgnore方法,就是
1 public bool isMethodIgnore(MethodInfo mtd) {
2 bool res = false;
3 String mtdName = mtd.Name;
4 for (IEnumerator eu = ignorePrefixList.GetEnumerator(); eu.MoveNext(); )
5 {
6 res = res || mtdName.StartsWith((String)eu.Current);
7 }
8 return res;
9 }
C#中的迭代子是不管后面有没有元素,先移动并返回移动成功与否(MoveNext),移动完了取当前元素(Current)。而java的迭代子是先向后瞟一眼(hasNext),发现有元素,取出下一个,并移动到那里(next)。
这样的话,移动操作并不是在 循环块内,因此可以保证每次都执行。
另外其实在C#和1.5以后的jdk里面,以上的for循环写法其实就是foreach的'手工版'。而如果用foreach的话,就不会有这个问题。
从上面的小错误,可以总结出两个经验:
- 用&&和||的时候得想一想,右边如果不执行会不会对整体造成影响。
- 尽量不把结束条件,前进操作之类写到for循环内部。