番茄的梦想

那一抹夕阳

  博客园  :: 首页  ::  :: 联系 :: 订阅 订阅  :: 管理

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()的时候按照常理也不应该被调用的。

posted on 2006-10-19 10:13  番茄的梦想  阅读(6233)  评论(0编辑  收藏  举报