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
Дополнительные сведения см. в разделе Практическое руководство. Скрытие предупреждений анализа кода.