CA1838: не используйте параметры StringBuilder для вызовов P/Invoke

Свойство Значение
Идентификатор правила CA1838
Заголовок Избегайте StringBuilder параметров для P/Invokes
Категория Производительность
Исправление является критическим или не критическим Не критическое
Включен по умолчанию в .NET 8 No

Причина

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.

В приведенных выше примерах используются двухбайтовые символы (CharSet.Unicode). Если собственная функция использует однобайтовые символы (CharSet.Ansi), то вместо буфера char можно использовать буфер byte. Например:

[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);
    }
}

Если параметр также используется в качестве входных данных, буферы должны быть заполнены строковыми данными с любым нуль-символом конца, добавленным явным образом.

Когда лучше отключить предупреждения

Запретить нарушение этого правила, если вы не обеспокоены воздействием на производительность маршала 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

Дополнительные сведения см. в разделе Практическое руководство. Скрытие предупреждений анализа кода.

См. также