Visual2000 · Архив статей А.Колесова & О.Павловой

Особенности работы со строковыми переменными в VB. Часть 1

Андрей Колесов

© 1999, Андрей Колесов
Авторский вариант. Статья была опубликована c незначительной литературной правкой в журнале "КомпьютерПресс" № 10/99, на компакт-диске.

VBP-проекты примеров, приведенных в данной статье, вы можете найти на Web-узле по адресу: www.visual.2000.ru/develop/vb/source/.


ВНИМАНИЕ! Эта статья была опубликована в журнале "КомпьютерПресс" 10'99 на прилагаемом к нему компакт-диске в файле VBSTRING.HTM. В разделах, связанных с национальными особенностями преобразования кодов символов, использовались символы из различных кодовых таблиц (Cyrillic, Western и Central European). К сожалению, при преобразованиях форматов файлов со статьей в окончательном варианте (VBSTRING.HTM в журнале) произошли искажения изображения символов кодовых таблиц cp1250 и cp1252. Во избежании подобных проблем в этой публикации (часть 1.2) фрагменты текста с использованием различных кодовых таблиц приведены в виде графических изображений.

Программный код многих приложений связан с обработкой строковых переменных. Visual Basic всегда отличался достаточно большим набором функций для выполнения таких операций, однако их эффективное применение требует от разработчика хорошего понимания внутреннего механизма их реализации. На этот аспект проблемы уже не раз обращалось внимание в наших "Советах для тех, кто программирует на VB" (например, см. Советы 11 и 135).

ПРИМЕЧАНИЕ. Список публикаций "Советы для тех, кто программирует на VB" можно найти в Архив статей А.Колесова & О.Павловой

Строки переменной и фиксированной длины

VB использует строковые переменные двух типов: с переменной длиной (до 2^31 байт) и с фиксированной длиной (до 2^16 байт):

Dim VariableLength$ ' строка переменной длины 
Dim VariableLength As String ' строка переменной длины 
Dim FixedLength As String*8 ' строка фиксированной длины (=8)

Обратите внимание, что тип строки переменной длины можно задать с помощью суффикса $. Так было принято еще в первых версиях Бейсик и, на наш взгляд, данное представление более удобно для работы. Но это, конечно, дело привычки и вкуса.

Принципиальным отличием строк двух этих типов является порядок формирования их содержимого.

ПРИМЕЧАНИЕ. Здесь нужно еще помнить об отличии формирования статических и динамических переменных. Первые формируются при компиляции кода, а вторые — в процессе выполнении программы. Но вопрос работы со статическими и динамическими данными не входит в тему настоящей статьи.

В момент создания строки переменной длины оператором Dim в программе резервируется только внутренний описатель переменной (в начальный момент длина строки равна нулю), а память под содержимое переменной динамически выделяется (и заполняется соответствующим значением) только в момент выполнения операции присвоения:

VariableLength$ = Строковое выражение или переменная

Область памяти для содержимого строки фиксированной длины резервируется однозначно при ее первом определении и заполняется в начальный момент нулевым цифровым кодом.

VB-программисты обычно пользуются строками переменной длины, так как это позволяет не задумываться о необходимой длине, а также экономить память (выделяется столько места, сколько действительно нужно). Однако при этом следует помнить, что операции с любыми динамическими переменными по определению требуют больше времени по сравнению со статическими, поэтому при работе со строковыми переменными данный тезис особенно актуален.

Ключевым положением является то, что в отличие от работы с данными фиксированной длины (практически всеми простыми типами данных, в том числе числовыми) самая длительная операция с динамическими строками — это самое простое присвоение: A$=..., так как в этом случае каждый раз идет обращение к внутреннему диспетчеру динамической памяти, который фактически формирует новую переменную. Например, сравнение таких двух операторов:

VariableLength = "Андрей"
FixedLength = "Андрей"

покажет, что второй выполняется примерно в 2,5 раза быстрее, чем первый. В этой связи можно сформулировать такой СОВЕТ: при работе со строками переменной длины избегайте операций присвоения. Подробный пример реализации такой рекомендации подробно рассмотрен в нашем Совете 135. Здесь же рассмотрим два варианта решения задачи со строкой длиной 255 символов, каждый байт которой равен его номеру в строке.

Вариант 1:

a$ = "": For n% = 1 To 255: a$ = a$ & Chr$(n%): Next

