对于 C#,在调用 GetWindowText 等 Win32 函数时是否存在 down-side 以使用 'string' 而不是 'StringBuilder'?

For C#, is there a down-side to using 'string' instead of 'StringBuilder' when calling Win32 functions such as GetWindowText?

考虑 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('[=14=]', 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 也列出了那个。

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

我不这么认为,因为如果你通过将缓冲区增加 10 来测试它,并将你初始化它的字符更改为 'A',然后将更大的缓冲区大小传递给GetWindowText,你得到的字符串是实际的标题,right-padded 加上十个额外的 'A' 没有被覆盖,表明它确实更新了前面字符的内存位置。

所以如果你 pre-initialize 字符串,你不能这样做吗?这些字符串在使用时是否会 'move out from under you' 因为 CLR 假设它们是不可变的?这就是我想要弄清楚的。

First offpre-allocated 是当前的误导词 context.The string is nothing different than just another 。 Net 不可变字符串,与现实生活中的休·杰克曼一样不可变。我相信 OP 已经知道了。

事实上:

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

完全相同:

// assuming windowTextLength is 5
var buffer = "[=11=][=11=][=11=][=11=][=11=]"; 

为什么 我们不应该使用 String/string 而是使用 StringBuilder 将被调用者可修改的参数传递给 Interop/Unmanaged 代码?是否存在它会失败的特定情况?

老实说,我发现这是一个有趣的问题并测试了一些场景,方法是编写一个接受字符串和 StringBuilder 的自定义本机 DLL,同时我强制垃圾收集,通过强制 GC不同的线程等等。我的意图是在对象的地址通过 PInvoke 传递到外部库时强制重定位该对象。在所有情况下,即使其他对象重新定位,对象的地址也保持不变。在研究中,我发现了 Jeffrey 本人:The Managed Heap and Garbage Collection in the CLR

When you use the CLR’s P/Invoke mechanism to call a method, the CLR pins the arguments for you automatically and unpins them when the native method returns.

所以要点是我们可以使用它,因为它似乎有效。但是我们应该吗?我相信没有:

  1. 因为它在文档中明确提到,Fixed-Length String Buffers。所以 string 目前有效,在未来的版本中可能无效。
  2. 因为 StringBuilder 是库提供的可变类型,逻辑上允许修改可变类型比不可变类型更有意义 (string)。
  3. 有一个微妙的优势。使用 StringBuilder 时,我们会预先分配 容量 ,而不是字符串。这样做的目的是,我们摆脱了 trim/sanitize 字符串的额外步骤,也不必担心终止空字符。

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

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

这意味着该函数将毫无问题地覆盖前几个字符,但 buffer.Length 将保持不变,您最终会发现字符串末尾的现有数据仍然存在于字符串中。 .NET 字符串将它们的长度存储在一个字段中。它们也是以 null 终止的,但 null 终止符只是为了方便与 C 代码的互操作而使用,在托管代码中没有任何作用。

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

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

另一方面,如果您通过 P/Invoke 传递 StringBuilder,则您明确告诉封送拆收器该函数预期 写入实例,当您对其调用 ToString() 时,它 根据空终止字符更新长度,并且一切都完全同步。

最好使用正确的工具来完成工作。 :)