На сайте представлен полный неавторизованный хронометрированный конспект на русском языке Лекции 2 Стэнфордского курса CS193P Spring 2020 “Разработка iOS с помощью SwiftUI ”.
Первая часть — 0 — 38 минута находится в этом посте,
Вторая часть — 38 — 104 минута находится здесь .
Код находится на GitHub.
—————— НАЧАЛО КОНСПЕКТА ——————————————
Добро пожаловать на вторую Лекцию курса Стэнфорда CS193P. Это семестр Весна 2020 (Spring 2020). Это курс “Разработка iOS приложений с помощью SwiftUI.”
Я собираюсь вернуться к демонстрационному примеру, который мы начали на первой Лекции, но сначала я хотел бы рассказать о двух очень важных концептуальных идеях.
Первая из этих идей — MVVM. Это парадигма конструирования, которую мы будем использовать при создании нашего приложения для организации кода.
И вторая вещь, о которой я собираюсь говорить, — это система ТИПов в Swift.
Архитектура
|
Давайте начнем с MVVM.
Архитектура
|
MVVM — это модель организации кода. То есть по сути определение мест, где “живет” код вашего приложения. MVVM работает в связке с концепцией “Pеактивного” пользовательского интерфейса, о которой я упоминал в прошлый раз. Необходимо придерживаться MVVM при работе в SwiftUI. Вы не можете работать в SwiftUI без MVVM. Те из вас, кто смотрел предыдущие версии этого CS193P курса для iOS 11, должны знать, что MVVM отличается MVC, что является сокращением для Model-View-Controller и используется UIKit, старым механизмом проектирования пользовательского интерфейса в iOS приложениях.
У MVVM много общего с MVC, и основное состоит в том, что и в том, и в другом случае мы пытается отделить Model, которая является бэкендом нашего приложения, от View, которое предстаёт непосредственно перед пользователем. Обе архитектуры пытаются сделать Model UI независимой.
Давайте сначала поговорим о Model и View, а затем о том, как MVVM соединяет их вместе.
Итак Model — UI НЕзависима. Model НЕ импортирует SwiftUI, например. Она пытается инкапсулировать (вобрать в себя) данные Data и логику Logic того, как ваше приложение работает.
На примере нашей карточной игры “на совпадение” карты — это данные Data, а логика Logic игры заключается в том, что происходит, если я выбрал карту? Что значит “совпадение”? Сколько очков я получу, если карты совпадут? Что произойдет, если карты не совпадут?
Вся эта логика Logic игры и сами карты как данные Data, “живут” в Model.
Model — единственный источник ИСТИНЫ “Truth”. Для получения данных Data и логики Logic единственным источником ИСТИНЫ является Model.
Мы НИКОГДА не будем хранить эти данные Data где-то ещё, и у нас НИКОГДА не будет двух различных версий данных Data. Мы ВСЕГДА будет обращаться к Model за ИСТИНОЙ ( данными Data и логикой Logic).
Теперь по поводу View, View отражает Model. Данные Data всегда “текут” ОТ Model к View. Мы всегда будем пытаться заставлять наше View выглядеть как наша Model.
Как бы наш View ни “рисовал” то, что находится в Model, как бы ни выглядела наша карточная игра на совпадение на экране, View ВСЕГДА будет отражать состояние игры, которое в данный момент зафиксировано в Model. Это очень важная вещь относительно того, как работает View.
ВСЕГДА View отражает то, что находится в Model.
View по большому счету вообще не имеет состояния State (stateless), потому что состояние State игры на совпадение находится в Model.
Так что само по себе View не должно иметь каких-либо состояний State.
View по сути просто берет текущее состояние Model и отображает его и оно должно уметь это делать в любое время. Просто в любое время должны иметь возможность сказать View:
“Посмотри на Model и отобрази её состояние на экране прямо сейчас.”
Именно таким образом мы должны сконструировать наш View. И это сделает наш View тем, что мы называем ДЕКЛАРАТИВНЫМ (declarative).
ДЕКЛАРАТИВНЫЙ (declarative) View означает, что мы будем просто декларировать то, как выглядит наш View, и что действительно будет меняться на экране, если изменится Model.
Давайте посмотрим на код, который мы написали на прошлой Лекции:
Заметьте, что мы нигде не вызываем функции, чтобы разместить элементы нашего UI на экране, мы просто создаем эти UI элементы. Мы просто создаем прямоугольник с закругленными углами
RoundedRectangle, или метку Text, или горизонтальный стек HStack, или ZStack, все эти UI элементы мы создаем и размещаем их там, где нам нужно на UI.
Единственные вызываемые функции в этом коде являются модификаторами (modifiers), то есть тем, что меняет то, как выглядят UI элементы, и они размещаются непосредственно рядом с тем элементом UI, который они меняют :
Итак, код, который мы написали на прошлой Лекции, просто декларирует, как выглядит наш пользовательский интерфейс.
Это совершенно отличается от старого способа разработки iOS приложений, а также от множества других систем, которые работали на нас в течение десятилетий и которые мы называем ИМПЕРАТИВНЫМИ.
Если слышите, что идет речь об ИМПЕРАТИВНОЙ модели разработки пользовательского интерфейса или об ИМПЕРАТИВНОЙ модели кодирования в целом, то думайте о том, что слово “ИМПЕРАТИВНЫЙ” (imperative) имеет тот же корень, что и слово “ИМПЕРСКИЙ” (imperial).
Имперское государство — это государство, в котором правит император и он говорит своим подданным: “Делайте это, стройте это, сажайте эти поля и т.д.” Император говорит, что нужно делать людям, и таким способом “живет” имперское государство.
Следуя этой метафоре в Мире UI, ВЫ говорите, где разместить ту или иную кнопку, как организовать UI элементы на экране и для того, чтобы это сделать, вы все время вызываете функции.
Чем же плоха ИМПЕРАТИВНОЙ модели разработки пользовательского интерфейса (UI)?
Главная причина состоит в том, что вам приходится иметь дело со ВРЕМЕНЕМ.
—— 5 -ая минута лекции ———
Все эти UI элементы, все эти функции для их создания вызываются в определенное время.
Разместите кнопку в этом месте, а затем позже мы изменим её заголовок, а затем ещё позже мы сделаем её видимой или что-то ещё.
Если вы хотите понять, как работает ваш ИМПЕРАТИВНЫЙ UI, вам придется иметь дело с ещё одним измерением, а именно со ВРЕМЕНЕМ, то есть вам нужно знать, КОГДА вызываются те или иные функции и как их вызов зависит от вызовов других аналогичный функций. Если ваш UI сформировался и работает в данный момент, то кто угодно может вызвать любую функцию, меняющую ваш пользовательский интерфейс. Вам все время нужно быть на страже и готовым ко всему. Это просто кошмар управлять таким UI и самое главное, практически невозможно доказать, что ваш UI действительно работает, ведь вы не можете вызывать каждую функцию отдельно во всевозможных обстоятельствах.
В противоположность этому, при ДЕКЛАРАТИВНОМ способе представления UI вы ВСЕГДА сможете посмотреть на описание UI и четко увидеть, что он показывает. Вы можете сделать это в любое время, потому что описание UI НЕ зависит от ВРЕМЕНИ.
Его можно спросить в любой момент времени, что он делает, что он рисует, и для этого достаточно посмотреть на код, который находится прямо перед вами.
Все, что рисуется на вашем UI находится прямо перед вами.
Тот код, который мы писали на прошлой Лекции, и является тем самым полным кодом, предназначенным для показа ваших карт, и больше нигде нет никакого другого кода, который будет вызывать какие-либо функции, связанные с этим кодом, и все портить.
Позже сегодня вы узнаете, что структуры struct, а все наши Views являются структурами structs, по умолчанию являются read-only (только для чтения). Поэтому никому не разрешено вызывать функции, которые их изменяют, это совершенно невозможно.
Таким образом, вы можете быть уверены, что этот View всегда будет выглядеть в точности так, как видите это в коде, который декларирует его и находится перед вами в данный момент.
Это огромное преимущество в понимание того, как этот код работает состоит в том, состоит в том, что вы абсолютно уверены в том, что никакие случайные вещи не произойдут с вашими Views во время работы вашего приложения
Это фантастика.
Огромное улучшение по сравнению с императивными моделями проектирования UI.
И последняя часть, касающаяся View, состоит в том, что он должен быть РЕАКТИВНЫМ (reactive):
Это означает, что как только Model изменится, View должен быть АВТОМАТИЧЕСКИ обновлен, потому что помните? Я говорил вам, что View не имеет СОСТОЯНИЯ (stateless). В любое время View следует сказать, чтобы он выглядел так, как требует Model, и для этого нам нужен некий механизм, который бы всякий раз, когда меняется Model, АВТОМАТИЧЕСКИ просила бы View выглядеть так, как этого требует изменившаяся Model.
Это называется РЕАКТИВНЫМ программированием, система реагирует (react) на изменения в Model. Вот и всё.
Для того, чтобы это происходило, нам понадобится в шаблоне MVVM ещё один компонент с именем ViewModel.
Работа ViewModel состоит в том, чтобы “привязать” (bind) View к Model. Как только происходят хоть какие-то изменения в Model, View тут же отражает эти изменения:
По поводу “привязки” View к Model. Что имеется в виду?
ViewModel может интерпретировать Model (то есть данные Data и логику Logic) для View.
Зачем это нужно?
Мы хотим, чтобы View был как много проще, так как код для него пишется ДЕКЛАРАТИВНЫМ способом. Мы не хотим, чтобы он содержал избыточный код наподобие преобразования одного ТИПА в другой. Мы будем просить ViewModel делать эти преобразования.
Model в нашей карточной игре, которую мы написали на прошлой Лекции, это игра на запоминание и наша Model — это обычная структура struct. Это очень маленький демонстрационный пример и у него очень простая Model, но вполне себе можно представить, что ваша Model — это SQL база данных или что-то, что мы получаем из интернета с помощью HTTP запросов. Model — может быть очень сложной и ваш ViewModel может упростить её, преобразовав в более простые структуры данных, которые сможет передать в View и позволив View использовать более простой код для их “рисования”.
Так что ViewModel выполняет роль интерпретатора данных Data модели Model.
Но мы собираемся вставить ViewModel между Model и View. Это будет тот самый механизм, который помогает АВТОМАТИЧЕСКОМУ обновлению View при изменении Model.
Так как же ViewModel делает это?
Прежде всего ViewModel пытается заметить те изменения, которые произошли в Model, и делает он это любыми возможными способами. Если ваша Model — это структура struct, то сделать это действительно очень легко.
Мы поговорим о структуре struct, как о ТИПЕ в языке программирования Swift через несколько мгновений, но замечательной отличной особенностью структуры struct является то, что при передаче структуры struct в качестве аргумента функции или ещё куда-то она копируется, и Swift точно знает, когда структура struct изменилась. Он отслеживает, когда структура struct изменилась и для ViewModel, чья Model — это структура struct, увидеть её изменения очень легко.
Но если Model — это SQL база данных, я не знаю, насколько вы все знакомы с базами данных, но очень просто, например, добавить некоторую информацию в SQL базу данных, так что если база данных изменяется, то вы получите уведомление. Но узнать об этих изменениях в Model, это ответственность ViewModel.
Это первостепенная вещь, которую должна выполнять ViewModel.
Если данные Data изменились, то ViewModel может интерпретировать данные Data, возможно преобразовывать их в другой формат или что-то подобное, а затем ViewModel публикует сообщение : “Что-то изменилось в этом Мире…” всем, кому это интересно.
И это всё, что делает ViewModel — публикует сообщение : “Что-то изменилось…”
Реально у ViewModel даже нет указателей (pointers) на какое-либо из этих Views.
ViewModel НИКОГДА не имеет указатель (pointer) на свой View.
Это очень ВАЖНО понимать. ViewModel НИКОГДА не говорит с View напрямую.
—— 10 -ая минута лекции ———
Когда что-то изменилось в Model, ViewModel публикует сообщение : “Что-то изменилось…”
А View “подписывается” на эту публикацию, и если View видит, что “Что-то изменилось…”, то обращается к ViewModel и запрашивает : “Какое текущее СОСТОЯНИЕ (State) в этом Мире? Я собираюсь нарисовать себя в соответствии с этим текущим СОСТОЯНИЕМ!”
Причина такого запроса к ViewModel заключается в том, что View не может напрямую обращаться к Model, так как именно ViewModel может интерпретировать данные Data модели Model, а может быть потому, что пытается защитить Model от возможных опасных действий некоторого View.
Вот как работает ViewModel, это очень просто.
ViewModel замечает изменения в Model, которые могут произойти в любое время, и говорит: “Что-то изменилось…”
Затем View просто отслеживает это сообщение “Что-то изменилось…”, “вытягивает” данные Data из ViewModel и “перерисовывает” себя, потому что именно это и должно делать View: ВСЕГДА “перерисовывает” себя в соответствии с текущем состоянием Model, которое View получает через ViewModel как интерпретатора.
Это всё, что нужно делать. В течение этого семестра мы увидим синтаксис Swift, который делает эту работу возможной.
Я разместил на рисунке некоторые синтаксические конструкции Swift типа ObservableObject, onReceive, objectWillChange и т.д. Об этом синтаксисе мы начнем разговаривать на следующей неделе.
На самом деле мы увидим этот синтаксис прямо сегодня в демонстрационном примере в конце Лекции 2. Мы даже начнем использовать некоторые из этих ключевых слов, чтобы сделать возможным для View отслеживает сообщение “Что-то изменилось…” и “вытягивать” данные Data из ViewModel.
А как насчет другого направления?
Мы говорили о том, как данные Data модели Model “перетекают” во View, и о том, что View ВСЕГДА отражает то, что происходит в Model.
А что если View, у которого есть кнопки и выполняются Swipe жесты, хочет изменить Model?
Как это работает?
Для этого мы возложим на ViewModel ещё одну ответственность, которая состоит в том, чтобы обрабатывать “намерения” (Intents) и под “намерением” (Intent) я понимаю “намерение” пользователя, фактически, конечного пользователя.
MVVM — это целая система. Но существует и нечто другое, также относящееся к архитектуре приложения, и называется это нечто Model—View—Intent (Модель — Изображение — Намерение — MVI). Что делает более понятной архитектуру приложения, когда пользователь что-то хочет сделать и проходит через это “намерение”.
В настоящий момент дизайн конструирования UI для iOS приложений с помощью Apple фреймворка SwiftUI не реализует Intent систему, так что я буду говорить об Intent системе, как о концепции.
Intent — это некоторое “намерение” пользователя.
Классическим примером “намерения” Intent в нашей карточной игре на запоминание является “намерение” пользователя выбрать карту. Это и есть “намерение” Intent.
Обработка этих “намерений” Intent остается на усмотрение ViewModel и ViewModel обеспечивает это, делая доступными функции, которые View вызывает, чтобы обозначить свои “намерения”.
То есть View, в котором работают различные “жесты”-Tap “жест”, Swipe “жест” и другие “жесты” — будет вызывать Intent функции в ViewModel.
Это просто документация, у нас в коде ViewModel будет секция для этих целей, в которой мы будем делать соответствующие комментарии о том, что эти функции отражают “намерения” Intents> конечного пользователя.
Теперь становится ясным, что происходит, если пользователь хочет изменить Model. Когда ViewModel получает от View вызов Intent функции, ViewModel модифицирует Model :
И опять ViewModel знает все о Model, в частности о том, что собой представляет Model. Если это SQL база данных, то ViewModel выполняет SQL команды для изменения Model, если это просто структура struct, то, возможно, просто устанавливаются некоторые переменные vars или вызываются функции, которые модифицируют Model. В принципе вы можете делать всё, что угодно для выражения “намерений” пользователя по изменению Model.
Итак, Model меняется.
Что происходит дальше?
В точности то, о чем мы говорили прежде.
ViewModel замечает изменения в Model, которые только что сделаны, публикует сообщение: “<Что-то изменилось…” Затем View отслеживает это сообщение “Что-то изменилось…” и АВТОМАТИЧЕСКИ “перерисовывает” себя.
И на этом всё.
Все эти этапы и составляют MVVM архитектуру.
Вы видите на рисунке также ключевые слова Swift, которые мы увидим при написании кода, когда будем реализовывать все те этапы, о которых мы говорили. Но это достаточно просто.
Ключом к применению MVVM архитектуры является понимание того, какую роль играет каждая из этих 3-х вещей — Model, View и ViewModel, потому что должны быть очень четко определены в коде.
В демонстрационном примере, который я покажу сегодня, мы реализуем MVVM архитектуру для нашей карточной игры на запоминание.
Мы не смогли бы реализовать никаким другим способом.
Но если бы и реализовали, то это было бы экстремально плохо.
Я не уверен, что мы смогли бы заставить её работать без MVVM архитектуру, но если бы даже смогли, то это было бы неправильно.
Мы хотим использовать MVVM парадигму проектирования.
Но прежде, чем мы приступим к демонстрационному примеру, необходимо рассмотреть небольшую тему — ТИПЫ языка программирования Swift.
Архитектура
|
О языке программирования Swift можно много чего изучать, но мы начнем с изучения ТИПОВ языка программирования Swift. Их у него 6.
——- 15 -ая минута лекции ———
Это структура struct, с которой мы уже знакомы.
Класс class предназначен для объектно-ориентированного программирования, но мы увидим его.
Протокол protocol, мы уже видели.
“Не важно, какой» ТИП — это просто generic.
Перечисление enum.
Функции. Да, функции являются ТИПами в Swift.
Но в условиях ограниченного времени мы рассмотрим только 4 ТИПА: структуру struct, класс class, “Не важно, какой» ТИП и функции.
О протоколе protocol и перечислении enum я расскажу в следующий раз.
Давайте начнем со структуры struct и класса class.
Структура struct и класс class выглядят почти одинаково. Их синтаксис очень похож.
У обоих есть хранимые (stored) переменные наподобие переменной var isFaceUp, которую мы видели в демонстрационном примере на прошлой Лекции:
Структура struct и Класс class
|
Как структура struct, так и класс class могут иметь вычисляемые (computed) переменные, которые вы также видели в демонстрационном примере на прошлой Лекции, а именно var body. Их значения вычисляются каждый раз, когда их запрашивают для структуры struct или класса class:
Структура struct и Класс class
var body: some View { |
Оба, и структура struct, и класс class имеют то, что называется lets. По сути let — это переменная var, которая НЕ изменяется, то есть вовсе и не переменная, а константа (constant):
Структура struct и Класс class
let defaultColor = Color.orange |
Как структура struct, так и класс class имеют функции:
Структура struct и Класс class
func multiply ( operand: Int, by: Int) -> Int { |
У нас нет достаточно времени для обсуждения синтаксиса функций, но позвольте мне все-таки уделить им немного внимания.
Мы уже знаем, что аргументы функции имеют метки (labels), и у нашей функции multiply имеется два аргумента. Первый аргумент называется operand, а второй — by, и оба аргумента имеют ТИП Int. Функция multiply возвращает Int, который является результатом умножения operand на by. И внутри функции multiply я использую эти метки (labels), чтобы заставить их работать.
Так что я могу написать multiply (operand: 5, by: 6)
И эта функция вернет нам 30.
Но я хочу сказать немного больше об этих метки (labels).
В действительности каждый аргумент может иметь 2 метки (labels):
Структура struct и Класс class
func multiply ( operand: Int, by: Int) -> Int { func multiply (_ operand: Int, by otherOperand: Int) -> Int { |
У меня опять функция multiply, у которой имеется два аргумента, но на этот раз у каждого аргумента этой функции по две метки (labels).
У первого аргумента две метки: _ (подчеркивание) и operand.
У второго аргумента также две метки: by и otherOperand.
У каждого аргумента по 2 метки: одна окрашена в голубой цвет, а другая в фиолетовый.
Но зачем нужны 2 метки?
Голубая метка используется при вызове функции, а фиолетовая используется внутри функции.
Фиолетовые метки ведут себя точно также, как и метки в прошлой функции multiply, то есть возвращается выражение operand * otherOperand. Именно так используются вторые метки для аргументов.
Но теперь посмотрите на вызов функции: multiply (5, by: 6).
Метка _ (подчеркивание) означает, что при вызове функции вообще НЕТ метки у этого аргумента.
Именно поэтому при создании эмоджи с помощью Text («Hello There, World!«) у нас нет никакой метки для аргумента. Потому что использование _ (подчеркивания) где-то в коде означает, что вы можете не указывать. Более того в Swift использование _ (подчеркивания) ВСЕГДА означает, что вы можете это не указывать, то есть применяется как “неиспользуемый” символ. Мы увидим это в сегодняшнем демонстрационном примере.
Для второго аргумента голубая метка by используется как внешнее имя (external name) при вызове функции. Для второго аргумента фиолетовая метка otherOperand используется как внутреннее имя (internal name) для вычислений внутри функции.
Вы можете более внимательно посмотреть на это позже, но это основной синтаксис для функций, а функции есть и у структуры struct, и у класса class.
Как у структуры struct, так и у класса class есть специальные функции, которые называются ИНИЦИАЛИЗАТОРАМИ (initializers).
Инициализаторы используются для создания структуры struct или класса class с некоторыми аргументами, которые необязательно являются их переменными vars.
Мы уже видели это при создании карты CardView (isFaceUp: true). Помните карту CardView? Мы создали карту с аргументом isFaceUp равным true и установили значение переменной var isFaceUp в нашей структуре struct CardView.
Мы всегда можем проводить инициализацию подобным образом, но что если мы хотим использовать при инициализации другого рода аргумент, который не будет инициализировать непосредственно переменные vars?
И прекрасный пример такой инициализации — это инициализатор нашей карточной игры на запоминание MemoryGame:
Структура struct и Класс class
struct MemoryGame { |
Переменными vars этой игры MemoryGame являются карты cards, но при создании игры MemoryGame мы хотим только указать, сколько пар карт будут участвовать в этой игре.
То есть определить, будет ли это большая игра MemoryGame с 20 парами карт или совсем маленькая с 6 парами карт.
Для этого мне необходим аргумент numberOfPairsOfCards, который имеет ТИП Int, и разместить инициализатор, который возьмет этот аргумент, непосредственно внутри структуры struct MemoryGame.
Самое замечательное то, что у меня может быть любое количество каких угодно инициализаторов inits, каждый из которых имеет различный набор аргументов.
Так что, возможны и другие инициализаторы для игры MemoryGame.
Таким образом, структуры struct и классы class, оба имеют инициализаторы inits.
Но в чем различие между структурами struct и классами class?
——- 20 -ая минута лекции ———
Структура struct и Класс class
|
Они выглядят примерно одинаковыми, слишком похожими, но есть несколько фундаментальных различий, и давайте поговорим о них.
VALUE — REFERENCE
Главное отличие состоит в том, что структура struct является Value ТИПом, а класс class — Reference ТИПом. Давайте поговорим немного подробнее о том, что значит Value ТИП в противоположность Reference ТИПу.
Reference ТИП повсюду передается с помощью указателей (pointers). Reference ТИПы “живут” в “куче” (heap). Когда вы создаете классы class, то они сохраняются в “куче” (heap).
Все знают, что это значит?
Они просто сохраняются в “памяти” и, если нужно передать куда-то эту переменную, то передают её по ссылке с помощью указателей (pointers) на неё. Множество людей может иметь где-то указатель (pointer) на тот же самый экземпляр класса class.
COW — ARC
Структуры struct НЕ передаются с помощью указателей (pointers), они КОПИРУЮТСЯ. Если вы передаете структуру struct в функцию в качестве аргумента, то эта функция получает копию структуры struct, даже если у меня есть одна переменная var one и другая переменная var another, то если я первой переменной присвою значение второй переменной one = another, то обе переменные будут отдельными копиями своих переменных.
Вы можете подумать: “Да вы издеваетесь надо мной? Я делаю копию массива Array, который является структурой struct и копируется весь огромный массив каждый раз, когда я передаю его в функцию или куда-то ещё?”
ОТВЕТ: Конечно, в реальности это не совсем так работает.
“За кулисами” при передачи таких вещей, как массив Array, Swift “копирует” эти структуры struct, но, конечно, не в смысле того, что он копируется бит за битом, а в том смысле, что при чтении данных используется общая копия, а в случае изменения данных (например, при записи) — создается новая копия. Если вы передаете массив Array в функцию, затем возможно копируете его в другую переменную и только потом добавляете что-то в этот массив. Когда вы добавляете что-то в массив, то создается реальная побитовая копия массива, потому что массив Array, который вы получили в результате добавления чего-то — это уже другая копия массива Array, отличающаяся от первоначального массива Array.
Это механизм копирования называется Copy-On-Write (COW), то есть “Копирование при записи”. Когда вы записываете что-то в структуру struct, то действительно делается физическая копия.
Но семантически каждый раз, когда вы передаете куда-то структуру struct, то она копируется, ВСЕГДА копируется.
Поэтому вы НИКОГДА не делаете общими (shared) эти структуры struct, они копируются.
С другой стороны, класс class передает повсюду указатели на себя, так что в результате идет подсчет ссылок на него. Смотрим, сколько указателей ссылаются на этот класс, это происходит автоматически, и если не остается ни одного указателя в “куче” на какой-то класс, то он убирается из “кучи”. Это называется Автоматическим Подсчетом Ссылок (Automatic Reference Counting или ARC).
ФУНКЦИОНАЛЬНОЕ — ОБЪЕКТНО_ОРИЕНТИРОВАННОЕ
Итак, у нас два совершенно разных представления о Мире.
Копировать в процессе передачи или использовать указатель (pointer).
Большинство того, что вы видите, это структуры struct. Это массивы Array, словари Dictionary, целые числа Int, булевские значения Bool, реальные числа с двойной точностью Double, все это структуры struct.
Структура struct в основном была создана для того, чтобы поддержать определенного рода программирование, которое называется Функциональным программированием. Функциональное программирование фокусируется на ФУНКЦИОНАЛЬНОСТИ вещей.
Классы class создавались для Объектно-ориентированного программирования. Объектно-ориентированное программирование фокусируется на ИНКАПСУЛЯЦИИ ДАННЫХ и ФУНКЦИОНАЛЬНОСТИ в некотором контейнере, объекте.
Это две совершенно разные концепции Мира. Они обе пытаются достичь аналогичных целей, которые включают в себя отчасти инкапсуляцию, а также понимание того, где “живет” ФУНКЦИОНАЛЬНОСТЬ в вашей программе. Но делают это они разными способами.
Об этом говорят ТИПЫ, на которых они построены: копирование при передаче или указатель (pointer) на них. И это приводит к совершенно разному поведению.
В течение этого семестра мы многое узнаем о Функциональном программировании и как оно работает. Уже в конце этой небольшой Лекции мы многое поймем относительно Функционального программирования.
Я полагаю, что большинство из вас знает, как работает Объектно-ориентированное программирование. Вы уже программировали на Java или C++ или на чем-то подобном.
НАСЛЕДОВАНИЕ
Структуры struct НЕ имеют наследования (inheritance). В Функциональном программировании нет никакого смысла в наследовании. У нас будет своего рода наследование (inheritance) в Функциональном программировании, и вы это увидите, но не со структурами struct.
У структур struct НЕТ наследования (inheritance).
Все классы class в Swift, конечно, имеют наследование (inheritance).
Конечно, у них может быть superclass, если вы этого хотите, но это “одиночное” наследование (single inheritance). Они могут наследовать только от одного класса class.
Это то, к чему мы привыкли. Так у Java тоже только “одиночное” наследование, C++ имеет “одиночное” наследование и т.д.
ОТСУТСТВИЕ и НАЛИЧИЕ НАСЛЕДОВАНИЯ — это также очень значительное различие между структурой struct и классом class.
ИНИЦИАЛИЗАЦИЯ
Я рассказывал вам об инициализаторах, специальных функциях с именем init. Структура struct получает “бесплатного” инициализатора init, который инициализирует все переменные vars в этой структуре. Имея “бесплатный” инициализатор, который инициализирует все переменные vars, мы смогли написать CardView (isFaceUp: true) и инициализировать переменную var isFaceUp этой структуры.
Класс class также получает “бесплатного” инициализатора init, но он не инициализирует НИКАКИЕ переменные vars. Это название класса и открывающая круглая скобка и закрывающая круглая скобка, это “бесплатный” инициализатор init. Всем вашим переменным vars нужно что-то присвоить после такой инициализации или создать свой собственный инициализатор init в классе class.
Для классов class мы почти ВСЕГДА пишем собственные инициализаторы init, так как у нас нет хорошего “бесплатного” инициализатора init.
Для структура struct смешанная ситуация : иногда мы делаем так, как с CardView и используем “бесплатного” инициализатора init, с инициализацией всех переменных vars, а иногда пишем свой собственный init.
——- 25 -ая минута лекции ———
ИЗМЕНЯЕМОСТЬ
Программирование с VALUE ТИПАМИ приводит к тому, что они повсюду копируются и их ИЗМЕНЯЕМОСТЬ (mutability или changeability) должна указываться ЯВНО для структур struct.
Если у вас есть структура struct, например, массив Array, и вы хотите добавить в этот массив какой-то элемент и сделать массив Array изменяемым, то вы должны ЯВНО сказать, что вы собираетесь это делать, и пометить свой массив Array ключевым словом var, а не let. Помните? Я сказал, что оба, и структур struct, и класс class, имеют константы let.
Если ваша переменная является структурой struct и она является let переменной, то вы не сможете изменить эту структуру. То есть если это массив Array, то вы ничего не сможете в него добавить. Но если ваша переменная является структурой struct и она является var переменной, то вы можете её изменять.
В то же время классы class, ВСЕГДА являются ИЗМЕНЯЕМЫМИ. Они “живут” в “куче” (heap), и если у вас есть указатели (pointers) на эти переменные, то через эти указатели (pointers) вы ВСЕГДА можете изменять то, что находится в в “куче” (heap).
НЕТ никакого контроля над ИЗМЕНЯЕМОСТЬЮ в классах class, что представляет собой очень большую проблему. Когда вы создаете код, используя классы class, вы должны очень хорошо думать, что вы делаете.
Если у кого-то есть указатель (pointer) на класс class, то он очень просто может пойти по этому указателю и изменить его. Это напоминает “Дикий Запад”, когда действительно трудно понять, что же происходит.
Иметь возможность ЯВНО указывать ИЗМЕНЯЕМОСТЬ — это реально очень хорошая возможность в Функциональном программировании и структурах struct.
ПРЕДПОЧТИТЕЛЬНАЯ СТРУКТУРА ДАННЫХ.
Структура struct — это ваша предпочтительная структура данных. В большинстве случаев вы сначала попытаетесь использовать структуру struct, использование класса class будет диктоваться только какими-то особыми обстоятельствами, и мы увидим одно из этих особых обстоятельств сегодня, им является ViewModel. ViewModel — это один из компонентов архитектуры MVVM и этот компонент ВСЕГДА является классом class.
ИСПОЛЬЗОВАНИЕ.
Старый способ программирования в iOS был основан на классах class, это было объектно-ориентированное программирование, а НЕ функциональное программирование.
Но почему ViewModel в архитектуре MVVM должна быть классом class?
Между прочим, я говорил об этом во время демонстрационного примера, но мы должны принять тот факт, что ViewModel необходимо “делится информацией” (share) со многими различными Views.
ViewModel — это своего рода “главный вход”, портал, в Model.
Множество различных Views хотели бы взглянуть на Model и они все хотят иметь доступ к этому порталу. Классы class являются замечательной вещью, дающей возможность “делится” (sharing) ресурсом, потому что у всех этих Views есть указатели на них.
Есть опасная сторона этого “разделения” ресурсов между различных Views, но мы пытаемся смягчить её в MVVM, и я покажу вам это в демонстрационном примере. Но в любом случае ViewModel — это пример использования класса class. Всё, что вы видели до сих пор, — это были структуры struct. Все эти Views (ContentView, CardView) являются структурами struct. Я говорил вам, что все массивы Array, целые числа Int, булевские значения Bool, реальные числа с двойной точностью Doubles, все диапазоны Range и т.д. являются структурами struct.
За исключением ТИПА View, он имеет совершенно другой ТИП, который называется протокол protocol.
View — это НЕ структура struct и НЕ класс class, это протокол protocol, и мы поговорим о протоколах protocol на следующей неделе.
Следующая вещь, о которой я бы хотел поговорить, это Generics.
Массив Array содержит ряд элементов, для чего, собственно, массив и предназначен, но ему совершенно всё равно, какого ТИПа эти элементы. Тем не менее внутри кода, реализующего массив Array, мы должны хранить эти элементы, для чего нам нужны некоторые переменные vars внутри кода. Эти vars запоминают элементы массива Array внутри реализующего кода.
Как нам решить эту головоломку, когда массиву Array необходимо хранить свои элементы, но он не беспокоиться о том, будет ли это массив Array целых чисел Int, или массив Array строк String, или массив Array других массивов Array, или массив Array множеств Set. Массив Array это абсолютно не волнует.
У массива Array есть также функции и переменные vars. Он позволяет вам делать определенные операции со своими элементами, например, добавлять новые элементы или получать значения элементов массива Array. Как нам в этом случае декларировать ТИПы возвращаемых переменных или ТИПы аргументов функций?
ОТВЕТ заключается в том, что мы делаем это с помощью GENERICS.
Другие языки программирования, например, Java также имеют GENERICS.
——- 30-ая минута лекции ———
Поэтому для некоторых из вас это будет своего рода повторение пройденного, но на следующей недели мы собираемся “поднять” GENERICS на следующий уровень, объединив их с некоторыми други особенностями ТИПов в Swift.
Давайте поговорим о том, как работают GENERICS с массивом Array.
Массив Array декларируется приблизительно следующим образом. Это структура struct с именем Array, в угловых скобках указан <Element>, а затем, например, определена функция funс append, у которой аргумент имеет ТИП Element.
Что такое ТИП Element?
Это то, о чем я говорил как о “Не важно какой” ТИП.
Element — это просто ТИП, которому массив Array дает имя, и это “Не Важно какой” ТИП.
Массиву Array тоже не важно, какой у Element ТИП. Это может быть массив Array целых чисел Int, или массив Array строк String, или массив Array чего угодно.
По сути здесь мы использует другого рода ТИП в Swift, которому я дал название “Не важно какой” ТИП.
Теперь по поводу реализации метода append, он также имеет дело с Element, ТИП которого “Не Важно какой”. Метод append не собирается посылать Element никаких сообщений или использовать какие-либо его переменные vars. Он просто запоминает Element. Так что по сути Element — это “заменитель” (placeholder) ТИПА.
Когда же происходит действительная установка реального ТИПА вместо “заменителя” (placeholder)?
Это происходит, когда люди используют массив Array в коде.
Если я декларирую массив Array, то я должен написать:
var a = Array <Int>
То есть разместить РЕАЛЬНЫЙ ТИП в угловых скобках. Это массив Array целых чисел Int.
Затем во всем коде Array происходит что-то наподобие того, что делаем, когда “search and replace” (“ищем и заменяем), то есть Element заменяется на Int.
Так что теперь наша функция append использует целое число Int в качестве аргумента.
Именно поэтому я могу написать a.append(5), и это прекрасно работает, так как 5 — это целое число Int, а append берет Int в качестве аргумента.
Итак, это использование массива Array с реальным ТИПом вместо Element при написании кода.
Заметьте, что если вы (как Array) используете “Не Важно какой” ТИП, то вам следует сообщить Миру об этом, потому что при использовании Array вам придется заменять “Не Важно какой” ТИП Element на реальный ТИП.
Это делается при декларировании в самом начале после имени с помощью угловых скобок вокруг Element :
Совершенно нормально, если у вас множество “Неважно какой” ТИПов:
struct Array <Element, Foo> { . . . .}
У вас может быть сколько угодно “Неважно какой” ТИПов, а затем тот, кто использует эту структуру, должен задать реальные ТИПы, соответствующие этим “Неважно какой” ТИПам.
Я назвал эти Swift ТИПы “Неважно какой” ТИПами.
Но реальное общепринятое имя такого ТИПа — это ПАРАМЕТР ТИПА (type parameter):
И последний Swift ТИП, о котором я хочу поговорить сегодня, это функции:
Функции — тоже люди!
Вы можете использовать функции как ТИПЫ повсюду, и синтаксис для этого очень простой и понятный. Синтаксис для определения ТИПА ФУНКЦИИ просто замечательный — при декларировании НЕТ никаких имен аргументов, только ТИПЫ присутствуют в этом определении.
На слайде представлены некоторые функции в качестве ТИПов. ТО, что представлено желтым цветом и есть ТИП. Просто представьте, что часть, выделенная желтым цветом, — это Int или String или Array< Int>.
На слайде представлены в точности такие ТИПЫ, как и только что перечисленные.
Например, у нас есть ТИП (Int, Int) -> Bool и это функция, которая берет в качестве аргументов два Ints и возвращает Bool.
Следующий ТИП (Double) -> Void и это функция, которая берет в качестве аргумента Double и НИЧЕГО НЕ возвращает.
Функция может не иметь аргументов и ТИП () -> Array<String> представляет такую функцию, которая не имеет аргументов и возвращается Array строк String.
Следующий ТИП (Double) -> Void представляет функцию, у которой НЕТ аргументов и она НИЧЕГО НЕ возвращает.
Всё это ТИПЫ, в них нет ничего особенного.
Это означает, что я могу декларировать переменную var одного из этих ТИПов.
var foo: (Double) -> Void
ТИП foo — это ”ФУНКЦИЯ, которая берет Double и НИЧЕГО НЕ возвращает”
У меня также может быть функция с аргументом what, у которого ТИП — это ФУНКЦИЯ, которая не берет никакие аргументы и возвращает Bool, предполагая, что в теле функции what что-то делается .
Давайте посмотрим, как мы используем некоторые из этих функций как ТИПы:
У нас есть переменная var с именем operation. Эта переменная имеет ТИП “ФУНКЦИИ, которая берет Double и возвращает Double.
Я создам функцию, которая берет Double и возвращает Double.
Это функция square, она возводит в квадрат Double число:
func square (operand: Double) -> Double {
return operand * operand
}
Теперь я могу присвоить переменной var с именем operation вполне определенное значение, и это будет выглядеть так:
operation = square
Переменная operation имеет ТИП “ФУНКЦИИ, которая берет Double и возвращает Double и square — это функция, которая берет Double и возвращает Double.
Так что эта операция присвоения вполне законная.
Теперь, когда у меня есть operation, я могу выполнить эту функцию :
let result1 = operation( 4 )
result1 получит значение 16, потому что это именно то, что делает функция func square.
Заметьте, что когда я вызывал operation, я НЕ написал operation (operand: 4), мы “сбросили” метку operand:. Это ВСЕГДА происходит, когда мы передаем что-то, что имеет ТИП ФУНКЦИИ. В этом случае этот ТИП ФУНКЦИИ теряет свои метки.
——- 35-ая минута лекции ———
Но я также могу присвоить моей переменной operation значение sqrt, где sqrt — это встроенная в Swift функция, которая берет Double и возвращает Double:
operation = sqrt
Функция sqrt вычисляет квадратный корень числа, это обычная функция.
Теперь, когда у меня есть новое значение operation, я могу выполнить эту функцию :
let result2 = operation( 4 )
На этот раз result2 получит значение 2, потому что значение переменной operation изменилось, теперь используется sqrt функция.
Все просто. Слишком просто, чтобы быть правдой.
Но что есть, то есть.
В демонстрационном примере, который я начну показывать через несколько секунд, мы создадим нашу собственную маленькую функцию, которая берет в качестве аргумента функцию.
Я думаю, вы слышали слово “замыкание” (closure), мы поговорим о замыканиях (closures) немного более подробно на следующей неделе или через неделю.
Замыкание (closure) — это по существу встроенная (inlining) в код функция. Она берет функцию, которую мы передаем как параметр в другую функцию и встраивает (inline) её непосредственно в код, вместо того, чтобы где-то отдельно её определять. Но Замыкание (closure) — это больше, чем просто “встраивание” (inlining) в код, это еще и “захват” локальных переменных, объявленных вне тела этой “встроенной” функции в окружающем коде и не являющихся её параметрами.
Вот о чем я собираюсь говорить на следующей неделе, но я покажу вам, как выглядит синтаксис “встроенной” (inlining) функции в демонстрационном примере, которым мы будем заниматься сегодня.
На сегодня это последний слайд.
Это то, о чем мы только что говорили:
Архитектура
|
Я возвращаюсь к демонстрационному примеру, в котором я попытаюсь показать всё, о чем мы сегодня говорили и я показывал вам на слайдах:
Помните, что вы должны воспроизвести этот демонстрационный пример в вашем первом Домашнем Задании на программирование, которое уже находится на Piazza. Посмотрите.
Давайте “выведем” наше приложение Memorize на следующий уровень, используя архитектуру MVVM для того, чтобы дать “мозги” нашей игре. То есть дадим нашей игре некоторую Логику и Данные, которыми являются карты.
——- 38-ая минута лекции ———