zoukankan      html  css  js  c++  java
  • C#中Finalize方法的问题

    C#中Finalize方法的问题

    ninputer在关于“值类型的Finalize不会被调用”中(http://blog.joycode.com/lijianzhong/archive/2005/01/13/42991.aspx#FeedBack)评论到“VB对Finalize管的可松呢,可以直接重写、直接调用、允许不调用父类的Finalize,或者多次调用父类的Finalize等等…… 完全不像C#”。

    其实C#的Finalize方法看起来只是比VB的好一点,但仍然有非常隐蔽的问题。问题如下。

    首先来看如下的代码:

    using System;

    public class Grandpapa
    {
         ~Grandpapa(){ Console.WriteLine("Grandpapa.~Grandpapa");}
    }

    public class Parent:Grandpapa
    {
         ~Parent(){ Console.WriteLine("Parent.~Parent");}
    }

    public class Son:Parent
    {
         ~Son(){ Console.WriteLine("Son.~Son");}
    }

    public class App
    {
         public static void Main()
         {
             Son s=new Son();
     
             GC.Collect();
             GC.WaitForPendingFinalizers();
         }
    }

    这段代码的运行结果毫无疑问是:

    Son.~Son
    Parent.~Parent
    Grandpapa.~Grandpapa

    这没什么问题。但是如果将Parent类重新定义如下,会出现什么情况呢?

    public class Parent:Grandpapa
    {
         protected void Finalize(){ Console.WriteLine("Parent.Finalize");}
    }

    运行结果变成了:

    Son.~Son
    Parent.Finalize

    情况已经有些不妙了,我在Parent中定义了一个“普通”的Finalize方法,竟然被它的子类Son的析构器给调用了?

    当然Finalize方法在C#中并不是一个“普通”的方法,析构器编译后就是一个有上述签名的Finalize方法。但C#编译器并没有禁止我们定义“普通”的Finalize,

    C#规范也没有指出定义这样的Finalize方法就是在定义一个析构器——实际上也不是,只是上述代码的表现如此——甚至还有这样一句诱人犯错的话:The compiler behaves as if this method(Finalize), and overrides of it, do not exist at all。分析IL代码可以看出,Parent中定义的“普通”的Finalize方法实际上“欺骗”了它的子类。它的子类只关心其父类是否定义了Finalize(当然签名要为上述形式)方法,它并不关心那个Finalize方法是否具有“析构器”语义。

    如果上述代码的行为通过理性分析还算可以接受的话,那么下面代码的运行结果就令人眩晕了,将Parent类重新定义如下(在上面的基础上添加了一个virtual关键字):

    public class Parent:Grandpapa
    {
         protected virtual void Finalize(){ Console.WriteLine("Parent.Finalize");}
    }

    编译后运行结果如下:

    Grandpapa.~Grandpapa

    这一次从IL代码的角度也解释不清了,我怀疑CLR对于析构器的判断是否还有另外的附加条件,但无论如何C#编译器呈现的行为是诡异的,因为这种结果放到哪里都是难以自圆其说的。我曾经为此挖掘了sscli源代码很长时间,但是就是找不到原因。

    这一方面是C#编译器的一个bug,另一方面也是CLR的一个bug。这个bug从.NET Framework的1.0版(VS.NET 2002),到1.1版(VS.NET 2003),以及Alpha版本的Longhorn操作系统中自带的1.2版都存在。后来我写信给C#的产品经理Eric Gunnerson(http://blogs.msdn.com/ericgu/)告诉他们这个bug。Eric Gunnerson随后回信告诉我他们会修复这个bug。

    我现在使用Visual C# Express 2005编译器编译(version 8.00.41013)上述代码,后面两种修改版都会得到一个warning:

    warning CS0465: Introducing a 'Finalize' method can interfere with destructor invocation. Did you intend to declare a destructor?

    但是如果不理会这样的警告,得到的exe文件执行行为仍然是非常奇怪。也就是说CLR中的bug仍然没有fix。我个人认为对于C#编译器来说,warning是不够的,应该彻底禁止定义这样的Finalize方法。

    实际上在我的Effective .NET (in C#)一书的draft里也有这样一个条款:

    # 不要在一个类中有定义任何Finalize方法的念头,因为那样会对你的“析构器链”造成潜在的严重的伤害。

    发表于 2005年1月13日 19:55

    评论

    # re: C#中Finalize方法的问题 2005-1-13 21:06 寒星

    受救了。

    # re: C#中Finalize方法的问题 2005-1-13 21:06 寒星

    受教了。

    # re: C#中Finalize方法的问题 2005-1-13 21:20 开心就好

    我喜欢这样的交流:)

    # re: C#中Finalize方法的问题 2005-1-14 1:03 lonelystranger

    # re: C#中Finalize方法的问题 2005-1-14 9:08 Ninputer

    好啊,看来我抛的砖头引来了不少美玉啊,呵呵
    看来该有人写写VB的书说说这些情况,好让VB程序员用好.NET语言中唯一可以自由使用的Finalize。

    # re: C#中Finalize方法的问题 2005-1-14 9:13 Ninputer

    在VB里更可以用Shadows将基类的Finalize彻底掩盖掉,然后在其后续子类中就没有原来那个Object的Finalize了!但如果CLR对Finalize特别干涉(就像Constructor那样),Finalize的语义就会发生变化,VB的语法就要发生Break的变化,这是很可怕的。所以我们还是在编译器上做这个限制比较安全。

    # re: C#中Finalize方法的问题 2005-2-22 17:53 Ivony

    我想了一会儿,觉得原因也许非常简单:

    所有的Finalize()方法都是这个样子:

    protected override Finalize()
    {
    //析构
    base.Finalize();
    }

    然后,最重要的是GC是这样调用Finalize():
    ((object) obj).Finalize()

    出现第一种情况是因为:
    Parent覆盖了默认的Finalize(),但这对继承链没有造成任何影响!!Finalize在这里形成了两个分支:
    protected override GrandPapa.Finnalize() {}// (A)
    protected override Parent.Finalize() {}//这个方法不存在,但这并不会影响虚方法链。(B)
    protected override Son.Finalize() { .... }//省略内容。(C)

    protetced new Parent.Finalize() { Console.WriteLine("Parent.Finalize");}//new修饰符被作者省略了。(D)

    GC开始调用Finalize方法,因为ABC都是在object.Finalize()这个虚方法链上的,所以GC会先调用C方法。
    然后,重要的分支出现了,因为D覆盖了B,所以C中的base.Finalize()是D方法!而D并未使用base.Finalize向上传播,所以执行到这里截止了。



    那么,第二种情况也很好解释了,第二种情况的继承链是:
    protected override GrandPapa.Finnalize() {}// (A)
    protected override Parent.Finalize() {}//不存在的方法(B)

    protetced virtual Parent.Finalize() { Console.WriteLine("Parent.Finalize");}//这里同样省略了new修饰符。(D)
    protected override Son.Finalize() { .... }//省略内容。(C)

    哈哈,现在知道为什么出现了作者百思不得其解的问题了吧,第二种情况由于Parent.Finalize有了virtual修饰,覆盖掉原来的override Parent.Finalize(),所以下面的override Son.Finalize()被继承到了virtual Parent.Finalize()下面。
    结果导致GC先执行那个不存在的B方法,然后执行A方法。。。。

    # re: C#中Finalize方法的问题 2005-2-23 15:20 李建忠

    To Ivony,这个解释比较牵强,而且很多地方都有漏洞:)

    1。首先((object) obj).Finalize()这样的说法就是错误的,对于虚方法来讲:

    obj.Finalize()和 ((object) obj).Finalize()的调用是完全一样的,不存在任何区别。这本身就是多态应有之意。

    2。“因为D覆盖了B,所以C中的base.Finalize()是D方法”

    这句话说得没道理。实际上第一种情况的错误无非是C#将Finalize和~Parent()划了等号,结果是定义
    protected void Finalize(){ Console.WriteLine("Parent.Finalize");}
    相当于定义了一个~Parent,只不过编译器没有在我们自己写的Finalize中插入base.Finalize的调用。

    3。“第二种情况由于Parent.Finalize有了virtual修饰,覆盖掉原来的override Parent.Finalize()”

    这个完全不叫覆盖(override),而叫隐藏(hide),注意其中编译后的元数据有一个newslot——也就是一个新的虚表slot。

    4。“所以下面的override Son.Finalize()被继承到了virtual Parent.Finalize()下面。”

    抱歉,听不懂这句话的意思。


    5。“结果导致GC先执行那个不存在的B方法”

    不存在如何执行呢?

    # re: C#中Finalize方法的问题 2005-2-23 17:49 Ivony

    1。首先((object) obj).Finalize()这样的说法就是错误的,对于虚方法来讲:

    obj.Finalize()和 ((object) obj).Finalize()的调用是完全一样的,不存在任何区别。这本身就是多态应有之意。

    ------------------------------------------

    的确是这样的,只需要:
    object obj = xxxx;
    然后obj.Finalize();就是执行object继承链上的Finalize方法,写一个强制类型转换只不过是为了强调GC在调用Finalize方法的时候,是对一个object类型的对象调用的。



    2。“因为D覆盖了B,所以C中的base.Finalize()是D方法”

    这句话说得没道理。实际上第一种情况的错误无非是C#将Finalize和~Parent()划了等号,结果是定义
    protected void Finalize(){ Console.WriteLine("Parent.Finalize");}
    相当于定义了一个~Parent,只不过编译器没有在我们自己写的Finalize中插入base.Finalize的调用。

    -----------------------------------------

    不是这样的,即使你写了Finalize方法,默认的Finalize方法依然存在,这就是所谓的隐藏掉了基类的方法。然后Parent的子类在调用base.Finalize()的时候,就会调用你写的那个,而不会导致原有默认的Finalize方法丢失。



    3。“第二种情况由于Parent.Finalize有了virtual修饰,覆盖掉原来的override Parent.Finalize()”

    这个完全不叫覆盖(override),而叫隐藏(hide),注意其中编译后的元数据有一个newslot——也就是一个新的虚表slot。

    ----------------------------------------

    嗯,微软是叫隐藏,反正就是这个意思了。



    4。“所以下面的override Son.Finalize()被继承到了virtual Parent.Finalize()下面。”

    抱歉,听不懂这句话的意思。

    --------------------------------------

    这里不知道怎么回事把我用于排版的空格给弄没了,我再发一下继承链(override链)。

    第一种情况
    protected override GrandPapa.Finnalize() {}//(A)
    __protected override Parent.Finalize() {}//(B)虽然不存在,但仍是override链的一环
    ____protected override Son.Finalize() { .... }//(C)这个方法override掉了上面的(B),但其base.Finalize却是下面的(D)。

    protetced new Parent.Finalize() { Console.WriteLine("Parent.Finalize");}//(D),这个方法没有override任何方法,不在override链中。

    第二种情况
    protected override GrandPapa.Finnalize() {}// (A)
    __protected override Parent.Finalize() {}//(B)

    protetced virtual Parent.Finalize() { Console.WriteLine("Parent.Finalize");}//(D)
    __protected override Son.Finalize() { .... }//(C)

    关键在这里:
    class Son
    {
    __protected override Finalize()//这里是override掉了(D)而不是(B)。
    }




    5。“结果导致GC先执行那个不存在的B方法”
    不存在如何执行呢?

    我们可以把情况想成这样,当一个类没有实现基类的virtual方法时,编译器会变出一个默认的方法来维系这个override链(当然,只是假设):
    [code]
    override return_type Method( params )
    {
    return base.Methos( params );
    }
    [/code]
    强调执行不存在的B方法只是为了说明override链。
    方法虽然不存在,但在描述override链的时候,必须写一下,否则override链就断了。

    # re: C#中Finalize方法的问题 2005-2-23 18:00 Ivony

    总结,当运行时多态的时候,系统会沿着override链条(上面的确是没说清楚,不应该用继承链这个词)寻找链条终点上的方法执行。而并不是将这个对象还原成最原始的面目(Son)进行调用。


    C#使用方法的签名来判断一个方法。

    当出现上文中第一种情况时,由于两个方法的签名并不相同,一个是sealed的,一个是virtual的,所以override链并不会被破坏,系统找到了正确的方法进行执行。

    然而在上文中第二种情况时,由于两个方法的签名完全一样,都是virtual的,所以override链在这里被打断,Son其实是override作者在Parant类中新定义的方法。



    发现我的真的说不好,干脆待会儿我发幅图片上来。。。

    # re: C#中Finalize方法的问题 2005-2-23 18:10 李建忠

    1. 我理解你的意思,但是你的表述很容易让人误解。我想更准确的说法是:

    GC调用的是一个类型中虚表slot上第一个Finalize方法。

    只有在newslot——也就是一个类型中可能存在多个同名的虚表(new virtual)——时,才存在obj.Finalize()和 ((object) obj).Finalize()调用的区别。

    2。“不是这样的,即使你写了Finalize方法,默认的Finalize方法依然存在,这就是所谓的隐藏掉了基类的方法”

    这不是隐藏(hide),而是实实在在的override——在同一个虚表slot上的override。

    3。我们尽量用英文术语来讨论,不然我很快就被你搞晕了,呵呵:)

    4。我猜出你实际上在叙述虚表slot的问题,尤其是有了一个newslot。但是你的文字表述很难让人看懂。

    5。我们把“override链”还是说成虚表slot比较准确,也容易讨论。

    # re: C#中Finalize方法的问题 2005-2-23 18:38 Ivony

    还有一个地方,也就是第一种情况:

    public class Parent:Grandpapa
    {
    protected void Finalize(){ Console.WriteLine("Parent.Finalize");}
    }

    这里没有override关键字,所以应该不是override。。
    我想写一个程序来验证我的想法。待会儿把结果发上来。



    抱歉,因为我对于系统到底是怎么实现虚方法的了解太少了,只是凭感觉把自己的想法说了出来,所以很多地方都不是专业名词,把大家给弄糊涂了。。。。

    # re: C#中Finalize方法的问题 2005-2-23 18:38 李建忠

    To Ivony,

    你的总结不错,但我想更精炼、更准确的总结应该是这样(可以在Rotor的源代码中得到印证):

    GC在调用Finalize方法时选择的是第一个虚表slot上的Finalize方法(也就是Object当初定义的那个虚表slot上的Finalize方法)。

    而第一种情况始终位于同一个虚表slot上。第二种情况则从class Parent开始引入了一个新的newslot。



    很喜欢这样的讨论,欢迎常来:)

    # re: C#中Finalize方法的问题 2005-2-23 18:47 李建忠

    To Ivony,

    这里我错了,确实不是override,虽然有的override并不需要override关键字。protected void Finalize()只是“欺骗”了C#编译器,虽然它有一个警告。Thanks。

    # re: C#中Finalize方法的问题 2005-2-23 19:08 Ivony

    将Son中的析构函数取掉后,验证了我的想法,Parent.Finalize没有被执行,可以肯定Parent.Finalize没有override了。

    # re: C#中Finalize方法的问题 2005-2-23 19:21 Ivony

    这应该是微软的一个失误,可能是设计人员将base.Finalize想当然的看成了被override掉的那个Finalize方法。而base.Method并不一定是你override掉的方法,这是C#里面的一个陷阱,也让C#引入new修饰符后变得复杂。

    有意思的是,在一个类里面不能同时写Finalize和析构函数,编译器会提示说已经存在了一个相同签名的Finalize方法。哈哈。。。。

    # re: C#中Finalize方法的问题 2005-2-25 13:46 Ivony

    再次出现古怪的现象:

    public class Parent : Grandpapa , IFinalize
    {
    // ~Parent(){ Console.WriteLine("Parent.~Parent");}
    #region IFinalize 成员

    public void Finalize()
    {
    Console.WriteLine("Parent.IFinalize.Finalize");
    }

    #endregion
    }


    这段代码的执行结果是抛出异常:

    未处理的“System.TypeLoadException”类型的异常出现在 未知模块 中。

    其他信息: 方法实现中引用的声明不能是 final 方法。类型:TestFinalize.Son,程序集:ConsoleApplication1, Version=1.0.1882.24453, Culture=neutral, PublicKeyToken=null。

    # re: C#中Finalize方法的问题 2005-2-25 13:51 Ivony

    原因可能是在Son中会自动生成一个 override Finalize()方法,而编译器认为这个Finalize方法是override Parent.IFinalize.Finalize的,所以又报错。。。。但这发生在运行期。。。。

    # re: C#中Finalize方法的问题 2005-2-28 13:09 李建忠

    IFinalize是一个什么接口呢?

    # re: C#中Finalize方法的问题 2005-2-28 13:21 李建忠

    如果是一个自己定义的IFinalize接口:
    interface IFinalize
    {
    void Finalize();
    }

    我并没有遇到你所说的问题,能详细讲一下吗?

    # 对不起,发得匆忙,代码没发全。 2005-3-1 18:29 Ivony

    using System;

    namespace TestFinalize
    {

    public class Parent: IFinalize
    {
    public void Finalize()
    {
    Console.WriteLine("Parent.IFinalize.Finalize");
    }
    }

    public class Son : Parent
    {
    ~Son(){ Console.WriteLine("Son.~Son");}
    }


    public interface IFinalize
    {
    void Finalize();
    }


    public class App
    {
    [STAThread]
    public static void Main()
    {
    Son s=new Son();
    s = null;

    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.ReadLine();
    }
    }
    }


    顺便说一下,我是在.NET Framework 1.1的环境下测试的,2.0还没有测试,因为据说2.0允许在子类中重新实现接口了。

    运行结果:

    未处理的“System.TypeLoadException”类型的异常出现在 未知模块 中。

    其他信息: 方法实现中引用的声明不能是 final 方法。类型:TestFinalize.Son,程序集:ConsoleApplication1, Version=1.0.1886.32955, Culture=neutral, PublicKeyToken=null。

    # re: C#中Finalize方法的问题 2005-3-2 12:09 Ivony

    将程序稍作改动,得到了另一个结果


    public void Finalize()

    改为:

    public virtual void Finalize() //强令子类能够覆盖



    结果:抛出异常。这一次是子类在重写方法的时候不能降低访问级别。。。。但奇怪的是,所有的这些编译时就该出现的问题都是到了程序的运行期抛出异常。。。。

    # re:C#中Finalize方法的问题 2005-4-10 20:28 实验室家具

    ^_^,Pretty Good!

    # re:C#中Finalize方法的问题 2005-4-16 17:17 拉力试验机

    ^_~

    # re:C#中Finalize方法的问题 2005-7-17 11:40 红外热像仪

    C#中Finalize方法的问题ooeess

    # re:C#中Finalize方法的问题 2005-8-1 18:07 红外热像仪

    C#中Finalize方法的问题ooeess

    # re: C#中Finalize方法的问题 2005-11-13 16:08 Farseer

    我想您误会了Anders 的意思了。在《C# Programming Language》的10.12 Destructors一节中Anders是这样说的
    Destructors are implemented by overriding the virtual method Finalize on System.Object. C# programs are not permitted to override this method or call it (or overrides of it) directly. For instance, the program
    class A
    {

    override protected void Finalize() {} // error

    public void F() {

    this.Finalize(); // error

    }

    }
    contains two errors.
    The compiler behaves as if this method, and overrides of it, do not exist at all。Thus, this program
    class A
    {
    void Finalize() {} // permitted

    }
    is valid, and the method shown hides System.Object's Finalize method.
    从前后文来看,Anders所指的as if the method中的method特指的是object的Finalize方法。而不是您自己所定义的Finalize方法,而Anders后面所举的例子也正如您说的,自己定义的Finalize方法,hide了System.Object's Finalize方法。

    # re: C#中Finalize方法的问题 2005-11-13 17:01 Farseer

    只是Anders没有明确说当hides ojbect这个Finalize方法的方法是个virtual的时候该怎么办。
    当没有virtual这个modified notation的时候,Parent 中的Finalize方法hide了obejct的Finalize方法,但还有析构语意,所以在GC.Collect()还会被调用。
    由于出现了一个new slot,Finalize在Parent又开辟了一个继承链体系,跟ojbect的Finalize方法已经没有任何关系了。Finalize方法在Parent中已经变成了没有析构语意的普通方法,当然~son也就是overidding的也是这个没有任何析构语意的Finalize方法,本身也就没有了析构语意。既然没有析构语意,当GC.Collect()的时候按照常理也不应该被调用的。

  • 相关阅读:
    微信浏览器取消缓存的方法
    iphone safari浏览器CSS兼容性的解决方案集合
    配置iis支持.json格式的文件
    win7下使用IIS服务器及自定义服务器端包含模块(SSI)步骤
    前端组件库集合
    ClientValidationFunction
    java 查询solr时间格式
    为何大量网站不能抓取?爬虫突破封禁的6种常见方法
    反爬虫四个基本策略
    ScheduledExecutorService 定时器用法
  • 原文地址:https://www.cnblogs.com/cxd4321/p/533265.html
Copyright © 2011-2022 走看看