Вариант 2:

a$ = Space$(255): For n% = 1 To 255: Mid$(a$,n%) = Chr$(n%): Next

Второй вариант выглядит более длинным, но он будет работать пример в два раза быстрее. Причем если увеличивать длину формируемых байтов, то разница в скорости будет расти до десятков и даже сотен раз. Этот алгоритм можно использовать и для случая, когда длина строки до начала цикла ее формирования неизвестна — сначала резервируется строка заведомо большей длины, а после окончания цикла выделяется нужное количество символов оператором Left$.

В начало статьи

Передача строковых параметров в DLL

Следует сразу отметить, что передача строковых переменных в VB-процедуры (внутри данного проекта) и в DLL выполняется по-разному.

При работе со строками необходимо иметь в виду различия в их формировании в VB и других системах программирования (С, Pascal, Fortran и др.). В VB для каждой строковой переменной формируется ее описатель, в котором хранится адрес самой строки и ее длина. Этот описатель и передается в качестве параметра при передаче данных между процедурами. В С строка передается с помощью непосредственно ее адреса (указателя), а ее длина символов определяется по нулевому значению байта (код ASCII = 0) в конце сроки. Таким образом, в C нулевой код не может использоваться в качестве значимого символа внутри строки.

ПРИМЕЧАНИЕ. Язык C использует несколько иной механизм динамического выделения памяти для строковых переменных. За счет некоторой избыточности резервирования области памяти под переменную повышается скорость работы со строками при изменении их длины.

Обычные DLL, в том числе и Win API, используют модель языка C при работе со строковыми переменными. Поэтому для передачи строки в такую DLL необходимо использовать ключевое слово ByVal. В этом случае автоматически создается новая строка, в конце которой приписывается нулевой код. В саму библиотеку передается адрес новой строки LPSTR. (Следует также помнить о преобразовании из кода Unicode в ANSI, о чем было писано в Совете 203.)

Такие функции не могут напрямую создавать строковые переменные в формате VB, однако они способны выполнять преобразование переданных им строк, значение которых возвращается в вызывающую программу. Рассмотрим для примера API- функцию, которая определяет значение системного каталога Windows. Типичное обращение к ней выглядит следующим образом:

Declare Function GetSystemDirectory Lib "kernel32" Alias _ 
  "GetSystemDirectoryA" (ByVal lpBuffer As String, _ 
  ByVal nSize As Long) As Long 
  ' 
  ' Резервируется буфер, достаточный для 
  ' размещения полного имени каталога 
  BufferLength& = 256 
  Buffer$ = Space$(BufferLength) 
  ' Для обеспечения надежной работы API-функции 
  ' в качестве параметра передается длина буфера 
  ' 
  Result& = GetSystemDirectory(Buffer$, BufferLength&) 
  If Result& > BufferLength& Then 
    MsgBox "Ошибка! Недостаточная длина буфера!" 
  Else 
    DirName$ = Left$(Buffer$, Result&) 
  End if

В данном случае функция сама возвращает число загруженных символов имени каталога. Но можно было бы обойтись и без значения Result&, написав такой код:

LenName& = Instr(Buffer$, Chr(0)) 
If LenName& > 0 Then 
  DirName$ = Left$(Buffer$, LenName& - 1) 
End If

В приведенном примере Buffer$ используется только для возврата данных. Но возможен случай, когда переменная используется и для передачи, и для новой строки. Тогда можно предложить такой вариант:

BufferLength& = 256 
' буфер заполняется нулевым кодом 
Buffer$ = String$(BufferLength, 0) 
StartName$ = "Andrei" 
Mid$(Buffer$) = StartName$ 
Call SomeApiFuction(..., ByVal Buffer$, ...) 
Result& = Instr(Buffer$, Chr(0)) 
ResultName$ = Left$(Buffer$, Result& - 1)

Здесь резервируется строка длиной 256 символов, но с точки зрения функции SomeApiFuction ее начальное значение имеет только 6 знаков.

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

При обращении к DLL строковая переменная может передаваться и без использования ключа ByVal. Однако это возможно делать только тогда, когда данная библиотека реализована в варианте OLE 2.0. В этом случае туда передается указатель в формате VB (BSTR) и DLL-процедура работает с ним примерно так же, как и VB- процедура.

