В данной статье приводятся наиболее полезные примеры перегрузки на основе имён аргументов, которые встретились в моей практике, и которые я воплотил в 11l. Просто про именованные аргументы функций/методов здесь я говорить не буду: их польза или вред являются предметом споров, а поддержка в языках программирования варьируется от практически полного игнорирования (привет, C++) до явного злоупотребления\overuse этой возможностью (привет, Swift).

Под перегрузкой функций на основе имён аргументов понимается возможность объявления функций, имеющих одинаковое имя и, как правило, одинаковое количество аргументов, но которые [аргументы] отличаются своим именем [тип аргументов при этом может быть одинаковым]. При этом при вызове такой функции аргументы [те, которые отличаются только своим именем] следует именовать, иначе непонятно, какой перегруженный вариант функции следует выбрать.

Конструкторы


Наибольшую пользу, по моему мнению, перегрузка на основе имён аргументов имеет для конструкторов. Т.к. если для обычных функций/методов вполне можно обойтись добавкой к названию функции/метода [например, index(of: 1) можно заменить на indexOf(1), а index(where: ...) — на indexWhere(...)], то для конструкторов такое уже подходит гораздо хуже: charFromDigit или intToStringWithRadix/stringFromIntWithRadix выглядят громоздко и некрасиво.

