您的问题的答案是:unsafe 关键字并不表示“不安全”,而是表示“可能不安全”。编译器和框架无法确保它是安全的。您可以确保代码不能对内存执行不安全的读取或写入操作。
我强烈建议您遵循您链接的文章中给出的建议:
1) 重新设计应用程序以具有更少的容器并减少嵌套级别的数量。
如果您将容器仅用于控制排列,请编写您自己的容器,它可以在一个级别完成所有排列。
更新
您可以修改该文章中的代码,使其不使用指针(即不需要 unsafe 关键字)。请记住,这现在需要编组,这意味着额外的复制。这可能是件好事,因为原始代码将 WINDOWPOS 指针从操作系统传递给 BeginInvoke,该指针在操作系统生成指针的同一调度事件期间不会执行。换句话说,该代码已经很臭了。
internal class MyTabPage : TabPage
{
private const int WM_WINDOWPOSCHANGING = 70;
private const int WM_SETREDRAW = 0xB;
private const int SWP_NOACTIVATE = 0x0010;
private const int SWP_NOZORDER = 0x0004;
private const int SWP_NOSIZE = 0x0001;
private const int SWP_NOMOVE = 0x0002;
[DllImport("User32.dll", CharSet = CharSet.Auto)]
extern static int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);
[DllImport("User32.dll", ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
extern static bool SetWindowPos(HandleRef hWnd, HandleRef hWndInsertAfter,
int x, int y, int cx, int cy, int flags);
[StructLayout(LayoutKind.Sequential)]
private class WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public int flags;
};
private delegate void ResizeChildDelegate(WINDOWPOS wpos);
private void ResizeChild(WINDOWPOS wpos)
{
// verify if it's the right instance of MyPanel if needed
if ((this.Controls.Count == 1) && (this.Controls[0] is Panel))
{
Panel child = this.Controls[0] as Panel;
// stop window redraw to avoid flicker
SendMessage(new HandleRef(child, child.Handle), WM_SETREDRAW, 0, 0);
// start a new stack of SetWindowPos calls
SetWindowPos(new HandleRef(child, child.Handle), new HandleRef(null, IntPtr.Zero),
0, 0, wpos.cx, wpos.cy, SWP_NOACTIVATE | SWP_NOZORDER);
// turn window repainting back on
SendMessage(new HandleRef(child, child.Handle), WM_SETREDRAW, 1, 0);
// send repaint message to this control and its children
this.Invalidate(true);
}
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_WINDOWPOSCHANGING)
{
WINDOWPOS wpos = new WINDOWPOS();
Marshal.PtrToStructure(m.LParam, wpos);
Debug.WriteLine("WM_WINDOWPOSCHANGING received by " + this.Name + " flags " + wpos.flags);
if (((wpos.flags & (SWP_NOZORDER | SWP_NOACTIVATE)) == (SWP_NOZORDER | SWP_NOACTIVATE)) &&
((wpos.flags & ~(SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)) == 0))
{
if ((wpos.cx != this.Width) || (wpos.cy != this.Height))
{
BeginInvoke(new ResizeChildDelegate(ResizeChild), wpos);
return;
}
}
}
base.WndProc(ref m);
}
}
注意:WINDOWPOS 从值类型到引用类型的变化是有意的。使用引用类型将副本数减少到只有一个(初始元帅)(**)。
再次更新
我刚刚注意到代码最初公开了 p/invoke 声明。永远不要在类(*)之外公开 p/invoke。如果您的意图是公开提供的功能,请编写调用私有 p/invoke 声明的托管方法;在这种情况下不正确,p/invoke 是严格内部的。
(*) 好的,有一个例外。您正在创建 NativeMethods、UnsafeNativeMethods 等。这是 FxCop 推荐的执行 p/invoke 的方式。
更新
(**) 我被要求(在其他地方)准确地描述为什么在这里使用引用类型更好,所以我在此处添加了该信息。我被问到的问题是,“这不会增加内存压力吗?”
如果WINDOWPOS 是一个值类型,这将是事件序列:
1) 从非托管内存复制到托管内存
WINDOWPOS wpos = Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));
2) 第二份?
BeginInvoke(new ResizeChildDelegate(ResizeChild), wpos);
等等! BeginInvoke 的签名是(Delegate, params object[])。这意味着 wpos 将被装箱。所以是的,这里出现了第二个副本:装箱操作。
BeginInvoke 会将委托和对象[] 添加到调用列表并发布已注册的窗口消息。当消息泵从队列中删除该消息时,将使用 object[] 参数调用委托。
3) 为ResizeChild 调用拆箱并复制。
此时您可以看到,份数甚至都不是问题。它被转换为引用类型(装箱)这一事实意味着我们最好将其作为引用类型开始。