对于 C#,在调用 Win32 函数(如 GetWindowText)时,使用 'string' 而不是 'StringBuilder' 是否有缺点?



考虑GetWindowText的这两个定义。一个使用string作为缓冲区,另一个则使用StringBuilder

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hWnd, string lpString, int nMaxCount);

以下是您对它们的称呼:

var windowTextLength = GetWindowTextLength(hWnd);
// You can use either of these as they both work
var buffer = new string('', windowTextLength);
//var buffer = new StringBuilder(windowTextLength);
// Add 1 to windowTextLength for the trailing null character
var readSize = GetWindowText(hWnd, buffer, windowTextLength + 1);
Console.WriteLine($"The title is '{buffer}'");

无论我传入string还是StringBuilder,它们似乎都能正常工作。然而,我看到的所有示例都使用StringBuilder变体。甚至PInvoke.net也列出了这一点。

我的猜测是,"在C#中,字符串是不可变的,因此使用StringBuilder",但由于我们深入到Win32 API并直接扰乱内存位置,并且内存缓冲区是根据其定义被分配值的性质为所有意图和目的(预)分配的(即,为字符串保留,当前由字符串使用),这个限制实际上并不适用,因此string工作得很好。但我想知道这种假设是否是错误的。

我不这么认为,因为如果你通过将缓冲区增加10来测试这一点,并将初始化时使用的字符更改为"A",然后将较大的缓冲区大小传递给GetWindowText,你得到的字符串就是实际标题,用十个没有被覆盖的额外"A"填充,表明它确实更新了早期字符的内存位置。

所以,如果你预先初始化了字符串,你就不能这样做吗?在使用这些字符串时,会不会因为CLR假设它们是不可变的而"从你的下面移出"?这就是我想弄清楚的。

首先,预分配在当前上下文中是一个误导性的词。这个字符串和另一个.Net不可变的字符串没有什么不同,它和现实生活中的休·杰克曼一样不可变。我相信OP已经知道了。

事实上:

// You can use either of these as they both work
var buffer = new string('', windowTextLength);

与完全相同

// assuming windowTextLength is 5
var buffer = ""; 

为什么我们不应该使用String/string,而使用StringBuilder向互操作/非托管代码传递被调用方可修改的参数?是否存在它将失败的特定情况?

老实说,我发现这是一个有趣的问题,并测试了一些场景,通过编写一个接受字符串和StringBuilder的自定义Native DLL,同时强制垃圾回收,在不同的线程中强制GC等等。我的意图是在通过PInvoke将对象的地址传递到外部库时强制重新定位对象。在所有情况下,即使其他对象重新定位,对象的地址也保持不变。在Jeffrey自己的研究中,我发现了这一点:CLR 中的托管堆和垃圾收集

当您使用CLR的p/Invoke机制来调用方法时,CLR当本机方法返回。

因此,我们可以使用它,因为它似乎有效。但我们应该这样做吗?我相信

  1. 因为文档中明确提到了固定长度字符串缓冲区。所以string目前有效,可能在未来的版本中不起作用
  2. 因为StringBuilder是库提供的可变类型,从逻辑上讲,允许修改可变类型比修改不可变类型更有意义(string)
  3. 这有一个微妙的优势。当使用StringBuilder时,我们预分配容量,而不是字符串。这样做的目的是,我们省去了修剪/清理字符串的额外步骤,也不用担心终止null字符

如果使用p/Invoke将string传递给函数,CLR将假定该函数将读取字符串。为了提高效率,字符串被固定在内存中,指向第一个字符的指针被传递给函数。不需要以这种方式复制任何字符数据。

当然,函数可以对字符串中的数据做任何它想做的事情,包括修改它

这意味着函数将毫无问题地覆盖前几个字符,但buffer.Length将保持不变,并且字符串末尾的现有数据仍存在于字符串中.NET字符串将其长度存储在字段中。它们也以null终止,但null终止符仅用于方便与C代码的互操作性,在托管代码中没有任何作用

使用这样的字符串并不方便,因为除非您预定义字符串的大小,以完全匹配以null结尾的字符最终写入的位置,否则.NET的长度字段将与底层数据不同步。

此外,这种方式更好,因为更改字符串的长度肯定会损坏CLR堆(GC将无法遍历对象)。字符串和数组是仅有的两种没有固定大小的对象类型。

另一方面,如果通过p/Invoke传递StringBuilder,则显式地告诉封送拆收器该函数应向实例写入,并且当对其调用ToString()时,它会根据null终止字符更新长度,并且所有内容都完全同步。

最好使用合适的工具来完成工作。:)

最新更新