Sign in to follow this  
Followers 0
Jawn

Подсчет размера строки

6 posts in this topic

Введение.

К сожалению, у многих пользователей, работающих с языком pawn, возникают вопросы, связанные с подсчетом размера строки. Вероятно, их просто отпугивает ошеломляющий вид конечной строки (которую мы получим на выходе). Хотя на самом деле здесь нет ничего сложного. Данная тема обретает актуальность, потому что все большее количество разработчиков пользуются данным методом. Ну, что ж, попробуем разобраться.

 

А действительно ли оно того стоит?

Было время, когда я и сам использовал таковой метод. Однако со временем я решил воздержаться от него. Почему? Казалось бы, все удобно и чудесненько. Но меня напрягает в таком случае мой же код: появляется некий мусор, который только мешает мне обрабатывать код. Давайте взглянем на два примера. В одном из них я использую метод, о котором пойдет речь. В другом же я воздерживаюсь от него и делаю это альтернативно (узнаем дальше).

Спойлер
Спойлер


#define MAX_LENGTH_MESSAGE	100 // объявим значение максимального количества символов в сообщении

CMD:send(playerid, params[])
{
	static const USE_COMMAND = !"Используйте команду следующим образом: /send [playerid] [text]"; // объявим массив, представляющий собой строку
	
	if(isnull(params)) // проверим, точно ли в params присутствует какой-либо текст
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если нет, уведомим пользователя и завершим работу функции
	
	new targetid; // объявим переменную для записи ID игрока, которому отправляем сообщение
	if(sscanf(params, "us[ "MAX_LENGTH_MESSAGE" ]", targetid, params)) // извлечем из массива params два аргумента: ID игрока (которому отправим сообщение) и само сообщение
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если не получилось выделить (нет этих аргументов), завершим работу функции с выводом сообщения
	if(targetid == INVALID_PLAYER_ID) // если игрок не подключен к серверу
		return SendClientMessage(playerid, -1, !"Указанный вами игрок отключен от сервера."); // завершим работу функции с выводом сообщения
	if(targetid == playerid) // если игрок пытается отправить сообщение сам себе
		return SendClientMessage(playerid, -1, !"Ну и зачем же отправлять сообщение самому себе?"); // завершим работу функции с выводом сообщения
	
	static const
		fmt_str1[] = "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)", // объявим строку для подсчета
		fmt_str2[] = "Новое сообщение от %s: %s"; // объявим строку для подсчета
		
	new
		string1[sizeof fmt_str1+(-2+MAX_PLAYER_NAME)+(-2+3)], // объявим переменную, в которую будет записана наша строка
		str2[sizeof fmt_str2+(-2+MAX_PLAYER_NAME)+(-2+MAX_LENGTH_MESSAGE)]; // объявим переменную, в которую будет записана наша строка
		
	GetPlayerName(targetid, string1, MAX_PLAYER_NAME); // получаем имя игрока
	format(string1, sizeof string1, fmt_str1, string1, targetid); // записываем в первую строку имя игрока и его ID
	SendClientMessage(playerid, -1, string1); // отправляем сообщение
	
	GetPlayerName(playerid, string2, MAX_PLAYER_NAME); // получаем имя игрока
	format(string2, sizeof string2, fmt_str2, string2, params); // записываем в первую строку имя игрока и его ID
	SendClientMessage(targetid, -1, string2); // отправляем сообщение
	return 1;
}

 

Спойлер


#define MAX_LENGTH_MESSAGE	100 // объявим значение максимального количества символов в сообщении

