Contents
  1. 1. 问题描述:Form.Show没有Dispose的警告
    1. 1.1. 第一次修改
    2. 1.2. 这个Coverity的警告是对的吗?
  2. 2. 再来看看Form.ShowDialog有没有什么不同
  3. 3. 试试GC能帮什么忙吧

在我的博文来试试这个来自静态代码分析工具PVS Studio提供C++的小测验吧提到过静态检查工具可以帮助我们找到很多的编程问题,但是在修改这些小问题时也要小心,弄清楚问题的来龙去脉,不能随便凭感觉改,否则会引入新的问题,最近就遇到这样的一个例子。

问题描述:Form.Show没有Dispose的警告

先看示例代码吧,很简单,就是在点击一个按钮时弹出一个Form。Coverity报错,说这个Form2创建之后在出作用域之前没有Dispose

1
2
3
4
5
private void button1_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.Show();
}

第一次修改

既然没有Dispose,那么标准Fix当然是使用Using。于是第一个修改就诞生了,如下所示:

1
2
3
4
5
6
7
private void button1_Click(object sender, EventArgs e)
{
using (Form2 f = new Form2())
{
f.Show();
}
}

嗯,Coverity不再报错了,可是一运行代码发现,不对啊,怎么这个对话框闪退呢……

原因很清晰了,打开了一个Form,但是紧接着就把它Dispose掉了,当然就关掉了。呵呵,想起了我曾经写过的博客谁动了我的timer?C#的垃圾回收和调试,是不是有点异曲同工:)

这个Coverity的警告是对的吗?

我觉得这个Coeverity警告是一个False Positive,因为Form调用完Show()之后用户关掉时会调用Dispose,具体可以参见MSDN的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void menuItemHelpAbout_Click(object sender, EventArgs e)
{
// Create and display a modless about dialog box.
AboutDialog about = new AboutDialog();
about.Show();

// Draw a blue square on the form.
/* NOTE: This is not a persistent object, it will no longer be
* visible after the next call to OnPaint. To make it persistent,
* override the OnPaint method and draw the square there */
Graphics g = about.CreateGraphics();
g.FillRectangle(Brushes.Blue, 10, 10, 50, 50);
}

再来看看Form.ShowDialog有没有什么不同

假如我们用的是模态对话框,那么代码是下面这样的:

1
2
3
4
5
private void button1_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.ShowDialog();
}

那估计Coverity还是会报一个警告,说创建完对象没有Dispose,那么这个警告是False Positive吗?继续翻MSDN吧,写着如下说明:

When a form is displayed as a modal dialog box, clicking the Close button (the button with an X at the upper-right corner of the form) causes the form to be hidden and the DialogResult property to be set to DialogResult.Cancel. Unlike non-modal forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property. Instead the form is hidden and can be shown again without creating a new instance of the dialog box. Because a form displayed as a dialog box is hidden instead of closed, you must call the Dispose method of the form when the form is no longer needed by your application.

MSDN上的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void ShowMyDialogBox()
{
Form2 testDialog = new Form2();

// Show testDialog as a modal dialog and determine if DialogResult = OK.
if (testDialog.ShowDialog(this) == DialogResult.OK)
{
// Read the contents of testDialog's TextBox.
this.txtResult.Text = testDialog.TextBox1.Text;
}
else
{
this.txtResult.Text = "Cancelled";
}
testDialog.Dispose();
}

所以如果用ShowDialog,就需要这样写了:

1
2
3
4
5
6
7
private void button1_Click(object sender, EventArgs e)
{
using (Form2 f = new Form2())
{
f.ShowDialog();
}
}

试试GC能帮什么忙吧

虽然ShowDialog()不能Dispose,但是因为这个Form是个局部变量,出了作用域应该就可以被回收了吧,我们试试看强制调用GC.Collect()会怎样。于是我加了个按钮,就是去强制垃圾回收,一切符合预期,这个Form确实被Dispose掉了。

那回过头来再试试Show(),假如我这样写:

1
2
3
4
5
6
private void button1_Click(object sender, EventArgs e)
{
Form2 f = new Form2();
f.Show();
f.Hide();
}

那么这个窗口还是会闪退,出了这个作用域,调用GC试试看吧。Oops!!!Dispose没有被调到,什么情况?这个Form究竟还被谁引用这呢?只好祭出windbg了,attach,然后敲!gcroot命令,得到了如下的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:005> !gcroot 02241e6c 
HandleTable:
000a13e8 (pinned handle)
-> 03205390 System.Object[]
-> 02227480 System.Collections.Generic.Dictionary`2[[System.Object, mscorlib],[System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]], mscorlib]]
-> 02227a30 System.Collections.Generic.Dictionary`2+Entry[[System.Object, mscorlib],[System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]], mscorlib]][]
-> 02228fb4 System.Collections.Generic.List`1[[Microsoft.Win32.SystemEvents+SystemEventInvokeInfo, System]]
-> 02228fdc System.Object[]
-> 02242354 Microsoft.Win32.SystemEvents+SystemEventInvokeInfo
-> 02242334 Microsoft.Win32.UserPreferenceChangedEventHandler
-> 02241e6c Form1.Form2

000a13ec (pinned handle)
-> 03203390 System.Object[]
-> 0222d7a4 System.Windows.Forms.FormCollection
-> 0222d7bc System.Collections.ArrayList
-> 0222d7d4 System.Object[]
-> 02241e6c Form1.Form2

Found 2 unique roots (run '!GCRoot -all' to see all roots).

这个UserPreferenceChangedEventHandler又是什么呢?它是一个静态的系统事件,具体参见StackOverflow上的这个回答。因为是静态的,所以这个Form还被引用,所以不能释放。

那问题又来了,为啥ShowDialog可以被垃圾回收呢?翻开C#的源代码,找到ShowDialog方法,可以看到最后如下的代码:

1
2
3
4
5
6
7
8
finally {
//...
if (IsHandleCreated) {
// ...
DestroyHandle();
}
//...
}

这个会调到ControlOnHandleDestroyed,然后会注销事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected virtual void OnHandleDestroyed(EventArgs e) {
//...
if (!RecreatingHandle) {
//...
ListenToUserPreferenceChanged(false /*listen*/);
}
}
private void ListenToUserPreferenceChanged(bool listen) {
if (GetState2(STATE2_LISTENINGTOUSERPREFERENCECHANGED)) {
if (!listen) {
SetState2(STATE2_LISTENINGTOUSERPREFERENCECHANGED, false);
SystemEvents.UserPreferenceChanged -= new UserPreferenceChangedEventHandler(this.UserPreferenceChanged);
}
}
else if (listen) {
SetState2(STATE2_LISTENINGTOUSERPREFERENCECHANGED, true);
SystemEvents.UserPreferenceChanged += new UserPreferenceChangedEventHandler(this.UserPreferenceChanged);
}
}

嗯,绕了一大圈,你搞明白Form.Show()Form.ShowDialog()的区别了吗?简单的说,就是调用Form.Show()不需要显示的Dispose,但是调用Form.ShowDialog()需要显示的Dispose

Contents
  1. 1. 问题描述:Form.Show没有Dispose的警告
    1. 1.1. 第一次修改
    2. 1.2. 这个Coverity的警告是对的吗?
  2. 2. 再来看看Form.ShowDialog有没有什么不同
  3. 3. 试试GC能帮什么忙吧