对于 C#,在调用 Win32 函数(如 GetWindowText)时使用“字符串”而不是“字符串生成器”是不是有不利之处?

Posted

技术标签:

【中文标题】对于 C#,在调用 Win32 函数(如 GetWindowText)时使用“字符串”而不是“字符串生成器”是不是有不利之处?【英文标题】:For C#, is there a down-side to using 'string' instead of 'StringBuilder' when calling Win32 functions such as GetWindowText?对于 C#,在调用 Win32 函数(如 GetWindowText)时使用“字符串”而不是“字符串生成器”是否有不利之处? 【发布时间】:2019-02-02 13:00:00 【问题描述】:

考虑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('\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'");

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

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

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

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

【问题讨论】:

这可以写成答案,但它包含您需要的所有信息limbioliong.wordpress.com/2011/11/01/… 我不确定注释“[...]托管字符串类型不可用。托管字符串不能预先分配其内部缓冲区,然后传递给非托管代码以进行填充。”因为我可以清楚地证明这种说法是错误的。你也可以。将缓冲区的大小增加到比需要的大 10 倍。将字符从 null ('\0') 更改为可见的内容,例如 'A'。将这个现在更大的缓冲区的大小传递给对“GetWindowText”的调用。你得到的字符串是你的字符串(即所有的'A'),第一部分被 Win32 API 调用覆盖,因此证明是错误的。 嗯,它预分配它,是的,它有点误导,这就是为什么我们不从 wordpress 页面学习编码 不完全是因为字符串应该是不可变的在 .NET 运行时。隐式使用 P/Invoke 调用的本质意味着您正在窥视它的基础并绕过这些保护措施。从技术上讲,以您必须的方式使用 StringBuilder 是在做同样的事情。你不是在“构建”一个字符串。您正在覆盖预分配的空间,这也违反了 .NET。我确实明白你在说什么,但我再次反驳,如果你在做 P/Invoke,你应该知道你不再在 .NET 领域了。 CLR 优化假设字符串是不可变的。例如,如果您对用作字典键的字符串进行变异,则字典将停止正常工作。 【参考方案1】:

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

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

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

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

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

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

最好为工作使用正确的工具。 :)

【讨论】:

“您明确告诉封送器该函数应写入实例” - 正确,但行为与 string 相同,以防您的被调用者覆盖超过分配的容量。测试过了! @subdeveloper 当然,您仍然需要遵守合同 - 这里声明您正在为函数提供固定长度的缓冲区。 卢卡斯,这是 非常 重要的信息(关于空终止字符和单独的长度值),如果我事先知道的话,我会在我之前回答这个问题发布它。这就是为什么 StringBuilder 是要走的路。但是,我看到你有 41k 的代表,但 SubDeveloper 只有 220,我印象深刻的是他实际上构建了一个本地 DLL,所以我有点想分分,但既然你不能这样做,请介意我给给他?试图在这里做正确的事,但同样,这是主要原因。 其实再想一想,他真的没有关于长度变量的关键部分,所以我真的不得不把这个给你。这就是不使用string 的决定性原因。干得好,伙计! @MarqueIV 您考虑研究和代表是非常周到的,但您通过标记正确答案做了正确的事情。我们都在这里学习和成长,昨天我确实从你的问题中学到了一些新东西,显然是从卢卡斯的回答中学到的。【参考方案2】:

首先pre-allocated 在当前上下文中是一个误导性的词。该字符串与另一个 .Net 不可变字符串没有什么不同,并且与 Hugh 一样不可变现实生活中的杰克曼。我相信 OP 已经知道这一点。

事实上:

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

一模一样

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

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

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

当您使用 CLR 的 P/Invoke 机制调用方法时,CLR 自动为您固定参数,并在 本机方法返回。

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

    因为它在文档中明确提到,Fixed-Length String Buffers。所以string 现在可以使用,在以后的版本中可能无法使用。 因为StringBuilder 是库提供的可变类型,从逻辑上讲,允许修改可变类型而不是不可变类型 (string) 更有意义。 有一个微妙的优势。使用StringBuilder 时,我们预先分配了容量,而不是字符串。这样做的目的是,我们摆脱了修剪/清理字符串的额外步骤,也无需担心终止空字符。

【讨论】:

"\0\0\0\0\0"new string('\0', 5) 完全不同。前者被拘留,后者不是。修改一个实习字符串是一个令人头疼的秘诀。 Lucas - 我的意思是从不变性的角度来看。虽然感谢您指定,但这将为读者增加价值。 是的,它不是预先分配的,它是完全分配和使用的。但是 Win32 API 并不关心它是否被使用,因为它无论如何都会覆盖它,所以与 Win32 的区别相同。尽管如此,正如卢卡斯指出的那样,真正的问题是托管代码不使用(甚至不关心)空终止字符来确定长度。它使用 Win32 API 更新的变量,因此虽然最终正确写入了底层内存,除非您在 p/invoking 之前定义了一个具有您需要的确切大小的字符串,否则您最终会长度变量在托管端不同步。

以上是关于对于 C#,在调用 Win32 函数(如 GetWindowText)时使用“字符串”而不是“字符串生成器”是不是有不利之处?的主要内容,如果未能解决你的问题,请参考以下文章

C#调用Win32 api时的内存操作

c# 调用 win32 API的 SendMessage 函数 ,里面的属性用法?

C# 通过调用Win32 API函数清除浏览器缓存和cookie

C#调用Win32 API 的方法

C#可以直接调用的Win32API(和VCL做的整理工作非常类似)

c# 调用windows API(user32.dll)