周末熬夜终于搞定的AOP尝鲜系列文章第三篇一出,就收到了很多朋友的反馈和邮件。首先感谢大家的关注!也衷心谢谢给我指出文章错误、文字疏忽的朋友们!
有一位细心朋友今早给我发来邮件,“批评”我又在简单问题上面犯了糊涂,他所指的是文章中出现的这个语句:
Console.WriteLine("Add: " + Thread.CurrentContext);
其中Thread.CurrentContext属性将返回当前线程所处的执行环境,类型是System.Runtime.Remoting.Contexts名称空间中的Context。一个字串常量怎么可以和一个对象直接相加呢!肯定是我忘了写ToString()了吧?他对此的评语也很有意思,“老兄以为在写JScript呐!”……嘿嘿!一针见血啊!
我回复他,感谢他的热心和仔细(再次:),顺便问了一句:我文中的代码你动手编译过吗?因为我清楚的记得像这样的代码我已经不是写一次两次了——偶的实践经验告诉我:这个语句是可以在Visual C# .NET中编译并正确运行的!不仅如此,我还经常写这样的语句:
string tempFilename = null;
tempFilename += Environment.TickCount + ".tmp"; // 啊?string += int?
或者:
string guidKey = "{" + Guid.NewGuid() + "}"; // 奋特!
再或者:
string logLine = "Log: " + DateTime.Now; // 咣当……
……就像读者所说:这是C#还是JScript啊!?实际上,我也没有在C#语言规范中找到能解释这个现象的理论依据(如果你知道的话希望能告诉我:)。所以我只能说这是Visual C# .NET编译器的一种编译器优化手段造成的side effect(将静态字串相加转化为String.Concat()调用)——不过事实证明,这种写法的效率比起String.Format()还要高(虽然没有人家灵活),甚至有时候比每一个用ToString()还要快!原因是什么?也很容易分析出来,留给聪明的你做个练习吧(可以把你的想法用反馈告诉我)!:)
最后还是按照惯例,做个练习——想想下面这个表达式中对quiz求值将是什么结果?
string quiz = null;
quiz += 1 + 2 + "3" + 4 + 5;
P.S: USE THIS TRICK AT YOUR OWN RISK! AND IT IS NOT RECOMMENDED AT ALL (THOUGH IT'S OFTEN USEFUL AND HANDY DURING DEBUGGING.)
前面用透明代理机制实现简单AOP的文章一发,很快收到很多朋友的反馈,其中就有一些朋友提出关于文中所述的透明代理等技术还能够用于什么应用场合的疑问。说实在的,本来我就是想写一些关于这些技术在实际项目中应用的随笔的,可是想到可能会有很多朋友对这些技术的基本原理还不熟悉,这才萌生了先写一系列文章作铺垫的念头(可惜由于工作比较忙的缘故,至今还欠一篇没有写完,还请大家见谅啦,我抓紧时间补上——这篇内容更精彩噢!呵呵)。
今天我们又一次把我前面这两篇文字中提到的技术应用在项目开发中,初步反馈还是非常positive的,这里我不妨把问题先说出来,请看过文章的朋友们好好想想,看看如何利用已经学到的知识来解决这个实际问题吧——稍后我会回来说说我的想法(当然如果大家看过偶的文章以后都能够想出这种解决方案的话那当然就最好了:),当然如果你有其他更好的办法,更是欢迎你通过发表评论和大家分享!
这个问题和单元测试(Unit Testing)有密切关系(虽然我们的项目还不能算是完全的TDD,但是充分、必要的单元测试还是不可或缺),这里我们还是以简单的例子来说明,比如这里有一个银行服务的接口:
interface IBankService
{
void Withdraw(Account account, int amount);
void Deposit(Account account, int amount);
int QueryBalance(Account account);
}
这个接口的三个方法的作用很简单:取款、存款、查询余额。我们肯定要开发一个实现这个接口的具体业务操作类;与此同时,由于该服务接口对安全性的严格要求,项目组中专门负责安全规则的部门将以DECORATOR的方式针对该接口的每一个方法编写负责安全保障的修饰类(最终在运行时由组件工厂动态组合)。以这种设计方式,只要确定了服务接口,则两个类的开发就可以充分的并行进行了——真的是这样吗?我们写写看:
class BankServiceSecurityGuard: ComponentDecorator, IBankService
{
void IBankService.Withdraw(Account account, int amount)
{
if ( … )
throw new DailyAmountExceedsException( … );
if ( … )
throw new AccountBlockedException( … );
if ( … )
throw new PermissionNotGrantedException( … );
…
((IBankService)Decoratee).Withdraw(account, amount);
}
…
}
这个方法的实现就是一个把门儿的逻辑:结合方法调用参数使尽各种必要手段确保只有在符合所有安全规则的前提下才能够把调用转发到被修饰的(即decoratee)对象(也就是IBankService的核心实现类上)。也就是说,开发并测试这个安全保障修饰类的时候最终还是要依赖于一个能够工作的核心实现类,否则就算可以编译通过(还好这种依赖已经被接口隔离开了),你也无法让单元测试通过。我们能不能用什么办法来独立的测试这个修饰对象使之不依赖于一个正确工作的核心实现类呢?换句话说,有没有办法在没有可工作的核心实现类的情况下(这种情况常见于核心类开发周期较长的时候),让下面针对安全保障修饰类的单元测试代码正常起到单元测试的作用(一是驱动开发过程、二是形成软件规格说明、三是跟踪软件缺陷……)呢(我们先假设Account类是已经存在且经过充分测试的——其实对该类的隔离测试也是类似的道理)?
[TestFixture]
public class BankServiceSecurityTest
{
private IBankService bankService;
[SetUp]
public void Initialization()
{
bankService = new BankServiceSecurityGuard(???);
}
[ExpectedException(typeof(DailyAmountExceedsException))]
public void TestForExceedingDailyAmount()
{
AccountState state = AccountState.DailyAmountReached;
Account account = new MockAccount(state);
bankService.Withdraw(10);
}
public void TestForNormalWithdraw()
{
AccountState state = AccountState.Normal;
int initialBalance = 1000;
Account account = new MockAccount(state, initialBalance);
try
{
bankService.Withdraw(10);
Assert.IsTrue(coreBankService's been called for Withdraw(10));
}
catch(Exception ex)
{
Assert.Fail("this call should not throw exception");
}
}
}
简单说,我们要测试两类情况:一类是应该抛出安全异常的情况(TestForExceedingDailyAmount),在这种情况下,我们需要确信在给定的前提下必将产生指定的后果(比如特定类型的安全异常);另一类则是不该抛出异常的情况喽,在这种情况下我们不光要确信没有安全异常抛出,还要进一步确保方法调用确实被正确的(包括传入参数的值)转发给内部的指定方法了(否则一是有可能放过人为的安全后门,也有可能漏过修饰类忘记或错误调用被修饰类的情况)!
我已经听见有人高呼“简单简单”啦……写个mock对象不就结了!好,现在请你来为系统中数以百计的安全修饰类以及其他功能修饰类编写一一对应的mock对象好啦(幸好我们的接口都不是很fat,每个里面平均也就不到10个方法吧:)!反正我可真是一个超懒的程序员啊……你能给个省事儿又简单的方案吗?
先来看这段NUnit测试代码,我们希望用反射机制在运行时访问一个对象的枚举类型的域或属性:
[TestFixture]
public class PaymentInfo
{
public enum PaymentType
{
Cash, CreditCard, Check
}
public PaymentType Type;
public void Test()
{
PaymentInfo payment = new PaymentInfo();
payment.Type = PaymentType.Cash;
System.Reflection.FieldInfo enumField = GetType().GetField("Type");
int paymentTypeInt32;
paymentTypeInt32 = (int)enumField.GetValue(payment);
Assert.AreEqual((int)PaymentType.Cash, paymentTypeInt32);
enumField.SetValue(payment, paymentTypeInt32);
Assert.AreEqual(PaymentType.Cash, payment.Type);
}
}
实际上运行测试时发现在标红的这行上抛出一个异常:“对象类型无法转换为目标类型”。究其原因,原来是因为CLR的反射机制不允许枚举类型与整数类型之间隐式转换。不过C#编译器还是允许我们通过强制类型转换的语法来进行两者间的显式转换。
在这个测试中,使之通过的办法其实非常简单:把划线部分强制转换为枚举类型即可,如:(PaymentType)paymentTypeInt32。可问题是:在运行时如何动态转换类型呢?比如说我在写ElegantDAL的时候,需要将从数据库读出的一个类型为int的数值写入到要返回的对象的一个枚举型字段中,此时我只有fieldInfo、columnValue和resultObject,然而写成fieldInfo.SetValue(resultObject, columnValue)就会出现前面提到的错误,可是我又只有一个运行时的Type信息(fieldInfo.FieldType),我又不能写成fieldInfo.SetValue(resultObject, (fieldInfo.FieldType)columnValue)……
只好将这种情况列为一个特例处理,而我们的救兵则是Enum.ToObject()方法——你知道有更好的方法解决这个问题吗?
BTW: 很多朋友写信来问偶承诺的下一篇文章什么时候才能搞定,其实我也不想拖,可最近工作确实比较紧张,而我的最佳写作时间又都在深更半夜(之前还要热身进入状态),所以迟迟不能结稿。目前其实已经写了好多(已经和第一篇长度差不多了),可是发现要写的内容还太多(也许是我写得太细了,因为我的目标不仅是讲how,更多的是what和why),所以现在决定把本篇文字再细分为两部分(上篇只讲TP/RP+MBR对象;把CBO相关的内容放到下篇再写),以便能够尽快让大家看到新的内容——嗯,争取周五release吧。至于还有朋友希望我能够深入写些关于ElegantDAL的设计与实现细节,这个留待稍后吧,正好我们将要整理的项目文档中也要有这一块内容(现在在我们这个企业级项目中用得还真挺好),到时候一起写啦(希望能够和canyue尽快完成第三版本的重写工作,目前使用的是第二版)。
TLS(线程局部存储,thread local storage)在类库和多线程应用开发中是个有用的东东,在很多语言和工具中都有很好的支持(如Visual C++里面的__declspec(thread),Delphi中的threadvar等等,Win32 API中也有对应的Tls族函数)。有些刚接触.NET的朋友就开始抱怨了,说在管制环境下没有TLS了,得自己写了。其实不然,虽然在C#/VB.NET等语言中没有直接的关键字或语句来声明TLS,但是CLR通过定制属性更直观的支持着这一特性,这个属性就是ThreadStaticAttribute。
如果你希望一个静态成员(static in C#, Shared in VB.NET)对于不同的线程(更准确的说,app-domain与线程的组合)有不同的值(也即TLS的行为),则只需要为其设置ThreadStatic属性就可以了,无需作任何编程处理(当然这是declarative的做法,相应的programmatic方法也有,具体的可以参见Thread.AllocateDataSlot和Thread.AllocateNamedDataSlot方法,或检索.NET SDK Documentation Index中的TLS条目)。
文档中提醒一点要注意的是,任何访问线程局部静态成员的代码,只要不是运行在访问该成员所在类的第一个线程上时,都应该将该成员看作是null引用(引用类型)或默认初始值(值类型)。也就是说,不要依赖于类的构造器去初始化ThreadStatic成员,原因是显而易见的。
另外,在ASP.NET等多线程环境中使用TLS成员也要慎重,因为这些线程的生命周期不是由你来控制的,它们是从HttpRuntime管理的线程池中被重用的,因此在一次请求中使用的TLS成员有可能在另一次毫不相关的请求中被得到或修改(除非这就是你希望的效果)。如果希望使用请求相关的存储环境,可以考虑使用HttpContext.Current实例的Items集合(该集合在Server.Transfer等情况下可用于在同一次请求的不同页面间传递和共享状态)。
在mmkk的blog上看到一个关于Object.GetType()的观点,想作些补充,和各位分享(原文请见http://v-instru.com/blog/posts/220.aspx)。
由于所有的类都隐式继承Object,并且获得Object.GetType(),不过由于派生类可以new一个新的GetType()遮蔽Object.GetType(),比如:
public new Type GetType()
{
return typeof(System.String);?
}
这样的话如果instance想调用GetType()来获得精确类型的话得到将是System.String,而不是期望得到的,相反通过Type.GetType(string)这种形式可以绕过这个不确定因素,Type.GetType()是否应该是一种更好的习惯呢?
很显然,按照平时使用GetType()方法时的直观感觉,该方法在不同类型的对象实例上执行时将得到不同的结果,以面向对象的说法,这应该就是传说中的“多态”方法咯!而实际上,该方法并非虚方法,其“多态”的行为实际上是CLR类型系统的特殊实现所形成的,细节掠过,感兴趣的朋友可以参考一下SSCLI(即Rotor)中的源码。
因此,从理论的角度讲,要想得到真正的Object.GetType()的行为,应该只通过类型为Object的实例引用调用该方法;从实践的角度看,除非有特别的企图,正常的类型定义也不会去故意的“隐藏”Object默认的GetType()行为,所以一般用任何类型的实例引用调用GetType()也不会得到意外的结果。
至于说到底有什么原因需要提供一个全新的GetType()方法以隐藏默认的Object.GetType()行为,我还没想出来。而且就算提供了新的实现,也需要使用派生类的引用类型去调用才能获得这一新的变化。
有意思的是,如果你用Visual C# .NET编译后面附带的代码(在评论中),你会发现在编译出来的IL代码中,调用GetType()用的是callvirt指令(而不是我以为的call指令!两者区别由名称即可看出,这里就不赘述)!起初我以为这是C#编译器对GetType()方法的特殊支持,后来发现原来所有调用类实例方法的指令都是用callvirt而不是call。查过资料后才知道,原来是VC#.NET的编译器利用了callvirt指令的一个特殊功能,即在实际调用方法前首先验证this指针(即对象引用)的有效性,而这个特性是call指令所不具备的(但却是一个非常重要且实用的特性)。除此区别之外,call指令是使用类型的方法表寻找所需调用的方法入口,而callvirt是从实例的虚方法表开始寻找。但当使用callvirt调用一个非虚方法时,因为其肯定不存在于实例的虚方法表中,因此最终还是在类型的方法表找到所需调用的方法的入口,因此调用的还是所指定的非虚方法(但这样是否引入了额外的开销呢?有空再深入研习一下咯……噢,好象跑题了)
罗嗦半天,其实就是为了得到这样两个结论:第一,总是使用Object类型的对象引用调用GetType()方法以得到实例的真实类型(objectInstance.GetType()或((Object)anyTypeInstance).GetType());第二,既然编译器已经将覆盖基类成员这一操作列入警告级别,则该操作就是很有可能引入问题的操作,那么任何显式的压抑警告的做法(如使用new成员修饰符)都是值得反复思量并显式说明的。
至于说是否改用Type.GetType()是一种更好的习惯,我想这两个方法的名称虽然相同,但是目的却是不同的(因为前者是Object类的,而后者是Type类的。所属的类不同,方法的作用基础也就不同,含义也就不同了),一般也是很难简单换用的。不知mmkk是否认同我的以上看法呢?:)
在使用Visual Studio .NET 2003的Visual C# .NET产品时发现一个问题(我用的是英文版本,在同事的中文版本中也存在同样的问题):SqlParameter.IsNullable属性无法在IntelliSense中正确列出(相反,其getter/setter方法却被错误的显示在成员提示中,即get_IsNullable和set_IsNullable)。大概看了一下IsNullable属性和其他属性成员的差别,看来是因为被打上了DesignOnlyAttribute的原因。然而即便如此,属性的两个hidden getter/setter也不应该显示出来啊(这么写也肯定编译不过去)。
同样的问题也存在于使用OdbcParameter、OleDbParameter时,因此问题可能是双方面的:一是VC#.NET的IntelliSense对DesignOnlyAttribute的支持似乎有问题(隐藏属性是对的,因为DesignOnly说明该属性只为设计时期所用,运行期代码中是不该引用的;然而不能因此就把getter/setter方法给列出来);另一方面,从IsNullable的文档来看,这也不应该是一个DesignOnly的属性,因此可能是.NET Framework的一处疏忽。
当然,也有可能是我有什么细节不知道的说,如果是这样请一定告诉我啊……
继续前面的话题,上次给出了个不甚明确的大图画,结果引来众多高手指教,不亦乐乎!这次我想明确一下、具体化一下,就是下面这样一个命题(C#示意性代码):
如果有:
[TableMapping("Book")]
public class BookInfo
{
public BookInfo(int bookID, string title, string publisher);
public int BookID;
public string Title;
public string Publisher;
}
怎样将这段代码:
public BookInfo[] SelectByPublisher(string Publisher)
{
SqlCommand command = new SqlCommand();
command.CommandType = CommandType.Text;
command.CommandText = "SELECT BookID, Title, Publisher FROM Book WHERE Publisher=@Publisher";
command.Parameters.Add("@Publisher", Publisher);
using (SqlConnection connection = new SqlConnection(connectionString))
{
command.Connection = connection;
connection.Open();
SqlDataReader reader = command.ExecuteReader();
if (!reader.HasRows) return new BookInfo[0];
ArrayList result = new ArrayList();
while (reader.Read())
{
result.Add(new BookInfo(reader[0], reader[1], reader[2]));
}
return (BookInfo[])result.ToArray(typeof(BookInfo));
}
}
用这样的统一方式来轻巧的实现:
public BookInfo[] SelectByPublisher(string Publisher)
{
return (BookInfo[])ElegantORM.Execute(Publisher);
}
显然,我们需要实现ElegantORM(暂且不论这个codename起的多烂啦:)这个helper。怎么实现最合理?它应该能够支持前文所述的几种常见数据访问组件接口方法的signature,且不仅依赖于使用custom attributes的映射元数据(即可扩展到支持外部映射配置文件等等)……
BTW: 在本文下我们不讨论三层结构相关的话题(不过欢迎你把这方面的见解继续发到我的前一个随笔中:),只针对这个具体命题的设计和实现,我很想听到大家的见解并分享自己的心得。:)
昨天看到erictang2003给我的一个留言,其中提到了一个乍一看很古怪的问题:
以下代码中用自定义class填充ArrayList:
为何当数据源被Cache缓存后,<%# ((MyInfo)Container.DataItem).Item %>就不成功,抛出"System.InvalidCastException: 指定的转换无效" ,但是如果不用Cache缓存,就可以成功?
(代码略,请见原始内容)
我试验了一下,还真遇到了这个问题。不过简单分析了一下原因,发现实际上与Cache无关(存在Session里面也一样),而且仅当把自定义类写在.aspx页面中的情况下才发生(在code-behind中就不会出问题)。这让我想到了ASP.NET Runtime在动态编译时的一个行为,即每次.aspx页面文件变化时都将自动重新编译出新的assembly(可以观察%SYSTEMROOT%\Microsoft.NET \Framework\v1.x.xxxx \Temporarily ASP.NET Files \virtualroot下面的dll文件)。为了确认这就是问题的原因,我清空了所有已经编译好的页面assembly,然后访问你写的这个页面(第一次访问将写入Cache),然后刷新页面(这次应该就是从Cache中取得),没有任何问题!然后将页面文件略作修改再次保存,访问(此时ASP.NET将重新编译出一个新的assembly),类型转换出错了!
实际上,如果你在页面上显示一下Container.DataItem.GetType().AssemblyQualifiedName和typeof(MyInfo).AssemblyQualifiedName就发现问题了。原来存在Cache中的对象还是原来的那个assembly中的类型,而再次数据绑定时你将它转换为的MyInfo是在新的assembly中的类型!虽然类型的全称是相同的,但是由于它来自于两个不同identity的assembly,因此CLR并不认为他们是同样的类型。而当使用code-behind的时候,你的自定义类型所在的assembly并不会随着.aspx文件的修改而变化,因此Cache中存的对象和你将要转换的对象也是一致的。这就是所谓的强类型系统喽(相对而言的有些弱类型系统可能只是通过比对类型的文本相等就认定类型相等)。
总之,又是一个值得注意的问题。:)
I found this tip in an incidental and quick Rebuild when I suddenly notice the extra comma but compiler didn't report any error before I could be able to abort it! I thought it were a nice and kind bug in C# compiler but it's NOT, as shown in C# Language Specification 12.6 (array initializers), 14.1 (enum declarations), 17.2 (attribute specification)!
As in the following examples:
object[] x = new object[] {a, b, c,}
and:
[Obsolete, Flags,]
public enum Y
{
A,
B,
C,
}
which allows you to easily reorder the declaration list by using ^L...^V or Shift-Alt-T in VS.NET:
[Obsolete, Flags,]
public enum Y
{
A,
C,
B,
}
Isn't?this useful and flexible??
And finally, in keeping with?tradition of leaving you a quiz, does this work for method parameter declaration?
void Method(string x, int y,) {} // WORK OR NOT?
关于C#中的const和readonly想必使用C# .NET开发的朋友都很了解吧?总结一下const和readonly也就这么几条吧:
- const和readonly的值一旦初始化则都不再可以改写;
- const只能在声明时初始化;readonly既可以在声明时初始化也可以在构造器中初始化;
- const隐含static,不可以再写static const;readonly则不默认static,如需要可以写static readonly;
- const是编译期静态解析的常量(因此其表达式必须在编译时就可以求值);readonly则是运行期动态解析的常量;
- const既可用来修饰类中的成员,也可修饰函数体内的局部变量;readonly只可以用于修饰类中的成员(UPDATED:谢谢MicroHelper的提醒!)。
前面几条也没什么可说的,不过关于这第4条,里面还是有些文章可做的。试试下面的例子,看看是否与你所想一致吧!
显示全部
0. 常量与静态只读变量类库(文件名Consts.cs)
public class Consts
{
? public const string Const = "const";
? public static readonly string Readonly = "readonly";
}
执行csc /t:library Consts.cs编译输出Consts.dll。
1. 客户端(文件名Quiz.cs,编译选项/r:Consts.dll)
public class Quiz
{
? public static void Main()
? {
??? System.Console.WriteLine(Consts.Const);
????System.Console.WriteLine(Consts.Readonly);
? }
}
执行csc Quiz.cs编译输出Quiz.exe.
2. 执行Quiz.exe,输出如下:
const
readonly
3. 现在把Consts.cs里面的两个常量的值都换成大写并重新编译Consts.cs(Quiz.exe不变,仍是引用Consts.dll)。再执行Quiz.exe,输出会是……什么呢?
下面的工具函数可以构造source对象的deep copy,很有用!不妨一试 
using System.IO;
using System.Runtime.Serialization.Binary;
public object static DeepClone(object source)
{
? if (source==null) return null;
? using (MemoryStream stream = new MemoryStream())
? {
??? BinaryFormatter?formatter = new BinaryFormatter();
??? formatter.Serialize(stream, source);
??? stream.Position = 0;
??? return?formatter.Deserialize(stream);
? }
}