Swift как ссылаются типы в памяти

Обновлено: 04.07.2024

На WWDC 2014 Apple представила одно из самых больших обновлений для iOS с 2008 года с точки зрения разработчика. Они представили HomeKit, HealthKit, CloudKit и расширения, просто чтобы назвать несколько. Но самым большим сюрпризом для WWDC 2014 стало внедрение совершенно нового языка программирования Swift.

Swift - замечательный язык программирования, который был построен с нуля для эффективности и безопасности. Он использует те же API, что и Objective-C. Или, что вы можете сделать в Objective-C, вы можете сделать в Swift. В нем также представлены некоторые новые концепции, которые оценят опытные программисты, и некоторые из которых я расскажу в этой вводной серии о Swift.

В этой серии я предполагаю, что вы уже знакомы с Objective-C. В первой статье этой серии я расскажу о философии Swift, структуре файла и синтаксисе. Во второй статье я перейду к более продвинутым аспектам синтаксиса Swift, таким как опции и управление памятью. Приготовьтесь, ребята, это будет незабываемым.

1. Философия

Чтобы лучше понять Swift, Apple за последние несколько лет создала нам условия для структурных улучшений в Objective-C. Улучшения Objective-C, такие как кодовые блоки, литеральные массивы и словарные определения, и ARC (Automatic Reference Counting) - это всего лишь несколько вещей Apple, добавленых в Objective-C, что облегчает переход на Swift.

Одним из важных элементов философии Swift является инициализация кода. Все объекты и переменные, определенные в Swift, должны быть инициализированы в коде. Неинициализированный объект или переменная приведет к ошибке времени компиляции в Swift. Это гарантирует, что объект или переменная всегда имеет значение. В Swift есть один специальный случай, когда начальное значение не может быть определено. В этом специальном случае ваша переменная называется optional. Мы рассмотрим варианты во второй части этой серии.

Еще одним важным компонентом является отраслевая полнота. Все условные ветви, будь то if или switch/case , должны охватывать все условия. Таким образом, ни один код не может потерпеть неудачу без его охвата. Отсутствие условия в вашей ветке утверждения будет установлено и сгенерирует ошибку времени компиляции.

Одним из последних элементов философии Swift является его предпочтение константам перед переменными. Swift определяет переменные и константы следующим образом:

В приведенном выше примере ключевое слово let используется для определения константы, в то время как ключевое слово var определяет переменную. Сделав определение констант таким простым, Apple по возможности поощряет использование констант. Это приводит к более безопасному коду в многопоточной среде и лучшей оптимизации кода, поскольку компилятор знает, что значение константы не изменится.

  • индексы вне диапазона в массивах
  • неинициализированные данные
  • непроверенные типы возврата
  • непроверенный доступ к указателю
  • неявное падение
  • ошибки goto

Как тот, кто работает как для iOS, так и для Android, я знаю из первых рук, насколько интереснее кодирование для платформы iOS на самом деле с Cocoa и UIKit. Эта серия покажет вам, насколько более интересным может быть кодирование, добавив Swift в микс.

2. Структура файла

В Objective-C у нас есть файлы заголовков (.h) и файлы реализации (.m). Это то, что Objective-C, унаследовал от языка C.

3. Синтаксис

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

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

Еще одно важное изменение в Swift заключается в том, что фигурные скобки являются обязательными для операторов if . Это означает, что больше нет Heartbleedbugs.

Синтаксис может быть сложным предметом для написания. У Swift есть много тонкостей, которые могут занять очень много времени, но это не является целью этой статьи. Для краткости я сосредоточу внимание на изменениях, которые бы заметил разработчик Objective-C.

4. Сходство с Objective-C

Я начну с отображения трех фрагментов кода, которые иллюстрируют некоторые сходства с Objective-C. Это поможет вам в понимании языка Swift.

Программисты Objective-C обнаруживают, что Swift имеет те же самые инструкции веток и итераций, с которыми вы уже знакомы, например, как if/else , for циклов, for..in циклов, и switch утверждения.

Swift включает двух операторов диапазона, ..< и . , чтобы указать диапазон значений. В вышеприведенном цикле for мы используем оператор полузамкнутого диапазона, ..< , чтобы указать диапазон значений, который включает в себя 1, 2, 3 и 4, но исключает 5. Другим оператором диапазона является оператор замкнутого диапазона, . . Он задает диапазон значений, который включает значение с обеих сторон оператора замкнутого диапазона. Например, 1. 5 . задает диапазон значений от 1 до 5, включая 5.

5. Определение переменных и констант

Давайте пересмотрим пример, который я показал вам ранее.

В Swift мы определяем константы, используя ключевое слово let и переменные, используя ключевое слово var . Двоеточие, : , является маркером для определения типов. В приведенном выше примере мы создаем константу и переменную типа String .

Мы также инициализируем константу и переменную со строкой. В Swift строки определяются точно так же, как строки C в Objective-C, они не предшествующий символу @ .

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

