Pull to refresh

Comments 39

Очень давно, холиварщики сообщали, что PHP проще и поэтому лучше. С такими тонкостями, он уже не выглядит таким простым.

Неудачное название для функции, лучше бы strlen назвали datalen, а mb_strlen должна быть strlen

mb = multibyte

Вполне нормально.

Сбивает с толку количество. Интуитивно, хочется один strlen, а проблему кодировок отдать наоткуп какому-нибудь decode()

mb_ в первую очередь значит что это функция из расширения mbstring (а вот оно уже multibyte string), а не встроена в ядро

Нужно очень глубоко влезать, чтобы разобраться во всем этом. Если никогда не пользовался php, вызывает слишком много вопросов.

Это всё вызывает вопросы только у людей, которые не слышали про обратную совместимость и не застали доюникодную эпоху.

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

Чтобы проиллюстрировать проблему, можно, например, попробовать перевести в верхний регистр название турецкого города «стамбул», или греческое приветствие «яссас»:

iex|🌢|1 ▶ String.capitalize "istanbul", :turkic
"İstanbul" # опа, заглавное «İ» с точкой
iex|🌢|2 ▶ String.upcase "γειά σας", :greek
"ΓΕΙΆ ΣΑΣ" # последняя сигма не отличается от третьей с конца
iex|🌢|3 ▶ String.downcase "ΓΕΙΆ ΣΑΣ", :greek
"γειά σας" # для строчных последняя буква другая

Но это ладно, кого там волнуют греки и турки, правда? Тогда вот:

https://emojipedia.org/family-man-woman-girl-boy (надеюсь, Хабр нарисует этот эмоджик корректно): "👨‍👩‍👧‍👦"

Длина? — Попробуйте угадать:

  • javascript → 11

  • ruby → 7

  • python → 7

  • elixir → 1

Вероятно, Вы пропустили слова - если никогда не пользовался php.

На одной чаше весов обратная совместимость, а на другой последние стандарты. В зависимости от ситуации, выбираем что-то одно.

Вероятно, Вы пропустили слова - если никогда не пользовался php.

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

Нужно добавить на банеры по курсам для No-Code и Vibe-coding: "Хочешь программировать без родовых травм всех ЯП? Спроси меня, как."

Интуитивно, хочется один strlen

Дорого. Надо или каждый раз utf-8 парсить, или хранить в 32-битном UNICODE и придумать, что делать с графемами, которые отображаются как один символ, но состоят из нескольких.

При этом это еще и редко надо. Типичные задачи: понять, сколько места надо зарезервировать для строки, сконкатенировать несколько строк, найти в строке символ '/' - для них для всех простой Сишный strlen - то, что надо. А для отображения на экране нужен не умный strlen, а функция, которая вычислит экранный размер с учётом особенностей фонтов.

Типичные задачи: […]

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

Дорого в данном случае — это смешной аргумент. Медленно будет не здесь (особенно — в PHP). Единственная причина — обратная совместимость.

Для отрезания хвоста вообще нужна функция, которая принимает максимальный размер в байтах а режет по графемам.

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

Это еще почему? VARCHAR(30) — зависит от кодировки таблицы и это не 30 байт в общем случае.

Смотря в каком диалекте SQL. Например, в MS SQL указывается именно размер в байтах. А уж сколько символов влезет - как получится.
Но по хорошему таки да, нужна ещё функция, принимающая максимальный размер в кодепоинтах.

Это родовая травма микрософтовских битв с юникодом, которые они все как одну проиграли, но в MSSQL есть NCHAR/NVARCHAR и они прекрасно подходят для хранения чего угодно по количеству графем.

Да, база распухнет почти вдвое, но на производительности это не скажется.

NVARCHAR(n) по прежнему не учитывает символы, только байтовые пары в UCS-2. То есть, размер в байтах тупо равен n*2.

strlen - наследие Си

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

Это ж название функции из Си, которую просто прокинули в пых. Как и ещё половина пыховских API. За эргономику эти ребята практически не занимались длительное время и просто прокидывали сишные апишки.

PHP проще для тех, кто нормальных языков не видел.

Какие языки "нормальные"?

Вы кстати тоже ошибаетесь, mb_strlen счтиает не в символах, а в кодпоинтах. В символах считает grapheme_strlen

> strlen("sincérité")
= 13

> mb_strlen("sincérité")
= 11

> grapheme_strlen("sincérité")
= 9

Вы абсолютно правы, mb_strlen() считает кодпоинты.С некоторыми наборами символов он тоже выдаст не то, "что видно на экране". Супер комментарий, спасибо!
Дополню статью.

Стоит заметить, что и mb_strlen не гарантирует точного результата. Она вернёт не количество символов, а количество UTF-кодепоинтов. Есть ещё одна функция, возвращающая количество графем в строке - grapheme_strlen.