CMD:send(playerid, params[])
{
	static const USE_COMMAND = !"Используйте команду следующим образом: /send [playerid] [text]"; // объявим массив, представляющий собой строку
	
	if(isnull(params)) // проверим, точно ли в params присутствует какой-либо текст
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если нет, уведомим пользователя и завершим работу функции
	
	new targetid; // объявим переменную для записи ID игрока, которому отправляем сообщение
	if(sscanf(params, "us[ "MAX_LENGTH_MESSAGE" ]", targetid, params)) // извлечем из массива params два аргумента: ID игрока (которому отправим сообщение) и само сообщение
		return SendClientMessage(playerid, -1, USE_COMMAND) & 0; // если не получилось выделить (нет этих аргументов), завершим работу функции с выводом сообщения
	if(targetid == INVALID_PLAYER_ID) // если игрок не подключен к серверу
		return SendClientMessage(playerid, -1, !"Указанный вами игрок отключен от сервера."); // завершим работу функции с выводом сообщения
	if(targetid == playerid) // если игрок пытается отправить сообщение сам себе
		return SendClientMessage(playerid, -1, !"Ну и зачем же отправлять сообщение самому себе?"); // завершим работу функции с выводом сообщения

	new
		string1[102+(-2+MAX_PLAYER_NAME)+(-2+3)+1], // объявим переменную, в которую будет записана наша строка
		string2[25+(-2+MAX_PLAYER_NAME)+(-2+MAX_LENGTH_MESSAGE)]; // объявим переменную, в которую будет записана наша строка
		
	GetPlayerName(targetid, string1, MAX_PLAYER_NAME); // получаем имя игрока
	format(string1, sizeof string1, "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)", string1, targetid); // записываем в первую строку имя игрока и его ID
	SendClientMessage(playerid, -1, string1); // отправляем сообщение
	
	GetPlayerName(playerid, string2, MAX_PLAYER_NAME); // получаем имя игрока
	format(string2, sizeof string2, "Новое сообщение от %s: %s", string2, params); // записываем в первую строку имя игрока и его ID
	SendClientMessage(targetid, -1, string2); // отправляем сообщение
	return 1;
}

 

 

Заметьте: во втором случае мы сразу видим, с какой именно строкой работаем (при форматировании) - это визуальное удобство. Да и сокращается количество строк. Это нисколько не оптимизация, просто намного удобнее, когда в коде нет ничего лишнего. Не так ли? Впрочем, дело ваше. А свою позицию я высказал.

 

В чем принцип работы данного метода?

Идея проста - мы создаем константное выражение. Как известно, обработкой констант занимается сам компилятор, а не сервер. Именно компилятор и рассчитывает размер строки, которую мы будем форматировать. Например, использовав такой код, мы получим размер строки (8, поскольку учитывается еще нуль-символ):

new text[] = "привет";
printf("%d", sizeof text);

Здесь компилятор подсчитывает количество ячеек в массиве "привет!". Именно это количество ячеек (и завершающий нуль-символ) есть размер строки, который выведет функция sizeof.

 

Итак, в моем примере сама строка имеет следующий вид:

static const fmt_str1[] = "Ваше сообщение успешно отправлено указанному игроку (%s, ID: %d)";

Именно ее размер компилятор посчитает автоматически (почему используется static const?).

Далее мы создаем новую переменную, куда будем форматировать нашу строку. Но почему нельзя провернуть такую операцию с имеющейся строкой? Вся проблема в том, что наша строка (fmt_str1) объявлена на количество ячеек, которое занимает весь наш текст (а это количество ячеек - 64+1 - с учетом нуль-символа).

 

Обратите внимание: в нашей исходной строке присутствуют спецификаторы (%s и %d), вместо которых при форматировании будут подставлены наши строковое и цифровое значения соответственно. Именно длины, необходимые для данных аргументов. Например, для аргумента "hello world" нам необходима длина в 11 символов, для записи года нам нужно будет 4 символа, для записи имени игрока - MAX_PLAYER_NAME (24) символов. Динамические (непостоянные) значения использовать не получится. Однако, можно лишь предохраниться и задействовать количество ячеек, в которое вы точно уложитесь.

 