Если мы перепишем приведенный выше пример, позволяя сделать вывод о типе своей работы, то фрагмент кода будет выглядеть следующим образом:

Это намного лучше, и код намного точнее. Компилятор достаточно умен, чтобы понять, что someInt - это Int и someFloat - это Double .

6. Строки

Один из способов получить представление о силе языка - это изучить, как он обрабатывает манипуляции с строкой. Objective-C имеет множество функций и функций, которые позволяют нам обрабатывать строки, лучше, чем большинство языков, но они, как правило, неоднозначны и запутываются время от времени.

Начнем с примера Objective-C. Чтобы объединить две строки в Objective-C, мы делаем следующее:

В Swift, чтобы добавить строку в другую строку, мы используем оператор + . Это очень просто.

Строки в Swift - это Unicode, что означает, что мы можем написать:

Мы можем перебирать символы строки, используя утверждение for..in , как показано в следующем примере. Мы можем использовать цикл for..in для повторения строк Unicode. Это действительно здорово.

Последнее, что я хотел бы рассказать о строках, прежде чем двигаться дальше, - это интерполяция строк. В Objective-C, если мы хотим вывести строку с переменными, мы вызываем [NSString stringWithFormat:] . В Swift переменные могут быть внедрены. Взгляните на следующий пример.

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

7. Массивы и словари

Arrays & Dictionaries

Как программист Objective-C, вы уже знакомы с массивами и словарями. Swift также имеет классы сбора и добавляет к ним несколько дополнительных функций.

Как вы используете коллекции в Swift? В Swift они называются Array и Dictionary . Объявление массива в Swift аналогично объявлению литерала массива в Objective-C с использованием набора квадратных скобок [ ] , но без предшествующего им символа @ .

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

Видоизменяемость

Если объект Array эквивалентен объекту NSArray , а объект Dictionary эквивалентен объекту NSDictionary , то как мы создаем изменяемые массивы и словари в Swift?

Ответ очень прост, объявите объект переменной. Другими словами, в предыдущем примере someArray является эквивалентом экземпляра NSArray , а someOtherArray - экземпляром NSMutableArray . Это относится и к словарям. В предыдущем примере someDictionary является эквивалентом экземпляра NSDictionary и someOomeDictionary , который имеет образец NSMutableDictionary . Это здорово, правда?

Хотя массивы Objective-C и словари могут содержать только объекты, в Swift коллекции могут содержать объекты, а также примитивные типы данных, такие как целые числа и поплавки. Еще одно важное отличие от Objective-C состоит в том, что коллекции в Swift набираются, явно или через вывод типа во время компиляции. Указав тип объектов в коллекции, Swift добавляет дополнительную безопасность для этих коллекций.

Несмотря на то, что мы можем опустить тип переменной при ее объявлении, это не изменяет того факта, что компилятор назначит типы объектам в коллекции. Использование вывода типа помогает сохранить код четким и легким.

Мы можем переопределить объекты Array и Dictionary , которые мы объявили ранее, следующим образом:

Компилятор проверяет коллекции во время их инициализации и выводит правильный тип. Другими словами, он понимает someArray и someOtherArray - это набор объектов String , а someDictionary и someOtherDictionary - это словари с ключами типа String и значениями типа Int .

Манипуляция с коллекциями

Добавление объекта или другого массива в массив очень похоже на конкатенацию строк, поскольку мы также используем оператор + .

Манипулирование словарями так же просто.

Типичные коллекции

Раньше я упоминал, что наборы в Свифт напечатаны, но что это значит? Если мы определяем коллекцию, содержащую объекты String , вы можете добавлять объекты String только к этой коллекции. При этом может произойти ошибка.

Давайте посмотрим на пример, чтобы прояснить это. Мы определяем набор объектов Car . Следующий фрагмент кода показывает определение класса Car и cars с изменяемым массивом, содержащие три образца Car .

За кулисами компилятор выводит тип массива. Если мы хотим добавить новый экземпляр Car в коллекцию, мы можем просто использовать оператор + , как показано ниже.

Однако добавление объекта другого типа приведет к ошибке.

Это имеет дополнительное преимущество безопасности типов для извлечения объектов из коллекций. Это также устраняет необходимость в распределении объектов коллекции перед их использованием.

В нашем примере объект Car имеет метод accelerate . Поскольку коллекция введена, мы можем захватить элемент из массива и сразу вызвать метод объекта в одной строке кода. Нам не нужно беспокоиться о типе элемента, поскольку коллекция содержит только объекты Car .

Чтобы выполнить то же самое в Objective-C с таким же уровнем безопасности, нам нужно написать следующее:

Наконец, для итерации массива мы используем цикл for..in :

Наконец, для итерации массива мы используем цикл for..in :

Как вы можете видеть, типизированные коллекции - это мощная функция языка Swift.

Вывод

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

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