ВНИМАНИЕ. Традиционное использование ключевого слова ByVal означает одностороннюю передачу данных в вызываемую процедуру. Это происходит и при обращении к VB-процедурам (для переменных любого типа, в том числе и String), и при обращении к DLL-процедурам (для любых переменных, за исключением String). Соответственно при передаче строкой переменной в DLL в режиме ByVal ее значение в вызывающей программе может быть изменено.

В начало статьи

Обработка строк в VB

Для обработки строк в VB имеется довольно большой набор встроенных операторов и функций. Однако среди них можно выделить несколько базовых, с помощью которых получаются все остальные. Ключевыми функциями являются: операция конкатенации (слияния) строк, операции сравнения (равно, больше, меньше), Mid$ (оператор), Mid$ (функция), Asc и Chr$.

Вот примеры реализации некоторых встроенных функций:

Function MyLeft$ (MySting$, NumLeft%) 
  ' выделить NumLeft левых символов 
  MyString$ = Mid$(MyString$, 1, NumLeft%) 
End Function

Function MyLtRim$ (MySting$) 
  ' убрать левые пробелы 
  Dim NumS% 
  Do 
    NumS% = NumS% + 1 
  Loop While Mid$(MyString$, NumS%, 1) = " " 
  MyLtRim$ = Mid$(MyString$, NumS%, Len(MyString$) _
    - NumS% +1) 
End Function

Раньше, до версии VB 2.0, для операции конкатенации использовался только знак "+" (плюс). Однако в VB 3.0 стало можно также применять и знак "&" (амперсанд). Их принципиально отличие заключается в том, что "+" подразумевает наличие переменных и выражений только строкового типа, а "&" допускает любые типы (они автоматически преобразуются в строковые).

Например, оператор

Symbol1$ = Symbol2$ + 10

приведет к появлению ошибки (недопустимый тип операнда). А оператор

Symbol1$ = Symbol2$ & 10

выполнится без проблем. Хотя для конкатенации Microsoft рекомендует использовать второй вариант, отметим, что следующий эквивалентный код:

Symbol1$ = Symbol2$ + Str$(10)

не намного сложнее, но позволяет более точно следить за преобразованиями типов данных. С точки зрения быстродействия я не обнаружил сколь-нибудь значительных отличий в этих вариантов.

В начало статьи

Будьте осторожно с кодом 0

Ранее уже говорилось о том, что VB позволяет использовать любые значения кода байтов от 0 до 255. И тем не менее в ряде случаев он "плохо" реагирует на некоторые коды, в частности на 0, который он воспринимает как конец строки (чего он не должен делать). Например, напишите такой код с использование текстового поля:

Start$ = "Петя" + Chr$(0) + "Коля" 
Text1.Text = Start$ 
Result$ = Text1.Text 
Print Start$; Len(Start$) 
Print Text1.Text; Len(Text1.Text) 
If Text1.Text <> Finish$ Then Stop 'остановки не будет 
If Start$ <> Text.Text Then Stop ' здесь будет остановка

Вы получите такие результаты:

Петя Коля 9 Петя 4

Здесь видно, что текстовое поле восприняло код 0 как конец строки и обрезало ее. В переменную Finish$ было записано уже измененное значение строки. Получается, что в результате тривиальной операции присвоения происходит изменение строки.

В начало статьи

Использование байтовых строк

Переменные типа String традиционно называются "строковыми", при этом подразумевается, что в них содержится последовательность байтов с любым числовым значением от 0 до 255. Здесь следует еще раз подчеркнуть, что VB в отличие от C, позволяет использовать байт с кодом 0.

Использование строковых переменных для хранения и передачи данных произвольной структуры бывает очень удобно. Обычно для описания структуры данных применяется тип User Defined, например, так:

Type MyType 
  int1 As Integer 
  long1 As Long 
End Type 
Dim MyDate As MyType

Однако подобную структуру можно легко представить в виде строки:

MyString$ = Mki$(MyDate.int1) + Mkl$(MyDate.long1)

К сожалению, Microsoft не включила операторы Mkx$ в состав VB (они же были в ее Basic для DOS!), но их достаточно легко создать самому пользователю с помощью функций API (см. Совет 173). Мне сейчас не хотелось бы отвлекаться на обсуждение того, зачем нужно и когда полезно использовать строки вместо структур — это отдельная тема. Сошлюсь на собственный опыт. Такой технический прием обеспечил нам (когда мы еще писали "боевые" коммерческие программы) полную независимость программного кода от конкретной структуры данных: наши программы могли работать без перекомпиляции кода с любыми структурами таблиц и любыми конфигурациями визуального интерфейса.