format - функция, которая осуществляет форматирование строки. Ее суть проста - мы передаем в функцию ту строку, в которую записываем (а если быть точнее, то ее адрес), передаем адрес строки, которую записываем, передаем аргументы. Далее обычный цикл перебирает все ячейки нашей строки (которую записываем) и ищет спецификаторы (%d, %s, %b, %c, %f, %x, %i). Эти спецификаторы также могут иметь и другие нюансы (измененное количество символов, например, см. дальше). Как только цикл находит спецификатор, то буквально стирает его и записывает наш аргумент первый. Находит второй спецификатор - записывает второй аргумент. Если же количество спецификаторов не совпадает с количеством аргументов, тогда записываются до тех пор, пока существуют "свободные" аргументы и спецификаторы. Не хватает аргументов - спецификаторы просто удаляются. Не хватает спецификаторов - аргументы просто игнорируются. Если тип спецификатора не соответствует типу аргумента, то запись производится неправильно (ибо для избежания этого не реализованы какие-либо проверки).

 

Итак, как ранее было сказано, оператор sizeof вычисляет (причем еще на этапе компиляции!) длину строки, которая указана как аргумент. Наша переменная, в которую будем форматировать строку, должна иметь достаточное количество ячеек как для самой строки, так и для ее аргументов. Поэтому начинаем расписывать ее размер (самая пугающая для большинства часть). Кстати, если же рассмотреть со стороны математики, где A - размер переменной, B - размер строки, а C, D, E... (и так далее) - размеры аргументов, то мы получаем следующее множество: A=B+C+D+E..+Z.

 

Создадим строку, на основе которой будем проводить наши расчеты:

static const fmt_str[] = "Сегодня %02d/%02d/%d, время - %02d/%02d. Вас зовут %s. Ваше здоровье - %.2f";
new string[], year, month, day, hour, minute, Float:health;
getdate(year, month, day);
gettime(hour, minute);
GetPlayerName(playerid, string, MAX_PLAYER_NAME);
format(string, sizeof string, fmt_str, day, month, year, hour, minute, string, health);
SendClientMessage(playerid, -1, string);

Длина нашей строки - 75+1 символов (не забываем про нуль-символ). Любой нормальный редактор кода показывает количество символов в строке.

 

Наша строка хранит в себе информацию о сегодняшней дате (дд/мм/гггг - это 2(дд)+2(мм)+4(гггг)+2(/) =  10 символов), о времени (чч/мм - это 2(чч)+2(мм)  = 4 символа), об имени игрока (MAX_PLAYER_NAME - это константа, которая имеет значение 24), а также о его здоровье. Вещественное (дробное) значение после запятой (плавающей точки) может хранить как 1, так и 6, 7, 8 символов. Потому я обрезал выводимое количество символов после запятой лишь до двух, о чем свидетельствует сама форма записи: "%.2f". Соответственно, если бы она имела вид "%.5f", вы бы увидели после запятой пять символов (при наличии лишь одного выведется "1.800000"). А если вам вообще не нужны цифры после запятой, делаем как-то вот так: "%.0f". Аналогичный метод работает и в целочисленных выражениях. Если мы записываем в форматировании спецификатор вида "%03d" - это значит, что выражение обязательно должно состоять из трех символов (если их больше, остальные просто не отображаются, а если меньше - дописываются нули в начале).

 

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