В Swift есть два вида типов: именованные типы и составные типы. Именованный тип представляет собой тип, которому может быть дано определенное имя, когда он определен. Именованные типы включают в себя классы, структуры, перечисления и протоколы. Например, у экземпляра определенного пользователем класса MyClass есть тип MyClass . В дополнение к определенным пользователем именованным типам, стандартная библиотека Swift определяет множество часто используемых именованных типов, в том числе те, которые представляют собой массивы, словари и опциональные значения.

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

Составной тип представляет собой тип без имени, и определяется самостоятельно в языке Swift. Есть два составных типа: типы функций и типы кортежей. Составной тип может содержать именованные типы и другие составные типы. Например, тип кортежа (Int, (Int, Int)) содержит два элемента: первый именованный тип Int , а второй составной тип (Int, Int) .

В этой главе рассматриваются типы, определяемые самостоятельно в языке Swift и описывается поведение выводимых типов в Swift.

Грамматика типа

Аннотация типа

Аннотация типа явно указывает на тип переменной или выражения. Аннотации типов начинаются с двоеточия ( : ) и заканчиваются именем типом, как показано в следующем примере:

В первом примере выражению someTuple предписано иметь тип кортежа (Double, Double) . Во втором примере, параметру a функции someFunction предписано иметь тип Int .

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

Грамматика аннотации типа

Идентификатор типа

Идентификатор типа относится либо к именованному типу, либо к псевдониму именованного или составного типа.

По большей части, идентификатор типа непосредственно ссылается на именованный тип с тем же именем, что и идентификатор. Например, Int является идентификатором типа, который непосредственно ссылается на именованный тип Int , и идентификатор типа Dictionary<String, Int> непосредственно ссылается на именованный тип Dictionary<String, Int> .

Существует два случая, при которых идентификатор типа не ссылается на тип, имеющий такое же имя. В первом случае, идентификатор типа ссылается на псевдоним именованного или составного типа. Например, в приведенном ниже примере, использование Point в аннотации типа относится к типу кортежа (Int, Int) .

Во втором случае, идентификатор типа использует точку ( . ) для обозначения именованных типов, объявленных в других модулях или вложенных внутрь других типов. Например, идентификатор типа в следующем коде ссылается на именованный тип MyType , объявленный в модуле ExampleModule .

Грамматика идентификатора типа

Тип кортежа

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

Вы можете использовать тип кортежа в качестве возвращаемого типа функции. Чтобы функция возвращала один кортеж, содержащий несколько значений. Можно также называть элементы типа кортежа и использовать эти имена для обозначения значений отдельных элементов. Имя элемента состоит из идентификатора, за которым сразу следует двоеточие ( : ). Примером, демонстрирующим эти обе функции, будет раздел Параметры функции и возвращаемые значения.

Если элемент кортежа имеет имя, то имя является частью типа:

Все типы кортежей содержат два или более типов, за исключением Void , который является псевдонимом типа для пустого типа кортежа, () .

Грамматика кортежного типа

Функциональный тип

Тип функции представляет собой тип функции, метода или замыкания и состоит из параметра и возвращаемого типа, разделенного стрелкой ( -> ):

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

Параметр типа функции () -> T (где T произвольный тип) может применять атрибут autoclosure для неявного создания замыкания для своих вызовов. Это обеспечивает синтаксически удобный способ отложить вычисление выражения, без необходимости написания явного замыкания при вызове функции. Подробнее об autoclosure в разделе Автозамыканиях.

У функциональных типов может быть параметр с переменным числом аргументов (вариативный). Синтаксически, такой параметр состоит из имени базового типа, за которым сразу следует троеточие ( . ), как в Int. . Вариативный параметр рассматривается как массив, содержащий элементы базового типа. Например, вариативный параметр Int. рассматривается как [Int] . Подробнее о Вариативных параметрах.

Для того, чтобы указать параметр In-Out , приставьте тип параметра с ключевым словом inout . Вы не можете обозначать вариативный параметр или возвращаемый тип ключевым словом inout . In-Out параметры подробнее рассматриваются в разделе Сквозные параметры.

Имена аргументов не являются частью типа соответствующей функции:

Поскольку аргументы Labels не являются частью типа функции, вы опускаете их при написании типа функции.

Если тип функции включает в себя более чем одну стрелку ( -> ), типы функции группируются справа налево. Например, тип функции Int -> Int -> Int понимается как Int -> (Int -> Int) , то есть функция, которая принимает Int и возвращает другую функцию, которая принимает и возвращает Int .

Типы функций, которые могут генерировать ошибку должны быть отмечены ключевым словом throws , а также типы функций, которые могут повторно генерировать ошибку должны быть отмечены ключевым словом rethrows . Ключевое слово throws является частью функционального типа, а негенерирующие функции являются подклассами генерирующих функций. В результате, вы можете использовать негенерирующую функцию там же, где и генерирующую. Генерирующие и повторно генерирующие функции описаны в "Генерирующие функции и методы" и "Повторно генерирующие методы и функции".

Ограничения для несбегающих замыканий

Параметр, который является несобранной функцией, не может быть сохранен в свойстве, переменной или константе типа Any , поскольку это может позволить вывести значение.

