CA1838:不要对 P/Invoke 使用 StringBuilder 参数

属性
规则 ID CA1838
标题 避免将 StringBuilder 参数用于 P/Invoke
类别 “性能”
修复是中断修复还是非中断修复 非中断
在 .NET 8 中默认启用

原因

P/Invoke 具有一个 StringBuilder 参数。

规则说明

StringBuilder 的封送处理总是会创建一个本机缓冲区副本,这导致一个 P/Invoke 调用出现多次分配。 若要将 StringBuilder 作为 P/Invoke 参数进行封送,运行时将:

  • 分配本机缓冲区。
  • 如果是 In 参数,请将 StringBuilder 的内容复制到本机缓冲区。
  • 如果是 Out 参数,请将本机缓冲区复制到新分配的托管数组中。

默认情况下,StringBuilderInOut

若要详细了解如何封送字符串,请参阅字符串的默认封送

此规则在默认情况下为禁用状态,因为它可能需要根据具体情况分析冲突是否值得关注,以及是否可能需要进行重大重构来解决冲突。 用户可通过配置其严重性来显式启用此规则。

如何解决冲突

通常情况下,解决冲突涉及到重新处理 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

有关详细信息,请参阅如何禁止显示代码分析警告

另请参阅