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

Mar*_*eIV 9 c# winapi user32

考虑这两个定义GetWindowText.一个使用a string作为缓冲区,另一个使用a 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);
Run Code Online (Sandbox Code Playgroud)

这是你怎么称呼他们:

var windowTextLength = GetWindowTextLength(hWnd);

// You can use either of these as they both work
var buffer = new string('\0', 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}'");
Run Code Online (Sandbox Code Playgroud)

无论我是通过a string还是a,它们似乎都能正常工作StringBuilder.但是,我见过的所有例子都使用了StringBuilder变体.甚至PInvoke.net也列出了那个.

我的猜测是"在C#字符串是不可变的,因此使用StringBuilder",但是因为我们正在寻找Win32 API并直接搞乱内存位置,并且该内存缓冲区用于所有意图和目的(预分配) (即保留用于字符串并且当前由字符串使用)由于其定义的值被赋予了一个值,该限制实际上并不适用,因此string工作得很好.但我想知道这个假设是否错误.

我不这么认为,因为如果你通过增加缓冲区来测试这个10,并改变你正在用'A'初始化它的字符,那么将更大的缓冲区大小传递给GetWindowText,你得到的字符串是实际的标题,右边填充了10个额外的"未被覆盖的A",表明它确实更新了早期角色的内存位置.

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

Luc*_*ski 2

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

当然,该函数可以对字符串中的数据执行任何它想要的操作,包括修改它。

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

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

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

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

更好地使用适合工作的正确工具。:)