Хотелось бы отметить, что для такого варианта (строка двоичных байтов) все операции сводились к слиянию или разделению отдельных поле строки, преобразованию их в переменных другого формата (например, Integer или та же String) и последующей обработке этих отдельных данных.

Еще в VB4 были также введены переменные типа Byte (беззнаковые целые числа 0-255). В этой связи нужно в первую очередь отметить, что скорость обработки (пересылка, арифметические и логические операции) целочисленных переменных различного типа (Byte, Integer и Long) практически одинакова в современных процессорах. Поэтому преимущество использования байтовых переменных проявляется в первую очередь при хранении информации, т.е. когда речь идет не о простых переменных, а о массивах.

ПРИМЕЧАНИЕ. Intel отказалась от блока оптимизации своих процессоров для 16-разрядных операций при переходе от Pentium к семейству P6. Во многом именно поэтому преимущества P6 (тогда еще Pentium Pro) были хорошо видны только для 32-разрядных приложений. Для моделей AMD K5 (о K6 мы не имеем такой информации) специальная оптимизация для 8-ми и 16-разрядных чисел существовала, а поэтомув этих режимах они имели порой заметное преимущество перед Pentium II. По нашим оценкам байтовые операции в VB6 (процессоры Pentium и Cyrix 200 МГц) выполняются даже немного медленнее, чем с 2-х и 4-х байтовыми целыми. Возможно, это происходит из-за того, что данный режим обработки на самом деле эмулируется чисто программным образом.

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

Рассмотрим такой, пример (Exam3.vbp): необходимо произвести инверсию всех байтов, хранящихся в переменной Source$. Тут можно предложить два варианта решения задачи:

' Source$ — исходная строка 
' 
' Вариант 1. Работа со строкой 
  For k% = 1 To LenB(Source$) 
    MidB$(Source$, k%, 1) = _
    ChrB$(Not AscB(MidB$(Source$, k%, 1))) 
  Next 
' 
' Вариант 2. Использование временного байтового массива 
  Dim arrByte() As Byte 
  arrByte = Source2$ 
  For k% = 0 To LenB(Source2$) - 1 
    arrByte(k%) = Not arrByte(k%) 
  Next 
  Source2$ = arrByte

По моим оценкам второй вариант (переменная с 60 байтами) работает в 5 раз быстрее, хотя и выглядит несколько громоздко (резервирование дополнительного массива, перезапись в него байтов, потом обратная перезапись данных в строку). Причина этого вполне понятна: копирование последовательности байтов выполняется очень быстро (одной машинной командой), но при работе с байтовым массивом не нужно использовать довольно длительные операции преобразования их строковой переменной в байтовую и обратно. (Каждая такая операция сопровождается созданием временных строковых переменных, но в данном случае это свидетельствует о слабой оптимизации таких часто встречающихся конструкций создателями VB.)

Здесь стоит также отметить, что я использовал очень простой вариант копирование из строки в массив и обратно. Однако это можно применять только для динамических байтовых массивов. Кроме того, нужно обратить внимание, что при операции:

arrByte = strSource$

всегда создается массив с нумерацией индекса от 0, даже если установлен режим Option Base 1. То есть всегда создается массив такой размерности:

ReDim arrByte(0 to LenB(strSource$) - 1 )

Отметим также, что использование строковых переменных для хранения двоичных байтов может быть гораздо эффективнее, чем применение массовов Byte. Например, массив srtArray$(N) фактически представляет собой уникальную конструкцию, позволяющую выполнять прямой доступ к записям переменной длины.

В начало статьи

Особенности работы с символьными данными

Однако основное применение строк приходится на работу с символьными данными — алфавитно-цифровым представлением при обмене информацией между человеком и компьютером. Более того, многие строковые функции ориентированы именно на обработку символьных данных. Например, LTrim$ убирает слева только крайние пробелы (код ASCII = 32). В то же время ряд функций поиска и сравнения могут работать со строками в двух режимах: двоичной обработки (в этом случае, например, будет Z < z) и символьной (Z = z).

Ключевая проблема заключается в том, что, с одной стороны, некоторые двоичные коды не воспринимаются как символы (например, тот же 0) или используются некоторым зафиксированным образом (коды управления печатью, например перевод строки — 13), с другой стороны, — 255 символов (на самом деле меньше) просто не хватает для всего многообразия символов народов мира.