Параметр, который является не ускользающей функцией, не может быть передан в качестве аргумента другому параметру несобранной функции. Это ограничение помогает Swift выполнять больше проверок на конфликтный доступ к памяти во время компиляции, а не во время выполнения. Например:

В приведенном выше коде оба параметра, которые принимают значения TwoFunctions (first: second:) , являются функциями. Ни один из параметров не отмечен как @escaping , поэтому они оба не отображаются в результате.

Четыре вызова функций, отмеченные «Ошибка» в приведенном выше примере, вызывают ошибки компилятора. Поскольку первый и второй параметры являются не ускользающими функциями, они не могут передаваться в качестве аргументов другому параметру не ускользающей функции. Напротив, два вызова функций, отмеченные «ОК», не вызывают ошибки компилятора. Эти вызовы функций не нарушают ограничение, потому что внешний не является одним из параметров takeTwoFunctions(first: second : ) .

Если вам нужно избегать этого ограничения, отметьте один из параметров как экранирование или временно конвертируйте один из параметров функции в функцию экранирования с помощью функции withoutActuallyEscaping (_: do:) . Информацию об избежании конфликта доступа к памяти см. В разделе Безопасность хранения.

Грамматика функционального типа

Типы массива

Язык Swift предоставляет следующий синтаксический сахар для стандартной библиотеки Swift тип Array<Element>:

Другими словами, следующие два объявления эквивалентны:

В обоих случаях константа someArray объявляется как массив строк. Элементы массива могут быть доступны через индексацию, указав допустимое значение индекса в квадратных скобках: someArray[0] относится к элементу с индексом 0 , "Alex" .

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

При обращении к элементам в многомерном массиве, самый левый индекс сабскриптов относится к элементу с этим индексом в самом нижнем массиве. Следующий индекс сабскрипта с правой стороны относится к элементу с этим индексом в массиве, вложенном на предыдущем уровне. И так далее. Это означает, что в приведенном выше примере, array3D[0] относится к [[1, 2], [3, 4]] , array3D[0][1] относится к [3, 4] , и array3D[0][1][1] относится к значению 4 .

Более подробно о стандартном типе Array библиотеки Swift, см. "Массивы".

Грамматика типа массива

Тип словаря

Язык Swift предоставляет следующий синтаксический сахар для типа Dictionary<Key, Value> стандартной библиотеки Swift:

Другими словами, следующие два объявления эквивалентны:

В обоих случаях константа someDictionary объявляется как словарь со строками в качестве ключей и целых чисел в качестве значений.

Значения словаря могут быть доступны через индексацию при указании соответствующего ключа в квадратных скобках: someDictionary["Alex"] относится к значению, связанному с ключом "Alex" . Сабскрипт возвращает опциональное значение типа значения словаря. Если указанный ключ не содержится в словаре, сабскрипт возвращает nil .

Основной тип ключа словаря должен соответствовать протоколу Hashable стандартной библиотеки Swift.

Грамматика типа словаря

Опциональный тип

Язык Swift определяет постфикс ? как синтаксический сахар для именованного типа Optional<Wrapped> , определенного в стандартной библиотеке Swift. Другими словами, следующие два объявления эквивалентны:

В обоих случаях переменная optionalInteger объявляется так, чтобы иметь тип опционального Int . Обратите внимание, что никаких пробелов не должно появляться между именем типа и ? .

Тип Optional<Wrapped> является перечислением с двумя кейсами, None и Some(Wrapped) , которые используются для представления значений, которые могут присутствовать, а могут и не присутствовать. Любой тип может быть явно объявлен (или неявно преобразован) так, чтобы быть опциональным типом. Если вы не укажете первоначальное значение при объявлении опциональной переменной или свойства, его значение автоматически устанавливается на nil .

Если экземпляр опционального типа содержит значение, вы можете получить доступ к этому значению, используя постфиксный оператор !, как показано ниже:

Использование оператора ! для извлечения опционала, имеющего значение nil , приводит к ошибке во время выполнения.

Вы можете также использовать опциональную цепочку и опциональное связывание для выполнения операции в опциональном выражении. Если значение равно nil , операция не выполняется, и, следовательно, не будет ошибки во время выполнения.

Для получения дополнительной информации см. Опционалы.

Грамматика опционального типа

Неявно извлеченный опциональный тип

Язык Swift определяет постфикс ! как синтаксический сахар для именованного типа ImplicitlyUnwrappedOptional<Wrapped> , определенного в стандартной библиотеке Swift. Другими словами, следующие два объявления эквивалентны:

Обратите внимание, что не должно появиться никаких пробелов между именем типа и ! .

Так как неявное извлечение изменяет смысл объявления, которое содержит тип, опциональные типы, которые являются вложенными внутри кортежа или универсального типа, например элемент словаря или массива, не могут быть обозначены как неявно извлеченные. Например:

Так как неявно извлеченные опционалы имеют тот же тип Optional<Wrapped> , то можете использовать неявно извлеченные опционалы в тех же местах в вашем коде, в которых можно использовать опционалы. Например, вы можете присвоить значения неявно извлеченных опционалов переменным, константам и свойствам опционалов и наоборот.

Как и с опционалами, если вы не обеспечиваете первоначальное значение при объявлении неявно распакованной опциональной переменной или свойства, его значение автоматически устанавливается на nil .

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

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

Для получения дополнительной информации см. в разделе Неявно извлеченные опционалы.

Грамматика неявно извлеченного опционального типа

Тип композиции протоколов

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

Тип композиции протоколов выглядит следующим образом:

Тип композиции протоколов позволяет указать значение, тип которого соответствует требованиям нескольких протоколов без необходимости явно определять новый, именованный протокол, который наследует от каждого протокола, которому вы хотите, чтобы тип соответствовал. Например, указать тип композиции протоколов ProtocolA & ProtocolB & ProtocolC так же эффективно, как определить новый протокол, который наследует от ProtocolA , ProtocolB , и ProtocolC , но без необходимости введения нового имени. Так же вы можете использовать SuperClass & ProtocolA вместо того, чтобы определять новый протокол, который является подклассом SuperClass и соответствует ProtocolA .

Каждый элемент в списке композиции протокола должен быть (этот список может иметь всего один класс):

  • имя класса
  • имя протокола
  • псеводоним типа, где внутренний тип является либо композицией протокола, либо самим протоколом, либо классом.

Когда композиция протокола содержит псевдонимы типа, то не исключено, что один и тот же протокол может появляться не один раз. В таких случаях повторения протокола игнорируются. Например, определение PQR в коде ниже аналогично P & Q & R .

Грамматика типа композиции протоколов

Тип метатипа

Тип метатипа относится к типу любого типа, в том числе к типам класса, типам конструкций, типам перечислений и типам протоколов.

Метатип типа класса, структуры или перечисления является именем этого типа, за которым идет .Type. Метатип типа протокола - не конкретный тип, который соответствует протоколу во время выполнения - это имя этого протокола с последующим .Protocol . Например, метатип типа класса SomeClass является SomeClass.Type и метатип протокола SomeProtocol является SomeProtocol.Protocol .

Вы можете использовать выражение постфикса self для доступа к типу в качестве значения. Например, SomeClass.self возвращает сам SomeClass , а не экземпляр SomeClass . И SomeProtocol.self возвращает сам SomeProtocol , а не экземпляр типа, который соответствует SomeProtocol во время выполнения задачи. Вы можете использовать функцию type(of:) с экземпляром типа для доступа к экземпляру динамической идентификации типа в период выполнения, в качестве значения, как показано в следующем примере:

Для получения дополнительной информации, см. type (of: ) стандартной библиотеке Swift.

Используйте выражение инициализатора для того, чтобы создать экземпляр типа из значения типа этого метатипа. Для экземпляров класса инициализатор, который вызывается, должен быть отмечен ключевым словом required , или весь класс должен быть отмечен ключевым словом final .

Грамматика метатипа

Наследование типа

Наследование типа используется для указания того, у какого класса наследует именованный тип и каким протоколам он соответствует. Наследование типа также используется для указания классовых требований по протоколу. Наследование типа начинается с двоеточия ( : ), за которым следуют либо требования class , либо список идентификаторов типа, либо и то и то.

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

Другие именованные типы могут только наследовать или соответствовать списку протоколов. Типы протоколов могут наследовать от любого количества других протоколов. Когда тип протокола наследует от других протоколов, и требования от всех протоколов собираются в совокупность, и любой тип, наследующий от текущего протокола, должен соответствовать всем этим требованиям. Как обсуждалось ранее, вы можете включить ключевое слово class в качестве первого пункта в условие типа наследования, чтобы отметить объявление протокола с class-требованием.

Наследование типа в определении перечисления может быть либо список протоколов, или в случае перечисления, которое присваивает исходные значенияк своим кейсам, может быть один, именованный тип, который определяет тип этих исходных значений. Подробнее Исходные значения.

Грамматика наследования типа

Вывод типа

Swift широко использует вывод типа, что позволяет опустить тип или часть типа множества переменных и выражений в коде. Например, вместо написания var x: Int = 0 , вы можете написать var x = 0 , опуская тип полностью, компилятор правильно понимает, что x это значение типа Int . Кроме того, вы можете опустить часть типа, когда полный тип может быть выведен из контекста. Например, когда вы пишете let dict: Dictionary = ["A": 1] , компилятор делает вывод, что dict имеет тип Dictionary<String, Int> .

В обоих приведенных выше примерах информация о типе передается от листьев дерева выражения к его корню. То есть, для типа x в var x: Int = 0 сначала проводится проверка типа 0 , а затем эта информация передается корню (переменной x ).

В Swift, информация о типе может также протекать в обратном направлении: от корня к листьям. В следующем примере, явный тип аннотации ( : Float ) на константе eFloat приводит к тому, что у числовой константы 2,71828 выведенный тип Float вместо Double .

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.


