CA1838:不要对 P/Invoke 使用 StringBuilder
参数
属性 | 值 |
---|---|
规则 ID | CA1838 |
标题 | 避免将 StringBuilder 参数用于 P/Invoke |
类别 | “性能” |
修复是中断修复还是非中断修复 | 非中断 |
在 .NET 9 中默认启用 | 否 |
原因
P/Invoke 具有一个 StringBuilder 参数。
规则说明
StringBuilder
的封送处理总是会创建一个本机缓冲区副本,这导致一个 P/Invoke 调用出现多次分配。 若要将 StringBuilder
作为 P/Invoke 参数进行封送,运行时将:
- 分配本机缓冲区。
- 如果是
In
参数,请将StringBuilder
的内容复制到本机缓冲区。 - 如果是
Out
参数,请将本机缓冲区复制到新分配的托管数组中。
默认情况下,StringBuilder
为 In
和 Out
。
若要详细了解如何封送字符串,请参阅字符串的默认封送。
此规则在默认情况下为禁用状态,因为它可能需要根据具体情况分析冲突是否值得关注,以及是否可能需要进行重大重构来解决冲突。 用户可通过配置其严重性来显式启用此规则。
如何解决冲突
通常情况下,解决冲突涉及到重新处理 P/Invoke 及其调用方以使用缓冲区而不是 StringBuilder
。 具体情况取决于 P/Invoke 的用例。
下面是使用 StringBuilder
作为要由本机函数填充的输出缓冲区的常见方案示例:
// Violation
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern void Foo(StringBuilder sb, ref int length);
public void Bar()
{
int BufferSize = ...
StringBuilder sb = new StringBuilder(BufferSize);
int len = sb.Capacity;
Foo(sb, ref len);
string result = sb.ToString();
}
对于缓冲区较小且可接受 unsafe
代码的用例,可以使用 stackalloc 在堆栈上分配缓冲区:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern unsafe void Foo(char* buffer, ref int length);
public void Bar()
{
int BufferSize = ...
unsafe
{
char* buffer = stackalloc char[BufferSize];
int len = BufferSize;
Foo(buffer, ref len);
string result = new string(buffer);
}
}
对于大型缓冲区,可以分配新数组作为缓冲区:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern void Foo([Out] char[] buffer, ref int length);
public void Bar()
{
int BufferSize = ...
char[] buffer = new char[BufferSize];
int len = buffer.Length;
Foo(buffer, ref len);
string result = new string(buffer);
}
如果频繁调用 P/Invoke 以获取大型缓冲区,可使用 ArrayPool<T> 避免随之而来的重复分配和内存压力:
[DllImport("MyLibrary", CharSet = CharSet.Unicode)]
private static extern unsafe void Foo([Out] char[] buffer, ref int length);
public void Bar()
{
int BufferSize = ...
char[] buffer = ArrayPool<char>.Shared.Rent(BufferSize);
try
{
int len = buffer.Length;
Foo(buffer, ref len);
string result = new string(buffer);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
如果缓冲区大小在运行时之前是未知的,则可能需要根据大小以不同的方式创建缓冲区,以避免使用 stackalloc
分配大型缓冲区。
前面的示例使用 2 个字节宽的字符 (CharSet.Unicode
)。 如果本机函数使用单字节字符 (CharSet.Ansi
),可使用 byte
缓冲区而不是 char
缓冲区。 例如:
[DllImport("MyLibrary", CharSet = CharSet.Ansi)]
private static extern unsafe void Foo(byte* buffer, ref int length);
public void Bar()
{
int BufferSize = ...
unsafe
{
byte* buffer = stackalloc byte[BufferSize];
int len = BufferSize;
Foo(buffer, ref len);
string result = Marshal.PtrToStringAnsi((IntPtr)buffer);
}
}
如果参数还用作输入,则需要使用显示添加了任何 NULL 终止符的字符串数据来填充缓冲区。
何时禁止显示警告
如果你不关心封送 StringBuilder
造成的性能影响,可禁止显示此规则的冲突警告。
抑制警告
如果只想抑制单个冲突,请将预处理器指令添加到源文件以禁用该规则,然后重新启用该规则。
#pragma warning disable CA1838
// The code that's violating the rule is on this line.
#pragma warning restore CA1838
若要对文件、文件夹或项目禁用该规则,请在配置文件中将其严重性设置为 none
。
[*.{cs,vb}]
dotnet_diagnostic.CA1838.severity = none
有关详细信息,请参阅如何禁止显示代码分析警告。