Для C #, есть ли обратная сторона использования 'string' вместо 'StringBuilder' при вызове функций Win32, таких как GetWindowText? - PullRequest
0 голосов
/ 28 августа 2018

Рассмотрим эти два определения для 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, строку вы получите реальный заголовок, дополненный десятью дополнительными буквами «А», которые не были перезаписаны, показывая, что он обновил место в памяти предыдущих символов.

Итак, если вы предварительно инициализировали строки, вы не можете это сделать? Могут ли эти строки когда-либо «выйти из-под вас» во время их использования, потому что CLR предполагает, что они неизменны? Вот что я пытаюсь выяснить.

Ответы [ 2 ]

0 голосов
/ 28 августа 2018

Если вы передадите string в функцию с помощью P / Invoke, CLR будет считать, что функция прочитает строку. Для эффективности строка закрепляется в памяти, и указатель на первый символ передается функции. Никакие символьные данные не должны копироваться таким образом.

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

Это означает, что функция будет перезаписывать первые несколько символов без проблем, но buffer.Length останется неизменным, и вы получите существующие данные в конце строки, все еще присутствующей в строке. .NET строки хранят свою длину в поле. Они также заканчиваются нулем, но нулевой терминатор используется только для удобства взаимодействия с кодом C и не влияет на управляемый код.

Использование такой строки было бы неудобно, поскольку, если вы предварительно не определили размер строки, чтобы она идеально соответствовала тому, где символ с нулем в конце будет в конечном итоге записан, поле длины .NET будет не синхронизировано с базовыми данными.

Кроме того, так будет лучше, поскольку изменение длины строки наверняка повредит кучу CLR (GC не сможет обходить объекты). Строки и массивы - это только два типа объектов, которые не имеют фиксированного размера.

С другой стороны, если вы передаете StringBuilder через P / Invoke, вы явно указываете маршалеру, что функция должна записать в экземпляр и при вызове ToString() на нем обновляет длину на основе символа нулевого завершения, и все идеально синхронизируется.

Лучше использовать правильный инструмент для работы. :)

0 голосов
/ 28 августа 2018

Прежде всего , предварительно выделено является вводящим в заблуждение словом в текущем контексте. Строка ничем не отличается от просто другой неизменяемой строки .Net, и такая же неизменная, как Хью Джекман в реальная жизнь. Я полагаю, что ОП уже знает это.

На самом деле:

// 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 для передачи изменяемых аргументов вызываемого в Interop / Неуправляемый код? Существуют ли конкретные сценарии, в которых он потерпит неудачу?

Честно говоря, я нашел этот интересный вопрос и протестировал несколько сценариев, написав собственную Native DLL, которая принимает строку и StringBuilder, в то время как я принудительно собираю мусор, форсируя GC в другом потоке и так далее. Мое намерение состояло в том, чтобы принудительно переместить объект, пока его адрес передавался во внешнюю библиотеку через PInvoke. Во всех случаях адрес объекта оставался неизменным даже при перемещении других объектов. При исследовании я обнаружил это самим Джеффри: Управляемая куча и сбор мусора в CLR

Когда вы используете механизм P / Invoke в CLR для вызова метода, CLR автоматически добавляет аргументы для вас и отключает их, когда возвращается нативный метод.

Так что на вынос мы можем использовать его, потому что, похоже, он работает. Но мы должны? Я верю Нет :

  1. Поскольку это четко указано в документации, Строковые буферы фиксированной длины . Так что string работает на данный момент, может не работать в будущих версиях.
  2. Поскольку StringBuilder является предоставляемым библиотекой изменяемым типом, и логически имеет смысл разрешить изменение изменяемого типа по сравнению с неизменяемым типом (string).
  3. Есть тонкое преимущество. При использовании StringBuilder мы предварительно выделяем емкость , а не строку. Это избавляет нас от дополнительных шагов по обрезке / дезинфекции строки, а также не беспокоит насчет завершения нулевого символа.
...