В этой статье мы рассмотрим принципы работы памяти в Swift и разберемся, как Swift располагает байты в памяти, как управляет памятью, и что из себя представляет жизненный цикл объектов.

Основной единицей информации является бит, который равен 1 или 0. Традиционно мы организовываем биты в группы по восемь, называемые байтами. Память — это просто длинная последовательность байтов, один за другим уходящих в даль. Но они расположены в определенном порядке. Каждый байт получает число, называемое его адресом.


Мы рассматриваем память, организованную по словам (word), а не по байтам. Слово — это расплывчатый термин в информатике, но обычно он означает единицу размером с указатель. На современных устройствах для 64-битного процессора (который и стоит на iPhone) одно слово (word) равняется 8 байтам (64 бита = 8 байтам). Именно столько мы можем получить байт за одно обращение к памяти.


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

Итак, первое, что нам интересно узнать — сколько потребуется выделить памяти для хранения структуры FullResume .

Проверить ответ нам поможет MemoryLayout, который позволяет узнать информацию о размере структуры. Через статические свойства мы можем получить информацию о размере, выравнивании и шаге.

Размер вычисляется достаточно просто — это сумма всех его полей. Как мы можем увидеть, String занимает 16 байт, Int — 8 байт, а Bool — 1 байт.

Ради интереса попробуем переставить наше Bool свойство на первое место, остальные поля просто сместим ниже. Сколько теперь? Кажется, 25. Или нет? По логике размер не должен измениться, ведь он считается по сумме всех полей. Проверяем!

Немного неожиданный результат: перестановкой мы заняли только больше памяти. Что ж, давайте разбираться дальше.

Посмотрим, как наша структура разместилась в памяти, в этом нам поможет метод withUnsafeBytes(_:) , который возвращает нам UnsafeRawBufferPointer , позволяющий итерироваться по каждому байту. Получаем следующую картину — Bool занял все 8 байтов, String как и ожидалось свои 16 байтов и Int занял 8 байтов.


Чтобы разобраться с этим, нам понадобится понять еще два термина — stride (шаг) и alignment (выравнивание). Для начала познакомимся со stride.

Stride

Я выделю простую структуру ShortResume . Простую в том плане, что она имеет меньший размер (Int32 — 4 байта, Bool — 1 байт) и будет проще восприниматься на изображениях.


Alignment

Первое, что нам хочется понять — для чего нужно выравнивание? В начале статьи выделялся такой термин как word, который на примере ShortReume выглядит так:


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

Для ясности рассмотрим пример не выровненных (смещенных) данных. Это грозит тем, что для получения значения Int32 нужно сделать два обращения к памяти — сначала прочитать слово 0, затем слово 1, соединить два прочитанных массива байтов, и только затем получить окончательное значение. На всё это накладывается непонятки: откуда читать данные у слова 0 и до куда у слова 1.

Дабы избежать таких ситуаций, у нас и есть такое значение — alignment (выравнивание).

У всех простых типов в Swift есть свое выравнивание. Простой Int или String должен выравниваться по 8 байт, Int32 и Int16 требуют меньше выравнивания — 4 и 2 байта соответственно, а для Bool достаточно одного. Как можно заметить, для простых типов выравнивание равно размеру. Но давайте рассмотрим, как эти числа влияют на memory layout структуры.

Возвращаясь к нашему FullResume (из которого был убран только String ), можно заметить следующее: размер — 9, выравнивание — 8, шаг — 16. Каждое свойство выровнено, мы можем получить любое значение свойства из резюме за один цикл чтения памяти.


И немного иная картина получится если переставить Bool на первое место. Из-за того что Int имеет выравнивание равное 8, он должен начинаться с байта, кратный 8, поэтому и образовывается пустое место между Bool и Int , что за собой влечет увеличения размера структуры, которая становится 16 вместо 9.


Выравнивание всей структуры рассчитывается достаточно просто — это наибольшее выравнивание из всех свойств. Если мы заменим Int на Int16 , у которого выравнивание равно 2, то и вся структура будет иметь выравнивание 2.

Шаг считается также просто, но немного хитрее — это размер округленный в большую сторону, кратный выравниванию. Именно поэтому при размере структуры равному 9 байт следующим числом, кратным 8, будет 16.

Проверь себя

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

Class

MemoryLayout работает и для классов. Попробуем вывести для них всё то же самое.

Что ж, везде будет 8, потому что классы — ссылочный тип, а все ссылки равны 8 байтам (на 64-битной машине).

Чтобы узнать реальный размер, занимаемый в куче, нужно воспользоваться Objective-C runtime функцией — class_getInstanceSize(_:) . В этом случае получится:

16 * 2 String + 8 Bool (1 + 7 alignment) + 8 Date + 8 Optional (1 + 7 alignment) + 16 metadata (isa ptr + ref count)


