代码改变世界

我犯了一个错误,您能指出吗?(结论)

2009-09-08 15:55  Jeffrey Zhao  阅读(13554)  评论(55编辑  收藏

其实许多朋友已经在回复中发现问题所在了,其中最早指出错误的是狼Robot同学,他说:

每个T都会使用一个新的连接。

泛型类中的静态变量会因为T的不同而产生不同的值,也就是说每个T所访问的静态变量都是独立的。

正是这个原因,导致UserRepository和ArticleRepository,虽然似乎都继承了Repository<T>类,但是因为使用了不同的T类型,所以实际上它们是不同的类,而它们的ConnectionKey值是不同的。使用不同的ConnectionKey,就无法从ResourceManager中获得同一个Connection对象了。以下的代码可以很轻易地证明这一点。

public static class MyClass<T>
{
    public static readonly Guid Key = Guid.NewGuid();
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("int: " + MyClass<int>.Key);
        Console.WriteLine("string: " + MyClass<string>.Key);

        Console.ReadLine();
    }
}

由于MyClass<int>和MyClass<string>是不同的类,因此它们的Key也是分离的,值也不同(Guid.NewGuid()了两次)。因此,在泛型类中定义静态字段的时候一定要注意:不同泛型参数生成的具体类(无论是值类型还是引用类型),它们的静态字段是独立的。这点说起来简单,但是有时候不太容易意识到。例如,我之所以犯这个错误,正是因为原本Repository类是非泛型的,而后面由于某些原因才将其改为泛型。这样的错误使用单元测试也很难检查出来,非常隐蔽。

不过解决方案也非常简单,例如随意给出一个具体的Guid,而不是每次都使用Guid.NewGuid生成新的值:

public abstract class Repository<T>
{
    private readonly static Guid ConnectionKey = new Guid("a18b2f49-cafc-43e3-a49d-3fac91701394");
}

这样,虽然UserRepostory和ArticleReposityr的ConnectionKey还是不同的Guid对象,但是它们的“值”是相同的(也就是说GetHashCode相同,Equals返回true),对字典来说它们是相同的“键”。当然,还有其他解决方案,例如把ConnectionKey放到其它非泛型的类中去即可。

有些朋友还提出了其他的观点。例如,ResourceManager是不同的实例,怎么做到“保留Connection对象”呢?其实只需要它们都基于一个合适的数据容器就可以了,比如都基于HttpContext.Current。这方面的例子很多,比如不同的Connection对象都是访问同一个数据库的。因此,这里不是问题。

还有,有朋友认为共享Connection对象的做法不好。其实这也是没有关系的,因为这里“共享”的范围只是“单个请求”。对于ASP.NET请求来说,这些操作都是同步的,因此不会产生线程安全的问题。而一个请求的时间很短,因此Connection的生命周期也不长。这样的实践很多,例如NHiberante推荐为每个请求分配一个唯一的ISession对象(Sharp Architecture就是怎么做的)——这就相当于一个Connection——不过我不喜欢,因此我使用的做法是为单个请求按需创建多个Session,但是共享一个Connection对象。此外,共享Connection对象还有其他一些好处,例如不会引发需要MSDTC的分布式事务。

这个问题已经解决了。但是上文的评论中还有其他一些讨论。例如,您知道为什么下面的代码中,两个时间是相同的吗?

public static class MyClass<T>
{
    public static readonly DateTime Time = DateTime.Now;
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("int: " + MyClass<int>.Time);
        Thread.Sleep(3000);
        Console.WriteLine("string: " + MyClass<string>.Time);

        Console.ReadLine();
    }
}

它们输出的结果是:

int: 2009/9/8 15:30:06
string: 2009/9/8 15:30:06

这和我们的理解好像不同,因为当我们访问MyClass<string>的时候,应该比MyClass<int>要晚3秒钟,但为什么时间是相同的呢?那么我们把测试代码换一种写法,会更清楚一些:

public static class MyClass<T>
{
    public static readonly DateTime Time = GetNow();

    private static DateTime GetNow()
    {
        Console.WriteLine("GetNow execute!");
        return DateTime.Now;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Main execute!");

        Console.WriteLine("int: " + MyClass<int>.Time);
        Thread.Sleep(3000);
        Console.WriteLine("string: " + MyClass<string>.Time);

        Console.ReadLine();
    }
}

我们增加了会输出一些内容的GetNow静态方法,Main方法的开头也打印出一些内容。这段代码输出如下:

GetNow execute!
GetNow execute!
Main execute!
int: 2009/9/8 15:34:31
string: 2009/9/8 15:34:31

可以发现,在Main方法执行之前,MyClass<int>和MyClass<string>的GetNow就被调用了。因此,它们的Time字段是相同的。不过,如果我们在MyClass<>中增加一个空的静态构造函数,结果就会有所不同:

public static class MyClass<T>
{
    public static readonly DateTime Time = GetNow();

    private static DateTime GetNow()
    {
        Console.WriteLine("GetNow execute!");
        return DateTime.Now;
    }

    static MyClass() { }
}

输出如下:

Main execute!
GetNow execute!
int: 2009/9/8 15:40:12
GetNow execute!
string: 2009/9/8 15:40:15

由于GetNow方法只在“第一次”用到MyClass<int>和MyClass<string>时执行,因此获得的时间是不同的。不过,为什么加入了静态构造函数之后,Time字段的初始化时机就有所改变呢?那是因为IL中beforefieldinit修饰在作怪。关于这一点,许多书中都有提及。园子中的Artech同学对这个问题也有所分析

在目前的情况下,泛型类这一性质给我们造成了一定的麻烦。但是,只要我们使用得当,它也可以在某些场景下简化开发。因此,最后请大家和我一起在心中默念:信脑袋,得永生,信脑袋,得永生……