Радикальным решением данного вопроса является переход от традиционной для MS DOS/Windows системы кодирования символов ANSI/DBCS к универсальной Unicode (см. часть 1.3 статьи, "От ANSI/DBCS к Unicode"). Поскольку DBCS используется только для азиатских стран, далее будем говорить только об ANSI и Unicode.)

Один из аспектов этой проблемы для VB-программистов заключается в том, что все 32-разрядные версии VB используют для внутреннего хранения Unicode, т.е. один символ занимает два байта, а не один, как ранее в 16-разрядных системах. Следовательно, понятия "байтовая строка" и "символьная строка" перестали быть синонимами. В этой связи мы обратим внимание на следующие моменты:

  1. Ранее любая двоичная байтовая строка могла быть интерпретирована как некий набор ASCII-кодов (значения 0-255 были значимыми), то теперь такая интерпретация может вызвать ошибку выполнения (незначимый код). В частности, VB (и многие другие приложения) при выводе символьной информации автоматически обозначают несуществующие значения знаком вопроса (?). Проблема заключается в том, что многие американские приложения не признают других наборов символов, кроме родного cp1252, и поэтому считают символы кириллицы также ошибочными. (Наверное, многие читатели сталкивались с получением электронных писем, состоящих из одних вопросительных знаков.)

  2. Многие привычные строковые функции на самом деле теперь работают только с символьными данными: Asc, Chr, Input, InStr, Left, Right, Len, Mid. Для работы с байтами нужно использовать модификации этих функций, в которых к тем же именам в конце добавлена латинская буква B: AscB, ChrB и т.д.

  3. В соответствии с принятыми в VB правилами младший байт в целочисленном коде является первым по порядку, а старший — вторым.

Проиллюстрирую сказанное на таких примерах:

Str1$ = "Петя" 
Print Len(Str1$); LenB(Str2$) 'напечатано: 4 8 
For i = 1 to Len(Str1$) 
  Print Hex$(Asc(Mid(Str1$, i, 1))), _
    Hex$(AscW(Mid(Str1$, i, 2)) 
  ' Будет напечатана таблица 
  ' с ANSI и Uni-кодами (шестнадцитиричными) 
  ' символьной строки: 
  ' CF 41F 
  ' E2 435 
  ' F2 442 
  ' FF 44F 
Next 
' 
For i = 1 to LenB(Str1$) 
Print Hex$(AscB(MidB(Str1$, i, 1))); 
  ' Будет напечатана последовательность 
  ' значений двоичных байтов: 
  ' 1F 4 35 4 42 4 4F 4 
Next
'
Str2$ = Str1$ + Chr$(43) + "Лена" 
Print Str2$ ' будет напечатано: Петя+Лена
'
Str2$ = Str1$ + ChrB$(43) + "Лена" 
Print Str2$ ' напечатано: Петя???? 
Print Len(Str2$); LenB(Str2$) ' напечатано: 8 17

В последнем случае я искусственно включил между двумя символьными строками один двоичный байт. В результате произошло смещение байтов (четные и нечетные поменялись местами) и коды символов для второй части строки стали незначимыми (их при выводе заменили вопросительные знаки "?"). Кроме того, число байтов в строке стало нечетным (17), что невозможно для символьной строки. Если вы работаете со строками, используя только символьные функции, то все сказанное здесь не имеет значения. Но если вы применяете двоичные операции (например, для повышения быстродействия — это будет показано далее), то все эти нюансы нужно иметь в виду.

В начало статьи

Выводы и советы

  1. Использование оптимальных конструкций при работе строковых данных позволяет порой увеличить скорость обработки в десятки раз. При этом внешне простые констукции порой работают медленнее, чем более сложные и вроде бы, громоздские.

  2. Если вам критична производительность — не поленитесь проветсти специальные тестовые испытания разных вариантов программных конструкций.

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

  4. При обработке символьных данных нужно учитывать национальные особенности алгоритмов (об этом см. Часть 1.2) и влияние тех или иных системных установок. Помните, что это относится не только к внутренним переменным, но и к идентификаторам, передаваемым, например, через OLE Automatiom. Поэтому рекомендую использовать для имен объектов, их свойств и методов, только английские символы.

В начало статьи