Хочется погрузиться немного глубже и найти какой-нибудь способ, чтобы обследовать содержимое памяти напрямую. Не просто распечатывать байты, как мы делали до этого, а посмотреть на указатели: кто где живет и как размещается в памяти. Для всего этого нам нужно:

  1. Дампить память
  2. Найти указатели
  3. Визуализировать

Поначалу были попытки написать такую программу самостоятельно, пока не нашелся интересный доклад от Mike Ash.


Суть в том, что Mike Ash написал программу на Swift, которая может прыгать по указателям и уходить в глубину, учитывая то, что в какой-то момент указатель может стать конечным. Для того, чтобы не словить краш при обращении к указателю, которого нет, он использует вспомогательные функции из языка C. Исходный код открыт, и с ним можно ознакомиться.


Вспомним наш FullResume и попробуем прогнать его через dumper.

Самая прелесть заключается в том, что этот dumper строит граф памяти, и имеется возможность оценить всё визуально.


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

Попробуем изменить структуру на класс и снова прогоним через наш Memory Dumper.


Немного сложней, чем структура, да.

Это и логично, классы в Swift сами по себе сложнее, так как связаны с Objective-C, хранятся в куче, имеют свои метаданные для указателей, счетчики ссылок и так далее.

Теперь нам удалось очень наглядно разглядеть эту разницу.

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

Но есть еще интересные моменты, связанные с тем, как и сколько живут классы в памяти. Давайте посмотрим, как счетчики ссылок влияют на управление памятью.

Reference Counters

Всего в Swift три счетчика ссылок:

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

До Swift 4

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

До Swift 4, счетчики ссылок располагались до свойств класса прямо в объекте. Класс имел только два счетчика — weak и strong.


На объект начинает ссылаться два внешних объекта — один сильно, другой слабо, счетчики прибавляются по одному.


В один момент времени объект с сильной ссылкой удаляется из памяти, и теперь у нас осталась только одна слабая ссылка. Что происходит в этот момент?


Данные объекта уничтожаются, но память не освобождается, так как счетчик еще требуется хранить. В памяти остается так называемый «зомби объект», на который ссылается слабая ссылка. Только при обращении по слабой ссылке в runtime будет выполнена проверка: «зомби» (NSZombie) этот объект или нет. Если да, счетчик ссылок уменьшается.

Xcode умеет находить такие объекты и сообщать о них, плюс имеет инструмент для этого.

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

Встречался еще один достаточно критичный баг: получение (загрузка) объекта по слабой ссылке было не потокобезопасным!

Данный кусок кода может получить ошибку в Runtime. Суть именно в том механизме, который был рассмотрен ранее. Два потока могут одновременно обратиться к объекту по слабой ссылке. Перед тем, как получить объект, они проверяют, является ли проверяемый объект «зомби». И если оба потока получат ответ true , они отнимут счётчик и постараются освободить память. Один из них сделает это, а второй просто вызовет краш, так как попытается освободить уже освобожденный участок памяти.

Такая реализация не очень хороша и с этим нужно что-то делать.

Side Table

В новой реализации появляется такое понятие как Side Table или, если на русском — «Боковая Таблица».

Боковая таблица — это область в памяти, содержащая некоторую дополнительную информацию об объекте, которую не нужно хранить в нем самом. В текущей реализации в боковой таблице хранятся счетчики ссылок, но в некоторых статьях мелькала мысль, что там можно было бы хранить associated objects. Сейчас они хранятся в глобальной таблице, доступ к которой замедлен из-за потокобезопасности.

Стоит разобраться как сегодня Swift работает с боковой таблицей. Потому что в новой реализации объект должен как-то ссылаться на боковую таблицу и работать со счетчиками ссылок.

Чтобы избежать дополнительных затрат в виде 8 байт на указатель боковой таблицы, Swift прибегает к изящной оптимизации.


Первоначально объект содержит pointer и имеет только два счетчика ссылок. Боковой таблицы нет, ибо объект в ней никак не нуждается. При увеличении счетчика сильных ссылок всё работает как обычно, и ничего особенного не происходит.


Во втором поле резервируется один бит, по которому определяется, используется ли сейчас боковая таблица в этом поле или здесь хранится счетчик ссылок.

Как только мы начинаем ссылаться на объект слабо (weak reference), то создается боковая таблица, и теперь объект вместо сильного счетчика ссылок хранит ссылку на боковую таблицу. Сама боковая таблица также имеет ссылку на объект.

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


С таким механизмом слабые ссылки ссылаются не напрямую на объект, а на боковую таблицу, которая указывает на объект. Это решает две предыдущие проблемы:

  1. Экономие памяти. Объект удаляется из памяти, если на него больше нет сильных ссылок.
  2. Это позволяет безопасно обнулять слабые ссылки, поскольку слабая ссылка теперь не указывает напрямую на объект и не является предметом race condition.

Такой механизм немного усложняет понимание жизненного цикла объекта, но в самом исходном коде Swift в комментариях он расписан хорошо и представляет из себя конечную машину состояний.