Конструктор символа в 11l имеет следующие формы:
  • Char(code' 65) — создаёт символ с кодом 65 [т.е. соответствующий латинской букве A];
  • Char(digit' i) — создаёт символ, соответствующий цифре числа i (при этом число i должно быть неотрицательным и однозначным, т.е. от 0 до 9 включительно, иначе конструктор порождает исключение), является сокращённой формой записи выражения Char(code' ‘0’.code + i) [плюс проверка на принадлежность i диапазону 0..9];
  • Char(string' s) — создаёт символ из односимвольной строки s (если строка s пустая, либо содержит более одного символа, то порождается исключение). Может быть полезно когда символ создаётся из строки, полученной от пользователя [через консольный ввод, аргумент командной строки или из конфигурационного файла], чтобы не добавлять проверку на длину строки вручную.

Такие конструкторы повышают читаемость кода создания символа, полностью исключая неоднозначности: Char(code' 0) создаёт символ с кодом 0 [также можно использовать более короткую запись: "\0"], а Char(digit' 0) создаёт символ, соответствующий цифре 0 (т.е. с кодом 48), при этом Char(0) является ошибкой компиляции {так как Char(0) можно воспринимать двояко: с одной стороны в языках C/C++ char(0) означает символ с кодом 0, а с другой стороны String(0)/str(0) является строкой, состоящей из символа цифры 0, и было бы логично если бы Char(0) сохранял такое же поведение}.
Чем полезен отдельный тип для символа, и почему подход C++ [при котором char по факту совпадает с типом int8_t] является неудачным решением я подробно написал здесь.

Конструктор Int имеет следующие формы:
  • Int(Float f) — создаёт целое число из вещественного числа f путём отбрасывания дробной части;
  • Int(String s) — создаёт целое число из строки s;
  • Int(s, radix' base) — создаёт целое число из строки s с заданным основанием системы счисления;
  • Int(bytes' b) — создаёт целое число из массива байт b (порядок от младшего к старшему [little-endian]);
  • Int(bytes_be' b) — создаёт целое число из массива байт b (порядок от старшего к младшему [big-endian]).

Конструктор String имеет следующие формы:
  • String(o) — создаёт строковое представление объекта o;
  • String(i, radix' base) — создаёт строку из числа i с заданным основанием системы счисления.

Если бы в C++ можно было использовать именованные аргументы, тогда std::vector логичнее было бы конструировать так:
std::vector<int> a(size: 10); // вместо std::vector<int> a(10);
std::vector<int> b(reserve: 10); // ну или b(capacity: 10);

Метод split() у строк


Допустим, необходимо провести разбор BBCode-подобной разметки.
Вот некоторые поддерживаемые теги.
  • [b]жирный[/b]
  • [url]http://...[/url]
  • [url=http://...]ссылка[/url]
  • [img]http://...[/img]
  • [img=50,40]http://...[/img] (изображение с заданными шириной и высотой)
  • [color=red]красный[/color]
  • [color=255,0,0]тоже красный[/color]
  • [color=255,0,0,128]красный полупрозрачный[/color]
Разбор можно реализовать посимвольным сканированием размеченного текста. При обнаружении символа открывающей квадратной скобки необходимо найти соответствующую закрывающую скобку, а затем проверить, является ли подстрока, заключённая в квадратные скобки, допустимым тегом. Первое, что необходимо сделать — это разделить такую подстроку символом =. На Python это выглядит так:
tag_arr = tag_str.split('=', maxsplit = 1)
Обратите внимание на именованный аргумент maxsplit метода split. Значение 1 означает, что строка tag_str будет разбита только по первому найденному символу =, что позволяет корректно парсить теги вида [url=https://www.google.com/search?q=test].

[В 11l аналогом аргумента maxsplit из Python является limit, причём maxsplit = 1 соответствует limit' 2. Такое поведение присуще Ruby и PHP и видится мне более естественным.]

Затем необходимо проверить значение tag_arr[0]:
match tag_arr[0]:
    case 'b':
        ...
    case 'url':
        ...
    case 'img':
        if len(tag_arr) == 2:
            size = tag_arr[1].split(',')
            assert(len(size) == 2)
            sizex, sizey = map(int, size)
            ...
        ...
    case 'color':
        if len(tag_arr) != 2:
            raise TextParseError(...)
        color = tag_arr[1].split(',')
        assert(len(color) in (1, 3, 4))
        if len(color) == 1:
            ...
        else:
            color_components = list(map(int, color))
            ...

Пару строк на Python:
color = tag_arr[1].split(',')
assert(len(color) in (1, 3, 4))
можно объединить в одну строку на 11l, используя именованный аргумент req:
var color = tag_arr[1].split(‘,’, req' (1, 3, 4))
При этом возможна оптимизация на основании того факта, что максимально возможная длина массива строк [либо массива из StringView], возвращаемого методом split, равна 4. Другими словами, такой вызов метода split может возвращать не динамический массив, а статический, память под который выделяется на стеке.

Аналогично, эти 3 строки на Python:
size = tag_arr[1].split(',')
assert(len(size) == 2)
sizex, sizey = map(int, size)
можно объединить в одну строку на 11l:
var (sizex, sizey) = tag_arr[1].split(‘,’, req' 2).map(Int)

Вообще говоря, использование assert-ов [как явных, так и неявных внутри метода split(..., req' ...)] в коде парсера — не самая лучшая идея, но вполне жизнеспособная: можно просто завернуть код парсинга тегов в блок try-catch и при возникновении исключения в development-сборке показывать MessageBox с сообщением об ошибке [с кнопками ‘Продолжить’ и ‘Debug break’], а в финальной сборке для конечных пользователей игнорировать неправильный тег, либо подкрашивать его красным цветом. [При этом падать приложение из-за ошибки в теге, разумеется, не должно в любом случае.]
Можно, конечно, аккуратно обработать все возможные ошибки в тегах {но основная проблема тут даже не в дополнительном коде обработки ошибок, а в придумывании хороших понятных текстов сообщений об ошибках :)(:}, но особого смысла в таких дополнительных проверках в коде я не вижу — документация по всем поддерживаемым парсером тегам должна быть обязательно, и вполне достаточно указания того, в каком именно теге размеченного текста присутствует ошибка — беглого взгляда на описание данного тега в документации будет, как правило, достаточно для нахождения и исправления ошибки.

Кроме limit и req вариантов метода split, в 11l есть ещё String.split(d, ', max) [который по сути равнозначен String.split(d, req' 1..max)] и String.split(d, ', first), который возвращает только первые first элементов массива строк.

Разумеется, можно было просто добавить методы split_limit(), split_req(), split_max() и split_first(), но перегруженная split() с именованными аргументами всё-таки красивее.

Функции min() и max()


Также как в Python, функции min() и max() в 11l имеют опциональный именованный аргумент key.
Следующий код выведет самую длинную строку в массиве arr:
var arr = [‘a’, ‘bc’]
print(max(arr, key' s -> s.len))

Именованные аргументы/параметры шаблонов


В попытках добавить класс статического массива [массива, максимальная длина которого известна на этапе компиляции] в 11l, я сделал неожиданное открытие. А именно то, что шаблонные параметры могут быть именованными аналогично аргументам функций. [Впрочем, поиском ‘named template parameters’ выяснилось, что не мне первому пришла в голову эта идея.]

Вот пример объявления статического массива в 11l:
Array[Int, max_len' 10] static_array
[Int, max_len' 10] static_array2 // сокращённая форма [тип такой же]

И аналогично, массив фиксированного размера:
Array[Int, len' 10] fixed_array
[Int, len' 10] fixed_array2 // сокращённая форма [тип такой же]

[Я предпочитаю len, а не size, т.к. последний у меня ассоциируется с размером в байтах (например, в языке Си sizeof(a) для массива из 10 целых 32-разрядных чисел возвращает 40, а не 10).]

Кроме того, массив фиксированного размера, инициализированный элементами, можно объявить так:
var fixed_array1 = -[1, 2, 3]
var fixed_array2 = -[0] * 10 // равнозначно -[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Зачем вообще нужен статический массив и массив фиксированного размера, когда есть более универсальный динамический массив?
Чтобы не выделять память динамически под массив небольшого размера. Особенно это полезно в критичных к производительности местах кода, а также просто в целях удобства: например для задания треугольника удобно использовать именно массив фиксированного размера, который состоит из 3-х элементов, задающих координаты вершин треугольника.

Стоит заметить, что статический массив в 11l является более продвинутым, чем std::array или boost::array в C++ (по сути, он является аналогом TArray<T, TFixedAllocator<MaxLen>> в Unreal Engine): статический массив в 11l позволяет добавлять и удалять элементы, и поддерживает практически все методы (все, за исключением метода reserve) и операторы, применимые к обычным динамическим массивам.

Заключение


Действительно полезных применений перегрузки на основе имён аргументов в моём опыте встретилось, как видите, совсем немного, и было бы здорово увидеть ещё хотя бы несколько штук, а также обсудить полезность этой возможности языка программирования, который вы используете [или необходимость добавления этой возможности, если ваш язык её ещё не поддерживает].

Комментарии (1)


  1. blood_develop
    31.07.2023 21:54

    А потом находишь такое:

    ctor(int a){\**\}
    ctor(int b){\**\}

    И фиг знает какой нужен. Пример выдуманный, на основе того, как реализовано апи iTextSharp работы с пдф - ни описания, ни нормальных именований.

    Я не спорю, возможность крутая, но я лучше корявым английским назову метод и добавлю комент. Мне кажется, что проблем от возможности иметь одинаковые сигнатуры с разным именем параметров больше будет