Теперь мы как бы начинаем приплюсовывать длины наших аргументов. Словно читаем, слева направо имеем:

  1. Вывод дня, месяца, года. Отнимаем длины спецификаторов и заместо них добавляем длины, которые будут записаны вместо этих спецификаторов: (-4+2) - это мы вычисли длину спецификатора "%02d" и добавили длину аргумента (дня), который содержит не больше двух символов. Аналогично имеем для месяца (-4+2), а для года получаем выражение следующего вида: -2+4, ведь здесь спецификатор состоит лишь из двух символов ("%d"), а аргумент (год) состоит из четырех символов (2019). Итого по данному пункту наша строка имеет вид: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)".
  2. Вывод времени. Здесь повторяем аналогичную операцию: вычитаем длину спецификаторов, прибавляем длину (максимальную) часов (2) и минут (2). Теперь наша строка должна выглядеть так: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)".
  3. Вывод имени игрока. Как известно, максимальная длина имени игрока - MAX_PLAYER_NAME (константное выражение, равное 24). Хотя на самом деле максимальная длина ника составляет 22 символа, но все-таки принято использовать константы (ведь неизвестно, какие изменения произойдут с рабочими папками в последующих обновлениях SAMP/CRMP). Вычитаем длину спецификатора, обозначающего запись строки (%s), и записываем максимальную длину имени игрока (константу): (-2+MAX_PLAYER_NAME). Новый вид всей нашей строки: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)+(-2+MAX_PLAYER_NAME)".
  4. Вывод количества здоровья игрока. Здесь мы сталкиваемся с вещественным числом. Известно, что здоровье игрока не может превышать 100. Следовательно, не может быть длиннее трех символов. А вот количество символов после запятой не ограничено системно (разве что максимальной вместимостью ячеек). Потому я их ограничил вручную, обрезав всего лишь до двух символов. Получаем нашего спецификатора "%.2f": 4. Вычитаем из длины форматируемой строки 4 символа, а заместо них добавляем максимальную длину здоровья игрока (3 символа до запятой и 2 символа после запятой): "-4+5". Теперь вся наша строка должна получить такой вид: "sizeof fmt_str+(-4+2)+(-4+2)+(-2+4)+(-2+2)+(-2+2)+(-2+MAX_PLAYER_NAME)+(-4+5)".

 

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

 

Автор статьи: @odosenok.

 

P.s. многовато получилось текста. Но я старался пояснить все, что только можно. Да и знания лишними не бывают. Не поленитесь и читайте статью полностью :$.

 

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

 

Спойлер

const
    MAX_LENGTH_PLAYER_LEVEL = 3,
	MAX_LENGTH_PLAYER_INTERIOR = 5; // 4 символа, но виртуальный мир может быть отрицательный. Под знак минуса тоже требуется ячейка

static const fmt_str[] = "Ваше уровень: %d, уровень розыска: %d, интерьер: %d";
new string[sizeof fmt_str+(-2+MAX_LENGTH_PLAYER_LEVEL)+(-2+1)+(-2+MAX_LENGTH_PLAYER_INTERIOR)];

 

 

Edited by odosenok

Share this post


Link to post
Share on other sites

Довольно не плохо, видно что постарался , держи плюсик
Но можно было бы сделать статью не много по меньше, не каждый User станет всё читать..

Share this post


Link to post
Share on other sites

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

Share this post


Link to post
Share on other sites

@odosenok Тут полностью согласен!

Share this post


Link to post
Share on other sites
4*(-4+2)+(-2+4)+(-2+MAX_PLAYER_NAME)+(-4+6)

И откуда взялся +1?

Share this post


Link to post
Share on other sites

@m1n1vv, не учел, что sizeof возвращает размер с учетом нуль-символа. В итоге он прибавлялся дважды в моем коде. Поправил везде. Спасибо.

Share this post


Link to post
Share on other sites

Your content will need to be approved by a moderator

Guest
You are commenting as a guest. If you have an account, please sign in.
Reply to this topic...

×   You have pasted content with formatting.   Remove formatting

  Only 75 emoticons maximum are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