Итак, машина заводится с самого первого состояния сразу, как только мы создали объект. Объект жив, его счетчики инициализируются со значениями strong — 1, unowned — 0, weak — 0 (weak появляется только с боковой таблицей). На данный момент нет боковой таблицы. Операции с unowned переменными работают нормально.

На объект также может быть unowned ссылка, которая прибавляет +1 к unowned и +1 к strong.


Когда strong RC достигает нуля, вызывается deinit() , и объект переходит в следующее состояние.


Это состояние Deiniting. На данном этапе операции со strong ссылками не действуют. Unowned отнимает -1 от strong, который был прибавлен на предыдущем шаге. При чтении через unowned ссылку будет срабатывать assertion failure. Но новые unowned ссылки еще могут добавляться. Если есть боковая таблица, то weak операции будут возвращать nil. Далее из этого состояния уже можно перейти в два других.


Первое: если нет боковой таблицы (то есть нет weak ссылок) и нет unowned ссылок, то объект переходит в Dead состояние и сразу удаляется из памяти.


Второе: если у нас есть unowned или weak ссылки, объект переходит в состояние Deinited. В этом состоянии функция deinit() завершена. Сохранение и чтение сильных или слабых ссылок невозможно. Как и сохранение новых unowned ссылок. При попытке чтения unowned ссылки вызывается assertion failure. Из этого состояния также возможно два исхода.


В том случае, если нет слабых ссылок, объект переходит непосредственно в состояние Dead, которое было описано выше.


В случае наличия weak ссылок, а значит и боковой таблицы, осуществляется переход в состояние Freed (Освобожден). Weak ссылки также добавляет +1 к unowned, и после освобождения отнимает обратно. В Freed состоянии объект уже полностью освобожден и не занимает места в памяти, но его боковая таблица остается жива.


После того как счетчик слабых ссылок достигает нуля, боковая таблица также удаляется и освобождает память, и осуществляется переход в финальное состояние — Dead.

В мертвом состоянии от объекта ничего не осталось, кроме указателя на него. Указатель на HeapObject освобождается из кучи, не оставляя следов объекта в памяти.

Инварианты счетчиков ссылок

Весь жизненный цикл сопровождается инвариантами счетчиков ссылок. Инвариантность — это выражение, определяющее непротиворечивое внутреннее состояние объекта.

можно ли как-то сымитировать [NSString stringWithFormat:@"%p", myVar] код на новом языке swift ?

теперь это часть стандартной библиотеки: unsafeAddressOf .

для Swift 3 Используйте withUnsafePointer :

печатает адрес памяти someVar.

печатает адрес памяти someVar. (спасибо @Ин)

обратите внимание, что этот ответ был довольно старый. Многие из описанных в ней методов больше не работают. В частности, .core не обращались.

однако ответ @drew правильный и простой:

теперь это часть стандартной библиотеки: unsafeAddressOf.

таким образом, ответ на ваши вопросы:

вот оригинальный ответ, который был отмечен правильно (для потомство/вежливость):

Swift "скрывает" указатели, но они все еще существуют под капотом. (потому что среда выполнения нуждается в этом и по причинам совместимости с Objc и C)

есть несколько вещей, чтобы знать, однако, но сначала, как напечатать адрес памяти быстрой строки?

это печатает адрес памяти строки, Если вы откроете Xcode - > Debug Workflow - > View Memory и перейдите к напечатанному адресу, вы увидите необработанные данные строки. Поскольку это строковый литерал, это адрес памяти внутри хранилища двоичного файла (а не стека или кучи).

однако, если вы это сделаете

это будет в стеке, потому что строка создается во время выполнения

Примечание: .ядро._baseAddress не документирован, я нашел его в инспекторе переменных, и он может быть скрыт в будущем

_baseAddress доступен не для всех типов, вот еще один пример с а Кинт

здесь takesInt является вспомогательной функцией C, как это

на стороне Swift эта функция takesInt(intptr: CMutablePointer<CInt>) , поэтому требуется CMutablePointer для CInt, и вы можете получить его с помощью & varname

печать функция 0x7fff5fbfed98 , по этому адресу памяти вы найдете 289 (в шестнадцатеричной записи). Вы можете изменить его содержимое с помощью *intptr = 123456

теперь, некоторые другие вещи, чтобы знать.

строка, в swift, является примитивный тип, а не объект.
CInt-это тип Swift, сопоставленный типу C int.
Если вам нужен адрес памяти объекта, вы должны сделать что-то другое.
Swift имеет некоторые типы указателей, которые могут использоваться при взаимодействии с C, и вы можете прочитать о них здесь: Типы Указателей Swift
Кроме того, вы можете больше узнать о них, изучая их объявление (cmd+нажмите на тип), чтобы понять, как преобразовать тип указателя в другой

printptr является вспомогательной функцией C, которую я создал, с этой реализацией

опять же, пример решения напечатано: 0x6000000530b0 , и если вы пройдете через инспектор памяти, вы найдете свой NSString

одна вещь, которую вы можете сделать с указателями в Swift (это можно сделать даже с параметрами inout)

Читайте также: