Сравнение строк в .NET
Платформа .NET обеспечивает несколько методов для сравнения значений строк. В таблице ниже перечислены и описаны методы сравнения значений.
| Имя метода | Использовать |
|---|---|
| String.Compare | Сравнивает значения двух строк. Возвращает целочисленное значение. |
| String.CompareOrdinal | Сравнивает две строки без учета локального языка и региональных параметров. Возвращает целочисленное значение. |
| String.CompareTo | Сравнивает текущий строковый объект с другой строкой. Возвращает целочисленное значение. |
| String.StartsWith | Определяет, начинается ли строка с переданной строки. Возвращает логическое значение. |
| String.EndsWith | Определяет, заканчивается ли строка переданной строкой. Возвращает логическое значение. |
| String.Contains | Определяет, встречается ли символ или строка в другой строке. Возвращает логическое значение. |
| String.Equals | Определяет, совпадают ли две строки. Возвращает логическое значение. |
| String.IndexOf | Возвращает индекс позиции символа или строки начиная с начала проверяемой строки. Возвращает целочисленное значение. |
| String.LastIndexOf | Возвращает индекс позиции символа или строки начиная с конца проверяемой строки. Возвращает целочисленное значение. |
Метод Compare
Статический метод String.Compare позволяет тщательно сравнивать две строки. Этот метод учитывает язык и региональные параметры. Эту функцию можно использовать для сравнения двух строк или подстрок двух строк. Кроме того, имеются перегруженные методы, которые учитывают или не учитывают регистр и вариативность языка и региональных параметров. В таблице ниже приведены три целочисленных значения, которые может возвращать этот метод.
Метод String.Compare в основном предназначен для использования при упорядочивании или сортировке строк. Не следует использовать метод String.Compare для проверки на равенство (то есть для явного поиска возвращаемого значения 0 без учета того, является ли одна строка меньше или больше другой). Для определения равенства двух строк используйте метод String.Equals(String, String, StringComparison) .
В примере ниже метод String.Compare используется для определения относительных значений двух строк.
Этот пример выводит на консоль значение -1 .
В предыдущем примере по умолчанию учитывается язык и региональные параметры. Чтобы выполнить сравнение строк без учета языка и региональных параметров, используйте перегрузку String.Compare метода, которая позволяет указать язык и региональные параметры для использования путем предоставления параметра String.Compare . Пример, демонстрирующий использование String.Compare метода для выполнения сравнения без учета языка и региональных параметров, см. в разделе String.Compare.
Метод CompareOrdinal
Метод String.CompareOrdinal сравнивает два строковых объекта без учета локального языка и региональных параметров. Возвращаемые этим методом значения идентичны значениям, возвращаемым методом Compare в предыдущей таблице.
Метод String.CompareOrdinal в основном предназначен для использования при упорядочивании или сортировке строк. Не следует использовать метод String.CompareOrdinal для проверки на равенство (то есть для явного поиска возвращаемого значения 0 без учета того, является ли одна строка меньше или больше другой). Для определения равенства двух строк используйте метод String.Equals(String, String, StringComparison) .
В примере ниже метод CompareOrdinal используется для сравнения значений двух строк.
Этот пример выводит на консоль значение -32 .
Метод CompareTo
Метод String.CompareTo сравнивает строку, которую инкапсулирует текущий строковый объект, с другой строкой или объектом. Возвращаемые этим методом значения идентичны значениям, возвращаемым методом String.Compare в предыдущей таблице.
Метод String.CompareTo в основном предназначен для использования при упорядочивании или сортировке строк. Не следует использовать метод String.CompareTo для проверки на равенство (то есть для явного поиска возвращаемого значения 0 без учета того, является ли одна строка меньше или больше другой). Для определения равенства двух строк используйте метод String.Equals(String, String, StringComparison) .
В примере ниже метод String.CompareTo используется для сравнения объекта string1 с объектом string2 .
Этот пример выводит на консоль значение -1 .
Все перегрузки метода String.CompareTo по умолчанию выполняют сравнение с учетом языка и региональных параметров и регистра. У этого метода нет перегрузок, позволяющих выполнять сравнение без учета языка и региональных параметров. В целях повышения ясности кода рекомендуется использовать вместо него метод String.Compare , указывая CultureInfo.CurrentCulture для операций с учетом языка и региональных параметров и CultureInfo.InvariantCulture для операций без учета языка и региональных параметров. Примеры, демонстрирующие использование String.Compare метода для сравнения как с учетом языка и региональных параметров, так и без учета языка и региональных параметров, см. в разделе String.Compare .
Метод Equals
С помощью метода String.Equals можно легко определить идентичность двух строк. Этот метод учитывает регистр и возвращает логическое значение true или false . Метод можно вызывать из существующего класса, как показано в следующем примере. В примере ниже метод Equals используется для определения того, содержит ли строковый объект фразу «Hello World».
Этот пример выводит на консоль значение True .
Этот метод также можно использовать как статический. В примере ниже два строковых объекта сравниваются с помощью статического метода.
Этот пример выводит на консоль значение True .
Методы StartsWith и EndsWith
Метод String.StartsWith можно использовать для определения того, начинается ли строковый объект с тех же символов, которые включает другая строка. Этот метод учитывает регистр и возвращает значение true , если текущий строковый объект начинается с переданной строки, и значение false в противном случае. В примере ниже этот метод используется для определения того, начинается ли строковый объект со слова «Hello».
Этот пример выводит на консоль значение True .
Метод String.EndsWith сравнивает переданную строку с символами, находящимися в конце текущего строкового объекта. Он также возвращает логическое значение. В примере ниже конец строки проверяется с помощью метода EndsWith .
Этот пример выводит на консоль значение False .
Методы IndexOf и LastIndexOf
С помощью метода String.IndexOf можно определить позицию первого вхождения конкретного символа в строку. Этот метод учитывает регистр и начинает отсчет с начала строки. Он возвращает позицию переданного символа, используя отсчитываемый от нуля индекс. Если символ не удается найти, возвращается значение –1.
В примере ниже метод IndexOf используется для поиска первого вхождения символа » l » в строку.
Этот пример выводит на консоль значение 2 .
Метод String.LastIndexOf аналогичен методу String.IndexOf за исключением того, что он возвращает позицию последнего вхождения конкретного символа в строку. Он учитывает регистр и использует отсчитываемый от нуля индекс.
В примере ниже метод LastIndexOf используется для поиска последнего вхождения символа » l » в строку.
Как сравнить две строки в c
Для хранения строк в C++ применяется тип string . Для использования этого типа его необходимо подключить в код с помощью директивы include :
Тип string определен в стандартной библиотеке и при его использовании надо указывать пространство имен std .
Либо можно использовать выражение using, чтобы не указывать префикс std:
В данном случае значение переменной hello, которая представляет тип string, выводится на консоль.
При компиляции через g++ может потребоваться указать флаг -static . То есть если код определен в файл hello.cpp, то команда на компиляцию для g++ может выглядеть следующим образом:
Для инициализации строк можно использовать различные способы:
Консольный вывод данной программы:
Если при определении переменной типа string мы не присваиваем ей никакого значения, то по умолчанию данная переменная содержит пустую строку:
Также можно инициализировать переменную строчным литералом, который заключается в двойные кавычки:
В качестве альтернативы можно передавать строку в скобках после определения переменной:
Если необходимо, чтобы строка содержала определенное количесто определеных символов, то можно указать в скобках количество символов и сам символ:
И также можно передать переменной копию другой строки:
Конкатенация строк
Над строками можно выполнять ряд операций. В частности, можно объединять строки с помощью стандартной операции сложения:
Сравнение строк
К строкам можно применять операции сравнения. Оператор == возвращает true, если все символы обеих строк равны.
При этом символы должны совпадать в том числе по регистру.
Операция != возвращает true, если две строки не совпадают.
Остальные базовые операции сравнения < , <= , > , >= сравнивают строки в зависимости от регистра и алфавитного порядка символов. Например, строка «b» условно больше строки «a», так как символ b по алфавиту идет после символа a. А строка «a» больше строки «A». Если первые символы строки равны, то сравниваются последующие символы:
В данном случае условие s1 > s2 ложно, то есть s2 больше чем s1, так как при равенстве первых двух символов («Ap») третий символ второй строки («o») стоит в алфавите до третьего символа второй строки («r»), то есть «o» меньше чем «r».
Размер строки
С помощью метода size() можно узнать размер строки, то есть из скольких символов она состоит:
Если строка пустая, то она содержит 0 символов. В этом случае мы можем применить метод empty() — он возвращает true, если строка пустая:
Чтение строки с консоли
Для считывания введенной строки с консоли можно использовать объект std::cin:
Однако если при данном способе ввода строка будет содержать подстроки, разделенные пробелом, то std::cin будет использовать только первую подстроку:
Чтобы считать всю строку, применяется метод getline() :
Метод getline принимает два объекта — std::cin и переменную, в которую надо считать строку.
Получение и изменение символов строки
Подобно массиву мы можем обращаться с помощью индексов к отдельным символам строки, получать и изменять их:
Символьные массивы
Массив символов, последний элемент которого представляет нулевой символ ‘\0’, может использоваться как строка:
Данный код выведет на консоль строку «hello». Подобное определение массива строк будет также эквивалентно следующему:
Однако подобное использование массива строк унаследовано от языка Си, а при написании программ на С++ при работе со строками следует отдавать предпочтение встроенному типу string, а не массиву символов.
Сравнение строк в C# (по умолчанию)
Часто бывает, что мы соединяем 2 коллекции или группируем коллекцию при помощи LINQ to Objects. При этом происходит сравнение ключей, выбранных для группировки или связывания.
К счастью, стоимость этих операций равна O(n). Но в случае больших коллекций нам важна эффективность самого сравнения. Если в качестве ключей выбраны строки, то какая из реализаций сравнения будет использована по умолчанию, подходит ли эта реализация для ваших строк и можно ли, указав IEqualityComparer<string> явно, сделать эту операцию быстрее?
Как же выбирается реализация компаратора, если пользователь не указал её явно?
В исходном коде метода Join можно увидеть следующее поведение:
Хорошо, в метод JoinIterator передаётся null, внутри не происходит никаких проверок и значение null передаётся в качестве параметра при создании Lookup в метод CreateForJoin.
Использование Lookup нечасто можно встретить в явном виде. Этот класс представляет собой коллекцию с доступом к элементам по ключу, при этом для каждого ключа может храниться несколько элементов, а в случае попытки доступа по несуществующему ключу просто вернётся пустая коллекция.
Нас интересует метод CreateForJoin:
EqualityComparer
- Для byte будет выбран свой особенный ByteEqualityComparer. Этот класс создан с целью повышения производительности при сравнении массивов байт, поскольку содержит реализацию IndexOf для байтового массива.
- Если T реализует интерфейс IEquatable<T>, то будет выбран GenericEqualityComparer<T>, который сравнивает объекты на основе вызова их реализаций метода IEquatable<T>.Equals(T). Кроме того, перед вызовом оба параметра будут проверены на неравенство null.
- Если T представляет собой Nullable<U>, значение которого реализует IEquatable<U>, будет использован класс NullableEqualityComparer<T>, аналогичный предыдущему и содержащий дополнительные проверки HasValue.
- Для перечислений в зависимости от базового типа будет выбрана одна из реализаций EnumEqualityComparer<T> (для типа long есть своя особенная реализация), которые отличаются только JIT-оптимизациями того, как значение перечисления будет приведено к числовому значению.
- Во всех остальных случаях используется ObjectEqualityComparer<T>, который сравнивает объекты на основе Object.Equals. Здесь всё как обычно — для ссылочных типов проверяется равенство ссылок, для значимых — совпадение типов объектов (в случае с ObjectEqualityComparer<T> всегда true) и совпадение значений всех полей объектов.
Сравнение строк
Метод String.Equals(string value) проверяет равенство ссылок строк, равенство длины строк, и в случае, если вычислить равенство на основе этих свойств не получилось, вызывает (почти) побайтовое сравнение буферов строк:
Для повышения производительности в Microsoft даже размотали циклы.
Итак, по умолчанию, для сравнения строк используется побайтовое сравнение содержимого этих строк. Эффективно? Наверное. А как ещё можно сравнивать строки?
StringComparer
- CurrentCulture/CurrentCultureIgnoreCase — сравнение по словам с учётом правил текущей культуры и языка (культуры текущего Thread)
- InvariantCulture/InvariantCultureIgnoreCase — сравнение по словам без учёта правил языка и культуры и языка (используется CultureInfo.InvariantCulture)
- Ordinal/OrdinalIgnoreCase — побайтовое сравнение
В случае учёта регистра символов отличие заключается в дублировании проверок равенства ссылок объектов и проверки на null. Это не очень много, хотя с точки зрения MSIL это пара десятков инструкций:
Как насчёт сохранения без учёта регистра? Признаться, я и не ожидал увидеть что-то вроде ToLower, поскольку эта операция зависит от культуры. Но результат всё-таки превзошёл ожидания. Для вызова String.Compare(x, y, StringComparison.OrdinalIgnoreCase) выполняется следующая ветвь кода:
Интересно, на сколько хуже должно быть медленное решение чтобы проверка IsAscii была оправдана?
В случае с ASCII-строками проверка осуществляется действительно посимвольно, каждый символ проверяется на регистр и, при необходимости, переводится в верхний регистр простым вычитанием 0x20.
Для не ASCII строк вызывается нативный код на C++ и далее в зависимости от условий может вызываться метод сравнения строк из ядра Windows или (судя по комментариям, это возможно только для Windows XP) метод, который проводит такое же посимвольное сравнение, переводя каждый символ в верхний регистр на основе таблиц символов операционной системы.
Это действительно вызывает интерес к вопросу производительности, поскольку производительность одного и того же компаратора может отличаться в зависимости от входных данных, в вырожденном случае — от изменения одного символа в строке.
А что на счёт культур? Класс CultureAwareComparer принимает на вход культуру, на основании которой будет сравнивать строки и флаг, сообщающий о том, будет ли игнорироваться регистр символов. Информация о культуре содержит свойство CompareInfo, объект которого содержит методы для сравнения строк с учётом данной культуры, которые и используются в CultureAwareComparer.
К сожалению, внутри нет ничего интересного, поскольку по факту вызывается нативный код, который снова лезет в ядро для вызова функции сортировки строк. Чтобы компенсировать отсутствие кода вот вам отрывок, который неоднократно встречается в функциях работы со строками в coreclr:
То есть, сравнение строк .net с учётом культуры и языка всегда происходит на уровне ядра операционной системы.
Тесты производительности
После такого путешествия я не смог не заинтересоваться реальной разницей в производительности. Изначально моим сценарием было объединение последовательностей, поскольку в результате происходит большое количество сравнений. Для чистого теста я оставил только сравнение строк без дополнительных операций. Исходный код теста можно посмотреть или сграбить на GitHub. Результаты немного плавали, но я решил, что 10.000 итераций по 1.000.000 будет достаточно и без доверительного интервала.
| Сценарий | Миллисекунд/1.000.000 операций | Относительная разница |
|---|---|---|
| string.Equals | 25.8 | 1x |
| EqualityComparer<string>.Default | 33.5 | 1.3x |
| StringComparer.Ordinal | 29.8 | 1.16x |
| StringComparer.OrdinalIgnoreCase | 50.3 | 1.95x |
| StringComparer.OrdinalIgnoreCase non ASCII | 82.2 | 3.19x |
| StringComparer.CurrentCulture | 136 | 5.27x |
| StringComparer.CurrentCulture non ASCII | 174.3 | 6.76x |
| StringComparer.CurrentCultureIgnoreCase | 134.5 | 5.21x |
| StringComparer.CurrentCultureIgnoreCase non ASCII | 172.1 | 6.67x |
| StringComparer.InvariantCulture | 132.2 | 5.12x |
| StringComparer.InvariantCulture non ASCII | 189.5 | 7.34x |
| StringComparer.InvariantCultureIgnoreCase | 134.1 | 5.2x |
| StringComparer.InvariantCultureIgnoreCase non ASCII | 188 | 7.29x |
Результаты подтверждают код — явный вызов string.Equals быстрее всего, GenericEqualityComparer<string> медленнее за счёт дополнительных проверок входных параметров. В OrdinalComparer тоже есть дополнительные проверки. А далее вызываются либо не размотанные циклы, либо методы неуправляемого кода, которые, вообще говоря, ведут себя по-разному на разных платформах, но в случае работы с культурой вызывают методы из ядра операционной системы.
Сравнение в других операциях над строками
Казалось бы, по умолчанию используется самое быстрое и простое сравнение, программисту можно не беспокоиться. На самом деле всё не совсем так. Есть ещё ряд операций над строками. Например, определение того, начинается ли строка с определенной подстроки:
В то же время проверка вхождения подстроки (казалось бы, тоже самое) снова осуществляется побайтово:
А проверка вхождения подстроки, которая вернёт индекс начала этой подстроки — снова с текущей культурой:
LastIndexOf вообще вызывает нативный код. Что в нём происходит? Только Сатья знает.