Loading...
Sign in to follow this  
Followers 0

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • Jawn
      By Jawn
      1. Описание макроса.
      Данный макрос возвращает модуль указанного числа.
      2. Исходный код макроса.
      #if defined abs #undef abs #endif #define abs(%0)\ (%0 < 0) ? (-(%0)) : (%0) 3. Пример использования макроса.
      new a = random(100)-90; // значение переменной может быть отрицательным и положительным printf("Исходное число: %d. Модуль числа: %d.", a, abs(a));  
    • Guest
      By Guest
      Константы


      Константа - величина, не меняющаяся в процессе выполнения скрипта. В Pawn константы объявляются почти так же, как и переменные, однако каждое объявление начинается со слова const:
      const <тип>:<имя> = <значение>;  

      Для примера объявим число Пи:
      const Float:M_PI = 3.141593;  

      Теперь объявим целочисленную константу (как и в new, для целых чисел указание типа не потребуется):
      const MAX_BYTE = 255;  

      Внимание! В отличие от переменных (new), в константах нужно обязательно указывать значение.

      Правила задания имён у констант те же, что и у переменных, за исключением одного необязательного:
      Имя константы должно записываться в верхнем регистре (все буквы большие), чтобы было легче отличить его от имён переменных и функций. Примеры имён констант: MAX_PLAYERS, MAX_VEHICLES, MAX_PLAYER_NAME.
    • Guest
      By Guest
      Код на языке Pawn состоит из двух частей:
      Операторы описания Исполняемые операторы Комментарии
      Оператор описания описывает данные, над которыми выполняются действия.
      Исполняемый оператор задаёт действия, выполняемые над данными. Пример: сложение двух чисел, запись числа в память, вывод на экран.

      Для удобства операторы описания называют описаниями, а исполняемые операторы - просто операторами.

      Все исходные тексты на Pawn пишутся при помощи алфавита языка, который состоит из:
      Прописных и строчных букв латинского алфавита Цифр от "0" до "9" Спец. символов: "_", "@", "&", "+", "*", "%" и т.д.
      Комментарии - пояснения к исходному коду, служащие для его документирования. Компилятор их игнорирует, поэтому на скомпилированный скрипт (.amx) они никак не влияют.

      В Pawn существуют два вида комментариев: однострочный и многострочный.
      Однострочный комментарий начинается с символов "//" и продолжается до конца строки.
      Пример:
      // это комментарий  
      Многострочный комментарий начинается с символов "/*" и заканчивается символами "*/".
      Пример:
       
      /* это многострочный комментарий */

      Скрипты на языке Pawn имеют 2 расширения: .pwn и .amx.
      .pwn - файлы с исходным кодом на понятном человеку языке программирования.
      .amx - файлы с машинным кодом, которые выполняются интерпретатором (в нашем случае - сервером SA:MP).

      Машинный код (байт-код AMX) получается из файлов исходного кода (.pwn) путём компиляции. Компиляцией в Pawn занимается (внимание!) компилятор (pawncc).
    • Jawn
      By Jawn
      1. Введение. Суть перехватов.
      В данном уроке я постараюсь рассказать про то, что такое перехваты. Мы разберем некоторые примеры перехватов функций, покажем пример, где эти перехваты могут пригодиться.
       
      Что такое перехваты? Зачастую возникает ситуация, когда нам необходимо как-то изменить действия, происходящие в функции. Но редактирование уже имеющейся функции (как правило, это стандартные функции) не есть лучший вариант. Почему? Если мы хотим внести какие-то дополнительные действия в нужную нам функцию, придется открывать файл, в котором содержится данная функция, ее редактировать. А если выйдет новая версия файла a_samp? Все наши действия придется повторить сначала, теперь уже в новом файле.
       
      Для того собственно и придуман перехват. Так, мы перехватываем заранее объявленную функцию, вследствие чего меняется порядок вызова функций:
      перехваченная_функция_1 -> перехваченная_функция_2 -> перехваченная функция_3 -> основная_функция_которую_перехватывали Обратите внимание: в цепочке перехватов присутствуют несколько перехваченных функций. Отсюда можно сделать вывод, что число перехватов не ограничено никем и ничем. Более того, перехваты функций - это лишь смена названий каждой последующей функции препроцессором (перед началом компиляции), каждая из которых вызывается впоследствии системой.
       
      Для перехвата функций нам потребуются некоторые директивы (#define, #if defined, #endif), а также сами функции, которые мы внедряем в перехваты.
       
      2. Построение перехватов.
      Давайте попробуем немного проанализировать перехват функций, не имея пока что представления о том, как их правильно перехватывать.
       
      У нас есть функция A. Мы хотим, чтобы в ней происходил вывод текста "Hello world" в логи сервера. Как говорили ранее, внедрять этот текст в код функции A мы не будем (потому, что если выйдет обновление версии файла, в котором содержится эта функция, мы утратим свои корректировки в данной функции). Значит, нам нужно сделать так, чтобы это изменение могли внести без изменения кода файла.
       
      Рассмотрим следующий вариант:
      #define InterceptFunctionA()\ printf("Hello world");A(); В данном варианте мы создали макрос. Так, при использовании "InterceptFunctionA()" сначала в логи сервера будет вноситься наш текст, а затем вызываться сама функция. Но в данном методе есть два нюанса: во-первых, у нас теперь новое название функции (а мы не хотим поправлять его во всем моде), во-вторых, данный метод нам не подойдет, если вместе с данной функцией мы хотим выполнить еще с десяток других действий.
       
      Так я постарался дать понять Вам, что на самом деле перехваты не создают визуальных неудобств: функция будет называться так, как называлась, свой код в макросы вы не вписываете.
       
      В общем и целом метод перехватов, которому мы сейчас обучаемся, выглядит следующим образом:
      <перехватчик 1> <выполняем действия перехватчика 1> <вызываем следующий перехватчик> <конец перехватчика> <перехватчик 2> <выполняем действия перехватчика 2> <вызываем следующий перехватчик> <конец перехватчика> <перехватчик 3> <выполняем действия перехватчика 3> <вызываем перехватываемую функцию> <конец перехватчика> Каждый перехватчик есть ни что иное как отдельная функция. И, соответственно, в ходе перехватов мы поочередно вызываем функции, в которых прописаны Ваши действия перехватчика.
       
      Перехваты нативных функций и функций, вызывающихся автоматически, несколько отличаются. Поэтому урок мы немного разделим.
       
       
      Как правило, перехваты применяются тогда, когда одну и ту же функцию необходимо использовать по-разному в разных файлах. С помощью перехватов мы можем в каждом файле перехватывать отдельные функции и вносить корректировки в их работу.
       
      3. Перехваты функций: за и против.
      В каждой ситуации есть сторонники какой-либо идеи и ее противники. Сейчас я постараюсь рассказать основные аспекты использования перехвата функций. А использовать их или нет - решать Вам.
       
      1. Вы можете манипулировать одной и той же функцией по-разному в разных файлах. Это значит, что Вам не придется каждый раз добавлять различные проверки на то, подключен ли какой-либо файл, чтобы внести код "под него". Так, Вам достаточно лишь подключить нужный файл, чтобы вся цепочка вызовов работала как следует. Соответственно, при отключении файла проблем также не возникнет.
      2. Перехват функций есть ни что иное как последовательные вызовы функций. Несомненно, на вызов функций уходит какая-то часть времени, но это время ничтожно для того, чтобы Ваш сервер действительно стал подтормаживать.
      3. Если перехватываемую функцию использовать до того, как она перехвачена, функционировать она не будет (как Вы хотите). При этом про проблему в перехвате подумаете далеко не сразу. И главное - компилятор Вам не даст понять, что у Вас это не сработает, ведь со стороны его логики все сделано правильно. Поэтому перехватам вводят альтернативу: создают новую функцию с постфиксом "Ex", в которую уже помещают код перехвата и вызов самой функции:
      stock ПерехватываемаяФункцияEx(аргументы функции) { // ваш код return ПерехватываемаяФункция(аргументы функции); } Нюанс данной реализации в том, что теперь Вам придется использовать только ПерехватываемаяФункцияEx, о чем Вы можете забыть и вызвать не данную функцию, а ее оригинал.
       
      4. Авторское право.
      Автор данной статьи: @odosenok
      Отдельное спасибо за разъяснения: Daniel_Cortez
    • DEST
      By DEST
      Эта тема создана автоматически для возможности комментирования статьи Операции с текстом и данными в PAWN Автор статьи: @DEST
      Статья опубликована: 05.01.2018 17:52
       
       
       
      Просмотреть полную запись