<?php
$emoji = "👩🏼‍👩🏼‍👦🏼‍👦🏼";
printf("strlen('%s') = %d\n", $emoji, strlen($emoji));
printf("mb_strlen('%s') = %d\n", $emoji, mb_strlen($emoji));
printf("grapheme_strlen('%s') = %d\n", $emoji, grapheme_strlen($emoji));
// strlen('👩🏼‍👩🏼‍👦🏼‍👦🏼') = 41
// mb_strlen('👩🏼‍👩🏼‍👦🏼‍👦🏼') = 11
// grapheme_strlen('👩🏼‍👩🏼‍👦🏼‍👦🏼') = 1

Точность зависит от задачи, например чтобы уложиться в условый VARCHAR(30) CHARSET 'utfmb4' в MySQL, надо считать в кодпоинтах

<?php
setlocale( LC_ALL, 'ru_RU.CP1251', 'rus_RUS.CP1251', 'Russian_Russia.1251' );
echo strlen('привет!');

файл, естественно, в кодировке 1251

Наиболее популярным ответом оказался «7», что неверно.

Будет ровно 7 - так что сначала нужно научиться четко ставить задачу.

От того, что вы задали локаль консоли, код автоматически не переехал из UTF-8 в cp1251 и по прежнему возвращает 13. Чтобы он вернул 7 надо не локаль менять, а сохранять исходник в cp1251.

Это само собой разумеется, ибо ставить локаль отличную от кодировки файла никто в здравом уме не будет, но я написал на всякий случай)

А задача должна быть сформулирована так "Что выведет echo strlen('привет!') в php, при кодировке файла с этой командой в UTF-8 и настройками локали PHP такими-то?".

Вопрос был на понимание строк. Да, условие не дает 100%-ой конкретики. Но может же быть вариант ответа "зависит от настроек/кодировок". И есть стандартные/распространенные настройки

Он и остался на понимание строк.
Может быть. Как и есть то, что ответ "7 неверно" - неверный ¯\_(ツ)_/¯

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

Поэтому все нужно проговоривать всегда - это снижает кол-во проблем до минимума.

В целом неплохо, но немного сумбурно и местами шероховато.

strlen() считает байты

а только что говорили, что на считает, а показывает уже посчитанное :)

Строки в PHP — не массивы: доступ через [] опасен для Unicode.

Хромает логика: то, что доступ опасен, следует не из того, что строки - это не массивы. А из байтовой натуры оператора []. Вместо двоеточия лучше поставить точку, а ещё лучше разнести в список.

Многие функции, применяемые к массивам, нельзя применить к строкам.

Вот тут даже интересно стало. "Многие" заведомо означает некоторые всё-таки можно. Это какие например?

Отличный коммент :)

Вот тут даже интересно стало. "Многие" заведомо означает некоторые всё-таки можно. Это какие например?

Я не имею ввиду какие-то специфичные только для массивов функции. isset, empty and etc - применимо к любому типу, и к строкам, и к массивам... В json_encode можно передать и то, и другое, и получить преобразование.

PHP - очень высокоуровневый язык с динамической типизацией. Это как бы сразу указывает на его аудиторию. Ещё сильнее делает акцент на аудиторию и скриптовый характер языка, и, особенно, то, что код на PHP - это фактически шаблон для вывода текста.

И что же у нас получается? Глядя на эту основную аудиторию, создатели языка не сделали функцию, само название которой говорит о подсчёте длины строки, универсальной для ASCII и Юникода... По-моему, это провал.

Я не знаю ни одного PHP программиста, который бы на начальном этапе своей карьеры не обжёгся хотя бы раз об юникод. Не говорит ли это о плохом дизайне?

И ведь уже ничего не изменить, ведь у нас "обратная совместимость", чтоб её...

А ведь могли бы при появлении проблемы с юникодом усложнить strlen с целью подсчёта символов, а новую функцию сделать именно для подсчёта байтов. Это же очевидно!!! И так же сделать с остальными подобными функциями. И все 10 человек, которые бы пользовались функцией подсчёта байтов в строке, были бы счастливы.

Подобные штуки есть во многих языках, это просто нужно знать, в том же js есть просто адские поведения. Любой, кто пишет на php на нормальном уровне, знает про такую особенность работы со строками, причём речь не только о strlen. Просто это всегда в голове.

Подсчёт длины строки — это наименьшее из зол. Реальные приколы начинаются, когда нужно получить подстроку. Например, первые 50 символов текста сохранить в поле анонс. Если использовать substr, то мультибайтный символ может оказаться разрезанным пополам, и далее при записи в базу mysql вылетит с ошибкой.

Ну или ещё прикол:

var_dump(ucfirst('привет'));

выведет "привет" с маленькой буквы.

Ну или ещё прикол:

mb_ucfirst уже завезли (в PHP 8.4).

Так в этом же и суть, чтобы по возможности использовать функции с префиксом mb, чтобы не попадать в такие ситуации.

Sign up to leave a comment.

Articles