Hello World
Spiga

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

2009-09-08 15:55 by 老赵, 14749 visits

其实许多朋友已经在回复中发现问题所在了,其中最早指出错误的是狼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同学对这个问题也有所分析

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

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

55 条回复

  1. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 15:56:00

    沙发!

  2. 小眼睛老鼠
    *.*.*.*
    链接

    小眼睛老鼠 2009-09-08 15:57:00

    板凳

  3. 老赵
    admin
    链接

    老赵 2009-09-08 16:00:00

    @小眼睛老鼠
    我每天都在犯错,所以每天都有文章可写。

  4. 老玉米
    *.*.*.*
    链接

    老玉米 2009-09-08 16:03:00

    值类型以及引用类型的泛型应用上的差异....以前有看,不过没有引起重视.现在看来..成也细节,败也细节..
    一个很深刻的教训.

  5. 老赵
    admin
    链接

    老赵 2009-09-08 16:05:00

    @老玉米
    这里和值/引用类型无关,不同的类型就是不同的。

  6. 在别处
    *.*.*.*
    链接

    在别处 2009-09-08 16:13:00

    信脑袋,得永生
    --------------------
    老赵也来这套套

  7. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-09-08 16:16:00

    为啥提到了我呢~ 飘过……

  8. 老赵
    admin
    链接

    老赵 2009-09-08 16:17:00

    @装配脑袋
    因为你的文章写得好啊。

  9. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 16:17:00

    @装配脑袋
    你咋知道就是你呢?不兴别人也叫脑袋啊。

  10. 老玉米
    *.*.*.*
    链接

    老玉米 2009-09-08 16:17:00

    @Jeffrey Zhao
    学习了.我也犯错了~~

  11. 老赵
    admin
    链接

    老赵 2009-09-08 16:17:00

    @DiggingDeeply
    都引用了他的文章了还有谁阿,嘿嘿。

  12. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 16:29:00

    泛型类这一性质很好的保持了一致性
    MyClass<T>对于每一个不同的T都相当于一个完全独立定义的Class.
    不过直觉上容易认为MyClass<T>的定义好像就是所有具体MyClass<具体的T>类型的公共的基类.

  13. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-08 16:44:00

    信脑袋 满状态原地复活

  14. a1234[未注册用户]
    *.*.*.*
    链接

    a1234[未注册用户] 2009-09-08 16:45:00

    泛型类(C++里叫模板类)的语义本来就是你给不同的类型T编译器最终产生出不同的类

  15. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-08 16:49:00

    其实在第一个例子里,两个时间也是不同的,只是由于直接write出来只精确到秒,所以看上去似乎是“相同”的。write的时候用Time.Tick就可以了,呵呵

  16. 560889223
    *.*.*.*
    链接

    560889223 2009-09-08 16:57:00

    我觉得这篇文章有一个很重要的结论没提……就是编译器何时会加入beforeinitfield修饰符。如果能把这个修饰符出现的充分必要条件总结出来的话,开发的时候脑袋里应该会清晰很多,比较容易注意到危险的情况。

  17. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 17:00:00

    @麒麟.NET
    tick也是一样的吧。tick和datetime是一个意思吧,只不过是long型。


    @韦恩卑鄙
    至少也得满血满状态原地复活500次啊

  18. 老赵
    admin
    链接

    老赵 2009-09-08 17:01:00

    @麒麟.NET
    嗯嗯,没错没错。

  19. 老赵
    admin
    链接

    老赵 2009-09-08 17:01:00

    @560889223
    这就不是这篇文章的重点了,而且……文章里引用的资料已经写明了嘛。

  20. 老赵
    admin
    链接

    老赵 2009-09-08 17:02:00

    @DiggingDeeply
    tick精度高,可能是不同的,不过打印时只精确到秒,所以分辨不出。

  21. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-08 17:05:00

    560889223:我觉得这篇文章有一个很重要的结论没提……就是编译器何时会加入beforeinitfield修饰符。如果能把这个修饰符出现的充分必要条件总结出来的话,开发的时候脑袋里应该会清晰很多,比较容易注意到危险的情况。


    在没有静态构造函数的时候,编译器就会加入beforfieldinit修饰符

  22. pk的眼泪
    *.*.*.*
    链接

    pk的眼泪 2009-09-08 17:06:00

    Jeffrey Zhao:
    @DiggingDeeply
    tick精度高,可能是不同的,不过打印时只精确到秒,所以分辨不出。


    取Ticks也是相同的,除非在处理当中内存卡了一下,改成这样就可以看出不同了
       public static class MyClass<T>
        {
            public static readonly long Ticks = GetTicks();
            public static long GetTicks()
            {
                Thread.Sleep(1000);
                return DateTime.Now.Ticks;
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("int: " + MyClass<int>.Ticks);        
                Console.WriteLine("string: " + MyClass<string>.Ticks);
                Console.ReadLine();
            }
        }
    

  23. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 17:10:00

    @Jeffrey Zhao
    此属性的值表示自 0001 年 1 月 1 日午夜 12:00:00 以来已经过的时间的以 100 毫微秒为间隔的间隔数。
    100ms也就0.1s,赋值用不了那么长时间。几个ms就了不得了。

  24. pk的眼泪
    *.*.*.*
    链接

    pk的眼泪 2009-09-08 17:12:00

    @560889223
    在老赵的上篇文章里,55楼说的很地道:
    C#的定义中指出当只有在一个类型不具备静态构造器时它的BeforeFieldInit才会被自动标记上。事实上,这是由编译器帮我们完成的,它可能会导致一些我们意想不到的效果。

    最后我要再次强调,静态构造器并不等同于类型初始化器。任何类型都有类型初始化器,但不一定有静态构造器

  25. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 17:18:00

    @pk的眼泪
    类型初始化器和静态构造器,有什么区别?
    什么是类型初始化器?

  26. pk的眼泪
    *.*.*.*
    链接

    pk的眼泪 2009-09-08 17:18:00

    @Jeffrey Zhao
    所以我觉得,如果ConnectionKey要动态,改成这样就可以了
    public abstract class Repository<T>
    {
    private readonly static long ConnectionKey = DateTime.Now.Ticks;
    }
    本质上是多对象,实际上还是同一值。

  27. pk的眼泪
    *.*.*.*
    链接

    pk的眼泪 2009-09-08 17:22:00

    @DiggingDeeply
    我引用的是戏水说的原话,我认为类型初始化器应该是类的默认构造函数

  28. 哥只是个传说 [未注册用户]
    *.*.*.*
    链接

    哥只是个传说 [未注册用户] 2009-09-08 17:22:00

    jeffrey 的clr 2.0 上有说这个的。

  29. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-08 17:23:00

    DiggingDeeply:
    @Jeffrey Zhao
    此属性的值表示自 0001 年 1 月 1 日午夜 12:00:00 以来已经过的时间的以 100 毫微秒为间隔的间隔数。
    100ms也就0.1s,赋值用不了那么长时间。几个ms就了不得了。


    额……机器慢的问题暴露出来了……

  30. 老赵
    admin
    链接

    老赵 2009-09-08 17:27:00

    pk的眼泪:
    @Jeffrey Zhao
    所以我觉得,如果ConnectionKey要动态,改成这样就可以了

    public abstract class Repository<T>
    {
        private readonly static long ConnectionKey = DateTime.Now.Ticks;
    }

    本质上是多对象,实际上还是同一值。


    你不能保证他们是同时执行的,例如先访问了UserRepository,三秒钟后才访问ArticleRepository,而恰好又加了静态构造函数。
    还是不要基于这种假设来编程吧。

  31. pk的眼泪
    *.*.*.*
    链接

    pk的眼泪 2009-09-08 17:33:00

    @Jeffrey Zhao
    呵呵,的确,我思想狭隘了,受教。

  32. ToBin
    *.*.*.*
    链接

    ToBin 2009-09-08 17:47:00

    这都什么毛病啊,IE6还不让访问

  33. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 17:53:00

    一些静态成员+实例成员的规则及布局定义了一个内存视图
    或者是一种Schama,当你要使用这个类的实例的时候,编译器必须保证类型本身已经准备好,所以必须确保类的静态成员内存分配及初始化(C++等语言编译器不负责),所有的能够使用类型实例之前的准备工作,你可以看作类型准备动作。
    静态构造函数只是编译器提供给我们自己施展拳脚的机会,所以他不是全部工作。

    其实就是C语言定义几个静态变量,对其执行必要的内存分配,
    根据需要对其初始化,这些代码编译器帮你生成。

  34. Cheese
    *.*.*.*
    链接

    Cheese 2009-09-08 17:57:00

    DiggingDeeply:
    @Jeffrey Zhao
    此属性的值表示自 0001 年 1 月 1 日午夜 12:00:00 以来已经过的时间的以 100 毫微秒为间隔的间隔数。
    100ms也就0.1s,赋值用不了那么长时间。几个ms就了不得了。



    毫微秒……10的负9次方吧
    不过机器还真是猛……

  35. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 18:02:00

    用高级语言就要相信他声称自己所确保的东西。
    其实不同类型及其实例相互关系就是一种协议,方便你解读内存里一陀东西

  36. 560889223
    *.*.*.*
    链接

    560889223 2009-09-08 18:03:00

    @Jeffrey Zhao
    @麒麟.NET
    @pk的眼泪

    感谢各位的回复。我参考了这样一篇文章:http://www.yoda.arachsys.com/csharp/beforefieldinit.html

    据文章中所述,我的总结大致是这样的:
    确实如大家所述,在类型里不存在静态构造函数的时候编译器就会为增加beforefieldinit标记。但是具体什么时候执行类型初始化器是由运行时环境决定的,微软实现的、Mono开发团队实现的,决定权似乎是在运行时手里。

  37. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 18:10:00

    静态构造函数既然是用户的舞台,那么他可能在这个里面添加各种奇妙的行为,所以编译器必须以保守策略对待,所以编译器把自己所做初始化工作和用户静态构造函数一起提前,完成类型构造。
    没有静态构造函数,他只要确保你用之前搞定就没问题。

  38. 老赵
    admin
    链接

    老赵 2009-09-08 18:30:00

    @feilng
    似乎……有一点道理,呵呵。

  39. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 18:40:00

    各种构造函数都是编译器提供的在其设定条件下才可用的用户附加控制。
    C++语言的各种构造函数就是典型代表,最让人烦恼,总让人多费脑筋,搞的太繁复。
    有时候觉得面向对象解决的问题比带来的多,只是隐藏复杂性并不能带来真的简单和直观

  40. 老赵
    admin
    链接

    老赵 2009-09-08 18:45:00

    @feilng
    “面向对象理论”和“面向对象语言实现”是两码事吧。
    你现在说的应该都是后者带来的复杂程度,而“解决问题”靠的是前者。
    其实C#的“隐藏复杂性”似乎真不多,现在这种都是很长时间内才遇到一次。

  41. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 18:53:00

    不是说C#,我现在倾向于回归原始的数据+ 算法,
    当然也要看应用的性质。

    数据待一边,相关处理函数呆一边,一通收拾
    这就像集权,面向对象就像民主,呵呵

  42. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 18:58:00

    面向对象理论在现阶段的计算机体系下的实现几乎是确定的。
    现在的整个体系从开始就是结构化的。

  43. 老赵
    admin
    链接

    老赵 2009-09-08 19:05:00

    @feilng
    结构化编程我可以理解,但是这和面向对象编程没有太大关系吧。
    面向对象是一种更高级的抽象方式了。

  44. feilng
    *.*.*.*
    链接

    feilng 2009-09-08 19:06:00

    呵呵,最近对面向对象发点牢骚

  45. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-08 20:26:00

    @Cheese
    汗,tick我给看成100ms级别的了,少了个微字。

    给大家做个参考:(原链接不知道怎么打不开了,贴的我的转载)10年编程无师自通
    各种操作的计时,2001年夏天在一台典型的1GHz PC上完成:
        执行单条指令            1 纳秒 = (1/1,000,000,000) 秒
        从L1缓存中取一个word        2 纳秒
        从主内存中取一个word        10 纳秒
        从连续的磁盘位置中取一个word    200 纳秒
        从新的磁盘位置中取一个word(寻址) 8,000,000纳秒 = 8毫秒

  46. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-09 09:24:00

    又一个脑袋崇拜者诞生了,阿门。。。。。

  47. 老赵
    admin
    链接

    老赵 2009-09-09 09:37:00

    @Ivony...
    我崇拜脑袋好多年了……

  48. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-09 09:42:00

    Jeffrey Zhao:
    @Ivony...
    我崇拜脑袋好多年了……



    这就是你的不对了。。。。。
    泛型字典这是几年前的东西了,话说我还用这东西来做过Entity转换器的缓存。恐怕到现在为止这个方案还是最快的。

  49. 老赵
    admin
    链接

    老赵 2009-09-09 10:12:00

    @Ivony...
    我也用了很多年了,我又没说是因为这个原因崇拜脑袋的……

  50. 要有好的心情
    *.*.*.*
    链接

    要有好的心情 2009-09-09 11:40:00

    就asp.net程序而言, 我new一个数据库连接,应该放到HttpApplication中,还是HttpContext对象中?各有什么优缺点,各自的适应场景是什么?请教各位。

  51. 老赵
    admin
    链接

    老赵 2009-09-09 11:47:00

    @要有好的心情
    没说一定要放到HttpApplication或是HttpContext之中。
    如果真要放,肯定不会是和HttpApplication里,因为它是全局的。
    HttpContext是每个请求一个。

  52. 要有好的心情
    *.*.*.*
    链接

    要有好的心情 2009-09-09 13:12:00

    HttpContext是每个请求一个,是在请求到达后由HttpRuntime根据HttpWorkerRequest创建的实例。如果我的asp.net程序就是一个数据库,而且连接串也是固定了,创建一个DbConnection后,或者我用企业库创建一个Database对象后,放到HttpApplication中的缺点是什么?
    放到HttpContext中的话,每次请求都有创建一个吧。

  53. 老赵
    admin
    链接

    老赵 2009-09-09 13:13:00

    @要有好的心情
    一个Connection对象不能由多个线程同时操作,因此,至少要为每个请求创建一个Connection对象。

  54. 要有好的心情
    *.*.*.*
    链接

    要有好的心情 2009-09-09 13:28:00

    喔,对,忘了这个了,谢谢

  55. criticalthink[未注册用户]
    *.*.*.*
    链接

    criticalthink[未注册用户] 2009-10-12 11:24:00

    老赵,我是一个mvc的初学者,可以学习哈你这个项目的架构嘛?
    谢谢!

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我