Продвинутые операторы
В дополнение к операторам, которые мы рассматривали в главе Базовые операторы, Swift предоставляет нам еще несколько продвинутых операторов, которые позволяют нам проводить более сложные манипуляции со значениями. Они включают в себя побитовые и операторы разрядного смещения, с которыми вы возможно знакомы из языков C или Objective-C.
В отличии от арифметических операторов C, арифметические операторы в Swift не переполняются по умолчанию. Переполнения отслеживаются и выводятся как ошибка. Для того, чтобы этого избежать, вы можете использовать оператор из второго набора арифметических операторов Swift (&+). Все операторы переполнения начинаются с амперсанда (&).
Когда вы определяете ваши собственные структуры, классы или перечисления, то может быть полезным обеспечивать ваши собственные реализации стандартных операторов Swift, для этих пользовательских типов. Swift позволяет создавать адаптивные реализации этих операторов, так что вы можете определить их поведения для каждого конкретного типа, который вы создаете.
Вы не ограничены в предопределенных операторах. Swift дает вам свободу определять ваши собственные префиксные, инфиксные, постфиксные операторы и операторы присваивания, которым вы можете задавать собственный приоритет и ассоциативность значений. Эти операторы могут быть использованы и приняты вашим кодом, как и любой другой предопреленный оператор, вы так же можете расширить уже существующие типы, для того, чтобы они могли поддерживать ваши пользовательские операторы.
Побитовые операторы
Побитовые операторы позволяют вам манипулировать отдельными битами необработанных данных внутри структуры данных. Они часто используются в низкоуровневом программировании, например программирование графики или создание драйвера для устройства. Побитовые операторы также могут быть полезны, когда вы работаете с необработанными данными из внешних ресурсов, например, кодирование или декодирование данных для связи через собственный протокол.
Swift поддерживает все побитовые операторы, которые были основаны в C о которых мы поговорим далее.
Побитовый оператор NOT
Побитовый оператор NOT (~) инвертирует все битовые числа:
Побитовый оператор NOT является префиксным оператором и ставится прямо перед значением (без пробела), над которым он оперирует.
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // равен 11110000
Целые числа типа UInt8 имеют восемь бит и могут хранить значения от 0 до 255. В этом примере инициализируем число типа UInt8, которое имеет бинарное значение 00001111, которое имеет первые четыре бита равные 0, а вторая четверка битов равна 1. Это эквивалент числа 15.
Далее используем побитовый оператор NOT для создания новой константы invertedBits, которая равна initialBits, но только с перевернутыми битами. То есть теперь все единицы стали нулями, а нули единицами. Значение числа invertedBits равно 11110000, что является эквивалентом 240.
Побитовый оператор AND
Побитовый оператор AND (&) комбинирует два бита двух чисел. Он возвращает новое число, чье значение битов равно 1, если только оба бита из входящих чисел были равны 1:
В примере ниже, значения firstSixBits и lastSixBits имеют четыре бита по середине равными 1. Побитовый оператор AND комбинирует их для создания числа 00111100, которое равно беззнаковому целому числу 60:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // равен 00111100
Побитовый оператор OR
Побитовый оператор OR (|) сравнивает биты двух чисел. Оператор возвращает новое число, чьи биты устанавливаются на 1, если один из пары битов этих двух чисел имеет бит равный 1:
В примере ниже значения someBits и moreBits имеют разные биты со значениями 1. Побитовый оператор OR комбинирует их для создания числа 11111110, что равно беззнаковому целому числу 254:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // равен 11111110
Побитовый оператор XOR
Побитовый оператор XOR или “оператор исключающего OR” (^), который сравнивает биты двух чисел. Оператор возвращает число, которое имеет биты равные 1, когда биты входных чисел разные, и возвращает 0, когда биты одинаковые:
В примере ниже, значения firstBits и otherBits каждый имеет один бит в том месте, где у другого 0. Побитовый оператор XOR устанавливает оба этих бита в качестве выходного значения. Все остальные биты повторяются, поэтому оператор возвращает 0:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // равен 00010001
Операторы побитового левого и правого сдвига
Оператор побитового левого сдвига (<<) и оператор побитового правого сдвига (>>) двигают все биты числа влево или вправо на определенное количество мест, в зависимости от правил, которые определены ниже.
Побитовые операторы левого и правого сдвига имеют эффект умножения или деления числа на 2. Сдвигая биты целого числа влево на одну позицию, мы получаем удвоенное первоначальное число, в то время как, двигая его вправо на одну позицию, мы получаем первоначальное число поделённое на 2.
Поведение сдвига для беззнаковых целых чисел
Поведение побитового сдвига имеет следующие правила:
- Существующие биты сдвигаются вправо или влево на требуемое число позиций.
- Любые биты, которые вышли за границы числа, отбрасываются.
- На пустующие позиции сдвинутых битов вставляются нули.
Такой подход называется логическим сдвигом.
Иллюстрация внизу отображает результат смещения 11111111 << 1 (что означает 11111111 сдвинутые влево на 1), и 11111111 >> 1 (что означает 11111111 сдвинутые на 1 вправо). Голубые цифры - сдвинутые, серые - отброшенные, оранжевые - вставленные:
Вот как выглядит побитовый сдвиг в виде Swift кода:
let shiftBits: UInt8 = 4 // 00000100 бинарный вид
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001
Вы можете использовать побитовый сдвиг для кодирования и декодирования значений внутри других типов данных:
let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16 // redComponent равен 0xCC, или 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent равен 0x66, или 102
let blueComponent = pink & 0x0000FF // blueComponent равен 0x99, или 153
Этот пример использует UInt32, который называется pink, для хранения значения розового цвета из файла CSS. Значение розового цвета #CC6699, что записывается в виде шестнадцатеричном представлении Swift как 0xCC6699. Этот цвет затем раскладывается на его красный(CC), зеленый (66) и голубой (99) компоненты при помощи побитового оператора AND (&) и побитового оператора правого сдвига (>>).
Красный компонент получен с помощью побитового оператора AND между числами 0xCC6699 и 0xFF0000. Нули в 0xFF0000 фактически являются “маской” для третьего и четвертого бита в 0xCC6699, тем самым заставляя игнорировать 6699, и оставляя 0xCC0000 в качестве результата.
После этого число сдвигается на 16 позиций вправо (>> 16). Каждая пара символов в шестнадцатеричном числе использует 8 битов, так что сдвиг вправо на 16 позиций преобразует число 0xCC0000 в 0x0000CC. Это то же самое, что и 0xCC, которое имеет целое значение равное 204.
Аналогично с зеленым компонентом, который получается путем использования побитового оператора AND между числами 0xCC6699 и 0x00FF00, который в свою очередь дает нам выходное значение 0x006600. Это выходное значение затем сдвигается на восемь позиций вправо, давая нам значение 0x66, что имеет целое значение равное 102.
Ну а теперь последний синий компонент, который получается при использовании побитового оператора AND между числами 0xCC6699 и 0x0000FF, что в свою очередь дает нам выходное значение равное 0x000099. Таким образом, нам не нужно сдвигать это вправо, так как 0x000099 уже равно 0x99, что имеет целое значение равное 153.
Поведение побитового сдвига для знаковых целых чисел
Поведение побитового сдвига для знаковых целых чисел более сложное, чем для беззнаковых, из-за того, как они представлены в бинарном виде. (Пример ниже основан на восьми битовом знаковом целом числе для простоты примера, однако этот принцип применим к знаковым целым числам любого размера).
Знаковые целые числа используют первый бит (известный как знаковый бит) для индикации того, является ли число положительным или отрицательным. Значение знакового бита равное 0 свидетельствует о положительном числе, 1 - отрицательном.
Остальные биты (известные как биты значения) хранят фактическое значение. Положительные числа хранятся в точности так же как и беззнаковые целые числа, считая от 0. Вот как выглядят биты внутри Int8 для числа 4:
Знаковый бит равен 0 (число положительное), остальные семь битов означают число 4, записанное в бинарной форме.
Однако отрицательные числа хранятся иначе. Они хранятся путем вычитания их абсолютного значения из 2 в степени n, где n - количество битов значения.
Вот как выглядит биты внутри Int8 для числа -4:
В этот раз, знаковый бит равен 1 (число отрицательное), а остальные семь знаковых бита имеют бинарное значение числа 124 (что означает 128 - 4):
Кодирование отрицательных чисел известно под названием дополнительный код. Это может показаться необычным способом для отображения отрицательных чисел, но в нем есть несколько преимуществ.
Первое. Вы можете добавить -1 к -4, просто выполняя стандартное сложение всех восьми битов (включая и восьмой бит), и отбрасывая все, что не поместится в ваши восемь бит:
Второе. Представление “дополнительного кода” также позволяет вам сдвигать биты отрицательных чисел влево и вправо, как в случае с положительными, и все так же умножая их при сдвиге влево или уменьшая их в два раза, при сдвиге на 1 место вправо. Для того чтобы обеспечить такое поведение при движении знаковых чисел вправо, мы должны применить дополнительное правило: Когда вы сдвигаете знаковое число вправо, используйте то же самое правило, что и для беззнаковых чисел, но заполняйте освободившиеся левые биты знаковыми битами, а не нулями.
Эти действия гарантируют вам, что знаковые числа имеют тот же знак, после того как они сдвинуты вправо, и эти действия известны как арифметический сдвиг.
Из-за такого способа хранения положительных и отрицательных чисел, сдвиг вправо двигает их значение ближе к нулю. Оставляя знаковый бит тем же самым в течение побитового сдвига, означает, что ваше отрицательное число, так же и остается отрицательным, в то время как его значение так же движется к нулю.
Операторы переполнения
Если вы попытаетесь ввести число в целочисленную константу или переменную, которая не может держать это число, то по умолчанию Swift выдаст сообщение об ошибке, а не будет создавать недействительное значение. Это поведение дает дополнительную безопасность, когда вы работаете с числами, которые слишком велики или слишком малы.
Для примера, целочисленный тип Int16 может держать любое знаковое целое число от -32768 и до 32767. Если вы попытаетесь установить число (константу или переменную) типа Int16 за границами приведенного диапазона, то вы получите ошибку:
var potentialOverflow = Int16.max
// potentialOverflow равняется 32767, что является самым большим значением, которое может содержаться в Int16
potentialOverflow += 1
// это вызовет ошибку
Обеспечивая обработку ошибки, когда значение является слишком большим или слишком маленьким, вы получаете намного большую гибкость, для кодирования краевых условий.
Однако, когда вы специально хотите осуществить условие переполнения, чтобы обрезать количество доступных битов, то вы можете получить именно такое поведение, вместо отчета об ошибке переполнения. Swift предоставляет три арифметических оператора переполнения, которые помогают перейти к поведению переполнения для целочисленных вычислений. Все эти операторы начинаются с символа амперсанда (&):
- Оператор переполнения с добавлением (&+)
- Оператор переполнения с вычитанием (&-)
- Оператор переполнения с умножением (&*)
Переполнение значения
Числа могут переполняться как в положительную, так и в отрицательную сторону.
Ниже приведен пример того, что случится, когда беззнаковое значение позволяет переполнить себя, с использованием оператора (&+):
var willOverflow = UInt8.max
// willOverflow равняется 255, что является наибольшим числом, которое может держать UInt
willOverflow = willOverflow &+ 1
// willOverflow теперь равно 0
Переменная willOverflow инициализирована самым большим числом, которое может держать UInt8 (255 или в бинарном виде 11111111). Затем оно увеличивается на 1 при помощи оператора переполнения (&+). Это выталкивает бинарное представление размерности UInt8, вызывая тем самым переполнение границ, что отображено на диаграмме ниже. Значение, которое остается в пределах границ значения типа UInt8 после переполнения и добавления выглядит как 00000000, или попросту 0 в десятичной форме:
Числа так же могут быть слишком маленькими, чтобы соответствовать определенному типу. Ниже приведен пример с использованием оператора недополнения (&-)
var unsignedOverflow = UInt8.min
// unsignedOverflow равен 0, что является наименьшим возможным значением UInt8
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow теперь равно 255
Самое маленькое значение, которое может держать UInt8 равно 0 (что отображается как 00000000 в восьмибитной бинарной форме). Если вы из 00000000 вычтите 1, с использованием оператора недополнения, число переполнится в обратную сторону к 11111111, или к 255 в десятичной форме:
Аналогичное недополнение случается и с знаковыми целыми числами. Все вычитание для знаковых целых чисел проводится как прямое бинарное вычитание с учетом знакового бита, в качестве части вычитаемых чисел, что описано в Операторы побитового левого и правого сдвига.
var signedUnderflow = Int8.min
// signedUnderflow равняется -128, что является самым маленьким числом, которое может держать Int8
signedUnderflow = signedUnderflow &- 1
// signedUnderflow теперь равняется 127
Самым маленьким числом, которое может держать Int8, является –128, что записывается в бинарной форме как 10000000. Вычитая 1 из этого бинарного числа с оператором недополнения, дает нам значение 01111111, что переключает наш знаковый бит на противоположный и дает нам положительное 127, что является самым большим числом, которое может держать Int8:
Конечный результат поведения переполнения и недополнения описан выше и одинаково работает как для знаковых, так и для беззнаковых целых чисел. Переполнение всегда переворачивает значение с самого большого на самое маленькое, недополнение всегда переворачивает самое маленькое число на самое большое.
Приоритет и ассоциативность
Оператор приоритета дает некоторым операторам более высокий приоритет по сравнению с остальными. В выражении сначала применяются эти операторы, затем все остальные.
Оператор ассоциативности определяет то, как операторы одного приоритета сгруппированы вместе (или ассоциированы друг с другом), то есть либо они сгруппированы слева, либо справа. Думайте об этом как “они связаны с выражением слева” или “они связаны с выражением справа”.
Это важно учитывать приоритет и ассоциативность каждого оператора, когда работаете с порядком, в котором должно считаться выражение. Вот простой пример. Почему данное выражение имеет равенство 17?
2 + 3 % 4 * 5
// это равно 17
Если вы прочитаете это выражение строго слева направо, то вы можете ожидать действия в таком порядке:
-
-
- 2 плюс 3 равняется 5
- 5 остаток от деления на 4 равен 1
- 1 умножаем на 5 и получаем 5
-
Однако, как не крути, правильный ответ равен 17, а не 5. Операторы более высокого приоритета выполняются раньше операторов более низкого приоритета. В Swift, как и в C, оператор умножения (*) и оператор остатка (%) имеют более высокий приоритет, чем оператор сложения (+). В результате они оба вычисляются раньше, чем вычисляется оператор сложения.
Однако оператор умножения и оператор остатка имеют один и тот же приоритет по отношению друг к другу. Для выяснения точного порядка вычисления вы должны обратиться к их ассоциативности. Операторы умножения и остатка оба ассоциируются с выражением слева от себя. Представляйте это, как будто вы добавили скобки вокруг этих частей выражения, начиная слева:
2 + ((3 % 4) * 5)
(3 % 4) равно 3, значит можно записать:
2 + (3 * 5)
(3 * 5) равно 15, и значит мы можем записать:
2 + 15
Таким образом наш конечный результат равен 17.
Для получения информации об операторах, предоставляемых стандартной библиотекой Swift, включая полный список групп приоритета операторов и параметров ассоциативности, см. Объявление операторов.
Заметка
Правила приоритета и ассоциативности операторов Swift проще и более предсказуемые чем в C или Objective-C. Однако это означает, что они ведут себя не так же как они вели себя в этих C-языках. Будьте внимательны с тем, как ведут себя операторы взаимодействия при переносе кода в Swift.
Операторные функции
Классы и структуры могут предоставлять свои собственные реализации существующих операторов. Действие переопределения оператора известно как перегрузка существующего оператора.
Пример ниже отображает как можно реализовать арифметический оператор сложения (+) для пользовательской структуры. Арифметический оператор сложения является бинарным оператором, потому что он оперирует с двумя операндами, то есть он является инфиксным, потому как вставляется между двумя операндами.
Пример определяет структуру Vector2D для двухмерного вектора положения (x, y), за которой идет статическая функция, которая добавляет друг к другу экземпляры структуры Vector2D:
struct Vector2D {
var x = 0.0, y = 0.0
}
extension Vector2D {
static func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
Операторный метод определен как метод типа Vector2D с именем метода, которое совпадает с именем оператора, который перегружают (+). Так как сложение не является неотъемлемой частью поведения вектора, метод типа определен в расширении нашего Vector2D, а не в основной структуре Vector2D. Так как арифметический оператор сложения является бинарным оператором, то этот оператор принимает два параметра типа Vector2D и возвращает единственное выходное значение, которое тоже имеет тип Vector2D.
В этой реализации входные параметры имеют имена left и right, для отображения экземпляров Vector2D, которые будут по левую и по правую сторону от оператора +. Функция возвращает новый экземпляр Vector2D, x и y которого инициализированы суммой свойств x и y из двух экземпляров Vector2D, которые были добавлены друг другу.
Метод типа может использоваться как инфиксный оператор между существующими экземплярами Vector2D:
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector является экземпляром Vector2D, который имеет значения (5.0, 5.0)
Этот пример складывает два вектора вместе (3.0, 1.0) и (2.0, 4.0) для создания вектора (5.0, 5.0), который нарисован ниже:
Префиксные и постфиксные операторы
Пример, отображенный выше, демонстрирует пользовательскую реализацию бинарного инфиксного оператора. Классы и структуры так же могут обеспечивать реализацию стандартных унарных операторов. Унарные операторы работают с одним операндом. Они бывают префиксными, если они предшествуют их операнду (например, -a) или постфиксными, если они следуют за операндом (например b!).
Вы реализуете префиксный или постфиксный унарный оператор при помощи модификаторов prefix или postfix перед ключевым словом func, когда объявляете операторную функцию:
extension Vector2D {
static prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
}
Пример выше реализует унарный оператор (-a) для экземпляров Vector2D. Оператор унарного минуса является префиксным оператором, таким образом эта функция должна быть модифицирована при помощи prefix модификатора.
Для простых числовых значений оператор унарного минуса конвертирует положительные числа в их негативный эквивалент и наоборот. Соответствующая реализация для экземпляров Vector2D проводит операции и на x, и на y свойствах:
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative - экземпляр Vector2D со значениями (-3.0, -4.0)
let alsoPositive = -negative
// alsoPositive - экземпляр Vector2D со значениями (3.0, 4.0)
Составные операторы присваивания
Составные операторы присваивания комбинируют оператор присваивания (=) с другим оператором. Например, оператор сложения-присваивания (+=) комбинирует в себе оператор добавления и оператор присваивания. Вы обозначаете левый входной параметр составного оператора как inout, потому что именно эта величина и будет изменена напрямую изнутри самой операторной функции.
Пример ниже реализует операторную функцию добавления-присваивания для экземпляров Vector2D:
extension Vector2D {
static func += (left: inout Vector2D, right: Vector2D) {
left = left + right
}
}
Так как оператор сложения был определен ранее, то вам не нужно реализовывать процесс сложения здесь. Вместо этого оператор сложения-присваивания использует существующую операторную функцию сложения и использует ее для установки нового значения левому значению, как сумму левого и правого значений:
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original теперь имеет значения (4.0, 6.0)
Заметка
Нет такой возможности перегрузить оператор присваивания (=). Только составные операторы могут быть перегружены. Тернарный оператор (a ? b : c) так же не может быть перегружен.
Операторы эквивалентности
Пользовательские классы и структуры не получают дефолтной реализации эквивалентных операторов, известных как “равен чему-то” оператор (==) или “не равен чему-то” (!=).
Чтобы использовать операторы эквивалентности для проверки эквивалентности вашего собственного пользовательского типа, предоставьте реализацию для этих операторов тем же самым способом, что и для инфиксных операторов и добавьте соответствие протоколу стандартной библиотеки Equatable:
extension Vector2D: Equatable {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
}
Пример выше реализует оператор “равен чему-то” (==) для проверки эквивалентности значений двух экземпляров Vector2D. В контексте Vector2D имеет смысл считать, что “равно чему-то” означает, что “оба экземпляра имеют одни и те же значения x и y”, таким образом это является той логикой, которая используется при реализации оператора. Пример так же реализует оператор “не равен чему-то” (!=), который просто возвращает обратный результат оператора “равен чему-то”.
Теперь вы можете использовать эти операторы для проверки того, эквивалентны ли экземпляры Vector2D друг другу или нет:
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
print("Эти два вектора эквиваленты.")
}
// Выведет "Эти два вектора эквиваленты."
Swift предоставляет синтезированные реализации операторов эквивалентности для следующих пользовательских типов:
- Структур, имеющих только свойства хранения, соответствующие протоколу Equatable
- Перечислений, имеющих только ассоциированные типы, соответствующие протоколу Equatable
- Перечислений, не имеющих связанных типов
Объявите о соответствии протоколу Equatable в исходной реализации для получения этих дефолтных реализаций.
Приведенный ниже пример определяет структуру Vector3D для трехмерного вектора положения (x, y, z), аналогичную структуре Vector2D. Поскольку свойства x, y и z являются эквивалентными, Vector3D принимает стандартные реализации операторов эквивалентности.
struct Vector3D: Equatable {
var x = 0.0, y = 0.0, z = 0.0
}
let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
print("These two vectors are also equivalent.")
}
// Выведет "These two vectors are also equivalent."
Пользовательские операторы
Вы можете объявить и реализовать ваши собственные пользовательские операторы в дополнение к стандартным операторам Swift. Список символов, которые можно использовать для определения пользовательских операторов, см. в разделе «Операторы».
Новые операторы объявляются на глобальном уровне при помощи ключевого слова operator и отмечаются модификатором prefix, infix, postfix:
prefix operator +++
Пример выше определяет новый префиксный оператор +++. Этот оператор не имеет никакого значения в Swift, таким образом мы даем ему собственное назначение, которое описано чуть ниже, которое имеет специфический контекст работы с экземплярами Vector2D. Для целей этого примера, оператор +++ рассматривается как новый “префиксный двойной” оператор. Он удваивает значения x и y экземпляра Vector2D, путем добавления вектора самому себе при помощи оператора сложения-присваивания, который мы определили ранее. Для реализации +++ оператора мы добавим метод типа Vector2D с именем +++ как показано ниже:
extension Vector2D {
static prefix func +++ (vector: inout Vector2D) -> Vector2D {
vector += vector
return vector
}
}
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled имеет значения (2.0, 8.0)
// afterDoubling так же имеет значения (2.0, 8.0)
Приоритет для пользовательских инфиксных операторов
Каждый пользовательский infix оператор принадлежит к своей приоритетной группе. Группа приоритета определяет приоритет оператора по отношению к другим инфиксным операторам, так же как и ассоциативность оператора. Посмотрите главу Приоритет и ассоциативность для объяснения того, как эти две характеристики влияют на взаимодействие инфиксных операторов между собой.
Пользовательскому инфиксному оператору, который явно не размещен в приоритетной группе, предоставляется дефолтная группа приоритета, которая является по приоритету следующей после тернарного условного оператора.
Следующий пример определяет новый инфиксный оператор +- левой ассоциативности и с приоритетом AdditionPrecedence:
infix operator +-: AdditionPrecedence
extension Vector2D {
static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector является экземпляром Vector2D со значениями (4.0, -2.0)
Этот оператор складывает значения x двух векторов и вычитает значение y второго вектора из значения y первого вектора. Так как этот оператор в сущности является оператором “сложения”, то приоритет будет равным операторам сложения (+) и вычитания (-). Для получения информации об операторах, представленных стандартной библиотекой Swift, включая список настроек ассоциативности и групп приоритета смотрите раздел "Объявления операторов". Для более полной информации по группам приоритета и для ознакомления с синтаксисом создания своих собственных операторов и групп ассоциативности смотрите "Объявление оператора".
Заметка
Вы не указываете приоритет, когда определяете префиксный и постфиксные операторы. Однако, если вы воздействуете на операнд сразу двумя операторами (префиксным и постфиксным), то первым будет применен постфиксный оператор.
Result Builders
Result Builder - это определяемый вами тип, который добавляет синтаксис для создания вложенных данных, таких как список или дерево, естественным, декларативным образом. Код, использующий result builder, может включать обычный синтаксис Swift, например if и for, для обработки условных или повторяющихся фрагментов данных.
В приведенном ниже коде определены несколько типов рисования на одной линии с использованием звездочек и текста.
protocol Drawable {
func draw() -> String
}
struct Line: Drawable {
var elements: [Drawable]
func draw() -> String {
return elements.map { $0.draw() }.joined(separator: "")
}
}
struct Text: Drawable {
var content: String
init(_ content: String) { self.content = content }
func draw() -> String { return content }
}
struct Space: Drawable {
func draw() -> String { return " " }
}
struct Stars: Drawable {
var length: Int
func draw() -> String { return String(repeating: "*", count: length) }
}
struct AllCaps: Drawable {
var content: Drawable
func draw() -> String { return content.draw().uppercased() }
}
Протокол Drawable определяет требования к чему-то, что можно нарисовать, например линии или фигуре: тип должен реализовывать метод draw(). Структура Line представляет собой однолинейный рисунок и служит контейнером верхнего уровня для большинства рисунков. Чтобы нарисовать линию, структура вызывает draw() для каждого из компонентов линии, а затем объединяет результирующие строки в одну строку. Структура Text оборачивает строку, чтобы сделать ее частью рисунка. Структура AllCaps оборачивает и изменяет другой рисунок, преобразуя любой текст в рисунке в верхний регистр.
Можно создать рисунок при помощи этих типов, вызвав их инициализаторы.
let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
Stars(length: 3),
Text("Hello"),
Space(),
AllCaps(content: Text((name ?? "World") + "!")),
Stars(length: 2),
])
print(manualDrawing.draw())
// Выведет "***Hello RAVI PATEL!**"
Этот код работает, но немного неудобен. Глубоко вложенные круглые скобки после AllCaps трудночитаемы. Логика отката для использования «World», когда name равно nil, должна быть встроена с использованием оператора ??, что было бы сложно использовать в более сложных кейсах. Если вам нужно было встроить инструкцию switch или цикл for для создания рисунка, то это было бы просто невозможно. Result Builder позволяет вам переписать такой код, чтобы он выглядел как обычный код на Swift.
Чтобы определить Result Builder, вы пишете атрибут @resultBuilder в объявлении типа. Например, этот код определяет Result Builder под названием DrawingBuilder, который позволяет вам использовать декларативный синтаксис для описания рисунка:
@resultBuilder
struct DrawingBuilder {
static func buildBlock(_ components: Drawable...) -> Drawable {
return Line(elements: components)
}
static func buildEither(first: Drawable) -> Drawable {
return first
}
static func buildEither(second: Drawable) -> Drawable {
return second
}
}
Структура DrawingBuilder определяет три метода, которые реализуют части синтаксиса Result Builder. Метод buildBlock(_ :) добавляет поддержку записи серии строк в блоке кода. Он объединяет компоненты в этом блоке в линию. Методы buildEither(first :) и buildEither(second :) добавляют поддержку if-else.
Вы можете применить атрибут @DrawingBuilder к параметру функции, который превращает закрытие, переданное в функцию, в значение, которое построитель результатов создает из этого закрытия. Например:
func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
return content()
}
func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
return AllCaps(content: content())
}
func makeGreeting(for name: String? = nil) -> Drawable {
let greeting = draw {
Stars(length: 3)
Text("Hello")
Space()
caps {
if let name = name {
Text(name + "!")
} else {
Text("World!")
}
}
Stars(length: 2)
}
return greeting
}
let genericGreeting = makeGreeting()
print(genericGreeting.draw())
// Выведет "***Hello WORLD!**"
let personalGreeting = makeGreeting(for: "Ravi Patel")
print(personalGreeting.draw())
// Выведет "***Hello RAVI PATEL!**"
Функция makeGreeting(for :) принимает параметр имени и использует его для рисования персонализированного приветствия. Функции draw(_ :) и caps(_ :) принимают в качестве аргумента одно закрытие, которое помечается атрибутом @DrawingBuilder. Когда вы вызываете эти функции, вы используете специальный синтаксис, который определяет DrawingBuilder. Swift преобразует это декларативное описание рисунка в серию вызовов методов в DrawingBuilder для создания значения, переданного в качестве аргумента функции. Например, Swift преобразует вызов caps(_ :) в этом примере в код, подобный следующему:
let capsDrawing = caps {
let partialDrawing: Drawable
if let name = name {
let text = Text(name + "!")
partialDrawing = DrawingBuilder.buildEither(first: text)
} else {
let text = Text("World!")
partialDrawing = DrawingBuilder.buildEither(second: text)
}
return partialDrawing
}
Swift преобразует блок if-else в вызовы методов buildEither(first :) и buildEither(second :). Хотя вы не вызываете эти методы в собственном коде, отображение результата преобразования позволяет легче увидеть, как Swift преобразует ваш код, когда вы используете синтаксис DrawingBuilder.
Чтобы добавить поддержку записи для циклов в специальном синтаксисе рисования, добавьте метод buildArray(_ :).
extension DrawingBuilder {
static func buildArray(_ components: [Drawable]) -> Drawable {
return Line(elements: components)
}
}
let manyStars = draw {
Text("Stars:")
for length in 1...3 {
Space()
Stars(length: length)
}
}
В приведенном выше коде цикл for создает массив рисунков, а метод buildArray(_ :) превращает этот массив в Line.