Содержание
Текст Домашнего Задания 2 на английском языке доступен на iTunes в пункте “Programming: Project 2: Calculator Brain″. На русском языке вы можете скачать здесь:
Задание 2 расширяет возможности калькулятора Calculator из Задания 1, позволяя ему вводить “переменные” и выполнять операцию Undo. Кроме того, вам необходимо внести в код все изменения, сделанные на лекции 3 (переменную program типа Property List и атрибуты управления доступом). Кроме того, выполнение всех его пунктов позволит вам овладеть навыками работы с такими синтаксическими конструкциями как enum, Dictionary, Array, кортежи, Optional, String, Наблюдатели Свойств.
Код Задания 2 представлен в двух вариантах:
- переменная как операнд — на Github для Xcode 7 и Swift 2.2.
- переменная как операция — на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Ниже представлено решение всех обязательных и дополнительных пунктов Задания 2.
Пункт 1 обязательный
Все изменения, сделанные с калькулятором Calculator на лекции, должны быть внесены в код вашего Задания 1. Это включает как var program, так и правильную установку доступа (access control) всех методов и свойств. Сделайте полученный Calculator полностью функционирующим прежде, чем вы приступите к выполнению остальных обязательных заданий. И, как и в прошлый раз, “печатайте” изменения в коде, а не используйте copy / paste.
Все уже сделано в Задании 1, но в этом пункте я переименую мой класс ViewController в CalculatorViewController. Это переименование состоит из 3-х шагов:
- переименовываем класс ViewController в CalculatorViewController.
2. переименовываем файл ViewController в CalculatorViewController.
3. изменяем класс, привязанный к экранному фрагменту на storyboard
Пункт 2 обязательный
Ничего не меняйте в non-private API в CalculatorBrain и продолжайте использовать Dictionary<String,Operation> в качестве основной внутренней структуры данных.
Пункт 3 обязательный
Ваш пользовательский интерфейс (UI) должен быть всегда синхронизирован с вашей Моделью (CalculatorBrain).
Все эти пункты выполнены в Задании 1.
Пункт 4 обязательный
Наделите ваш CalculatorBrain способностью вводить “переменные”. Сделайте это путем реализации следующего API в вашем CalculatorBrain …
func setOperand (variableName: String)
var variableValues: Dictionary<String, Double>
Это должно делать точно то, что говорят названия: первая функция должна вводить “переменную” как операнд (например, setOperand(“x”) будет использовать переменную с именем х), а вторая переменная позволит пользователю CalculatorBrain API установить любое значение для любой переменной (например, brain.variableValues [“x”] = 35.0. Ваш CalculatorBrain должен поддерживать любое число переменных. Вы можете предполагать,что никакое имя переменной не совпадает с символом операции.
Вначале реализуем переменную как операнд, что и говорит название метода
func setOperand (variableName: String)
Добавляем указанный выше API в класс CalculatorBrain.
Если словарь variableValues меняется, то result должен изменяться, отражая новые значения для “переменных”. Поэтому мы перезапускаем нашу program, то есть пересчитываем все, что мы ввели в калькулятор, но с новыми значениями переменных.
. . . . . . . . . . . . . . .
Вы видите, что, если значение переменной variable отсутствует в словаре
var variableValues: Dictionary<String, Double>, то берется значение 0, как значение по умолчанию с помощью оператора ??.
Описание var description должно продолжать работать правильно и должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят. Поэтому
descriptionAccumulator = variable
Свойство var program, добавленное на лекции, также нуждается в изменении для поддержания “переменных”.
В подсказке № 1 говорится, что «Даже если пользователи API класса CalculatorBrain вводят “переменную” с помощью метода с именем setOperand, нет причин, чтобы внутреняя реализация “переменных” в классе CalculatorBrain не использовала механизма “операций”, чтобы заставить это работать (возможно, это хорошая идея, так как внутри класса CalculatorBrain достаточно большая инфраструктура для управления операциями).» Мы рассмотрим этот вариант позже и это будет совсем другое приложение. А пока продолжим вариант реализации Задания, когда переменная представляется как операнд.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Пункт 5 обязательный
Результат var result должен теперь правильно отражать значения “переменных” (из словаря variableValues) всякий раз, когда используются “переменные” (как в прошлом, так и в будущем, см. 8e ниже). Если у “переменной” нет значения в словаре, то используйте 0.0 как ее значение. Если словарь variableValues меняется, то result должен изменяться, отражая новые значения для “переменных”.
Реализацию можно посмотреть в обязательном пункте 4.
Пункт 6 обязательный
Ваше описание var description должно продолжать работать правильно и должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят.
Реализацию можно посмотреть в обязательном пункте 4.
Пункт 7 обязательный
Свойство var program, добавленное на лекции, также нуждается в изменении для поддержания “переменных”.
Реализацию можно посмотреть в обязательном пункте 4.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Пункт 8 обязательный
- Добавьте две новых кнопки на UI калькулятора Calculator:→M и M. Не приносите в жертву никакие кнопки с требуемыми операциями из Задания 1 (хотя вы можете добавить больше операций, если хотите). Эти две кнопки будут соответственно устанавливать и получать переменную в CalculatorBrain, называемую M.
- →M устанавливает значение переменной M в brain в текущее значение на display и показывает на display результат result, полученный из brain.
- →M не должна выполнять setOperand.
- Нажатие M должно setOperand(“M”) в brain и затем показывать на display результат result, полученный из brain.
- →M и M являются механизмом Controller, а не механизмом Model (хотя они оба используют концепцию “переменных” в Model).
- Это не выдающаяся кнопка “memory” на нашем калькуляторе, но она является хорошим инструментом для тестирования, правильно ли работает концепция “переменных”, реализованная выше.
- Примеры …
- 9 + M = √ ⇒ description имеет вид √(9+M), display показывает 3, так как M не установлена (то есть равна 0)
- 7 →M ⇒ display теперь показывает 4 (квадратный корень 16), description все еще показывает √(9+M)
- +14 =⇒ display показывает 18, description теперь √(9+M)+14
Располагаем на storyboard кнопки →M и M и с помощью CTRL-перетягивания создаем для них @IBAction функции setM и pushM
Хотя методы @IBAction в названии ориентируются на переменную M, реализация методов setM и pushM предполагает произвольную переменную, которую мы читаем с заголовка кнопки:
Проверяем работоспособность на примерах, представленных в задании.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Пункт 9 обязательный
Убедитесь, что кнопка С вашего Задания 1 работает правильно в этом задании. В дополнение она должна убирать любые значения “переменной” M из variableValues Dictionary в CalculatorBrain ( а не устанавливать в 0 или какое-то другое значение). Это позволит вам тестировать случай “неустановленной” переменной.
Для придания гибкости API калькулятору — как нам советовали в подсказках — добавляем в CalculatorBrain еще два non-private метода clear и clearVariable
Хочу отметить, что метод removeAll() имеет параметр keepCapacity, который по умолчанию имеет значение true и означает сохранение области памяти, которую занимал массив. Нам же действительно нужно, чтобы массив «ушел» из «кучи», следовательно в нашем случае keepCapacity: false. Это эквивалентно коду
internalProgram = []
Используем обе этих функций в реализации кнопки С:
Вот как выглядит Action ClearAll:
Пункт 10 обязательный
Добавьте “Undo” к вашему калькулятору: в дополнительном пункте Задания 1 вы добавляли кнопку “backspace”, если пользователь ввел неверную цифру. Теперь мы говорим о комбинации “backspace” и реального “Undo” в единой кнопке. Если пользователь находится в середине ввода числа, то это кнопка работает как backspace. Если пользователь не находится в середине ввода числа, то должно сделать Undo последней вещи, которая была выполнена в CalculatorBrain. Не отменяйте запоминания значений переменной (но ДЕЛАЙТЕ отмену установки “переменной” как операнда).
Добавляем в API CalculatorBrain новый метод undoLast ()
И используем его в нашем Controller на кнопке «backspace»
После реализации операции «Undo» нам необходимо проверить работоспособность ситуации, указанной в подсказке № 3:
«Некоторые вещи (подобные π или результат другого выражения) несколько хитроумно запоминать в M после того, как вы ввели выражение, которое вы хотите оценить (например, ввели cos(M), а затем пытаетесь установить M в π). Как только вы реализуете Undo, вы сможете это сделать (путем “undoing” этого выражения, полученного просто нажатием π для вычисления значения, которое вы хотите запомнить в M).»
Для этого вводим:
M cos >⇒ description имеет вид cos(M) =, display показывает 1, так как M не установлена (то есть равна 0)
Затем вводим:
π →M ⇒ description> имеет вид π =, display показывает 3.141593
Undo ⇒ description имеет вид cos (M) =, display показывает -1, так как M установлена в π
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Пункт 1 дополнительный
Ваш Калькулятор должен сообщать об ошибках. Например, √ из отрицательного числа или деление на нуль. Есть несколько способов “обнаружения” этих ошибок (может быть добавить ассоциированное значение к Unary/ BinaryOperation вариантам, которые являются функциями, которые обнаруживают ошибку, или, возможно, заставить функцию, которая ассоциируется с Unary/ BinaryOperation возвращать кортеж как с результатом result, так и с ошибкой error (если она есть) или ???). То, как вы будете сообщать о любых обнаруженных ошибках пользователям CalculatorBrain API, потребует от вас некоторых изменений API, но не заставляйте пользователей CalculatorBrain API иметь дело с ошибками, если они этого не хотят (то есть позвольте Controllers, которые хотят показывать errors, делать это, а другим, которые не хотят показывать ошибки, позвольте иметь дело просто с NaN или +∞, появляющимися на вашем UI). Другими словами, не изменяйте никакие из существующих методов и свойств в non-private API CalculatorBrain для того, чтобы поддерживать эти возможности (вместо этого добавьте методы / свойства, если это необходимо).
Необходимо уметь спрашивать операции BinaryOperation и UnaryOperation, какие ошибки (если они есть) будут генерироваться при передачи в них операнда (ов).
Один из способов сделать это — иметь для BinaryOperation и UnaryOperation операций ассоциированное значение , которое представляют собой функцию, анализирующую потенциальные аргументы и возвращающую соответствующее String сообщение об ошибке, если выполнение операции генерирует ошибку (или nil в противном случае).
Большинство операций не могут сообщать о каких-то ошибках, да мы и не обязаны заставлять их это делать. В этом случае мы будем использовать nil в качестве функции, тестирующей ошибки. Можно сделать Optional целиком тип функции, размещая тип функции в круглых скобках и ставя знак ? после круглых скобок. Например, ((Double, Double ) -> String?)? — это Optional функция, которая берет два Double и возвращает Optional <String>.
Добавляем к операциям BinaryOperation и UnaryOperation новое ассоциированное значение в виде типа тестирующей ошибку функции.
Здесь интересны два варианта:
- первый — если сама тестирующая ошибки функция не установлена, то есть сама функция errorTest -это nil,
- второй — функция errorTest не nil, но она декларирована так, что возвращает nil. Это очень удобно.
В нашей реализации только некоторые унарные операции типа √, ln, x⁻¹, sin⁻¹, cos⁻¹ и бинарная операции «деление» получают тестирующую ошибку функцию.
Ошибку будем фиксировать в private переменной error: String?
. . . . . . . . . . . . . . . . . . . . . . . . . .
Прежде, чем выполнить унарную или бинарную операцию, тестируем полученные операнды на ошибку. Если ошибка обнаружена, то фиксируем ошибку в error.
. . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . .
Используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и здесь интересны два варианта:
- первый — если функция validator не установлена, например, для операций «sin«,»cos«,»tan«, то error равно nil,
- второй — функция validator не nil, но она декларирована так, что возвращает nil в случае отсутствия ошибки. Это очень удобно.
Здесь действительно очень удачно работают два перечисленных выше варианта. Если у вашей операции, например, «sin» или «cos» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции «√«, а аргумент не дает «Отрицательное значение», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator является очень удобным при обработке ошибок.
Бинарная операция может быть в отложенном состоянии, поэтому тестирующую функцию нужно запомнить в отложенной структуре PendingBinaryOperationInfo
. . . . . . . . . . . . . . . . . . . . . . . . . .
а затем использовать при вычислении на предмет проверки ошибки:
И опять используем синтаксис “цепочек” Optionals и пишем функции validator c вопросительным ? знаком и опять здесь удачно работают два перечисленных выше варианта. Если у вашей операции, например, «+» или «—» нет тестирующей функции, то error = nil и сообщение об ошибке не будет выдано. Если у вас есть тестирующая функция, как у операции деления «÷«,а аргументы не дают «Деление на ноль», то есть второй из вышеперечисленных вариантов, то также возвращается nil и сообщение об ошибке не будет. Именно это двоякое свойство определенной нами функции validator позволяет нам вести обработку ошибок одной строкой.
Для того, чтобы передать обнаруженную или не обнаруженную ошибку в CalculatorViewController, сформируем result в CalculatorBrain в виде кортежа, который содержит вычисленное значение accumulator типа Double и сообщение об ошибке error типа String?:
В CalculatorViewController добавим переменную кортеж resultValue, которая будет «встречать» результирующее значение result из CalculatorBrain, производить необходимую обработку кортежа с помощью оператора switch c паттернами соответствия образцу (pattern matching) и осуществлять всю необходимую настройку UI:
Для соответствующих кнопок, где результат отображается на дисплее, вместо dysplayValue, устанавливаем значение для новой переменной resultValue:
кнопка с операциями
кнопка backspace
кнопка →M
кнопка M
Пример работы. Если мы с помощью переменной M наберем функцию (sin(M)+M)÷M, то получим «Деление на ноль», так как если значение M не присвоено, то оно равно 0. Eсли мы затем пошлем в переменную M значение π/2, то значение выражения (sin(M)+M)÷M будет равно 1.63662.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.
Представление переменной как операции
Даже если пользователи API класса CalculatorBrain вводят “переменную” с помощью метода с именем setOperand, нет причин, чтобы внутреняя реализация “переменных” в классе CalculatorBrain не использовала механизма “операций”, чтобы заставить это работать (возможно, это хорошая идея, так как внутри класса CalculatorBrain достаточно большая инфраструктура для управлеия операциями).
Добавляем в enum Operation вариант для переменных, не требующий никаких ассоциированных значений:
В этом случае метод setOperand, для переменных будет выглядеть следующим образом.
Добавляем в словарь операций operation символ переменной variable как ключ, тип операции Operation.Variable как значение. И выполняем эту операцию с помощью метода performOperation, в который внесены необходимые изменения для Operation.Variable.
Если у “переменной” нет значения в словаре, то используйте 0.0 как ее значение:
accumulator = variableValues[symbol] ?? 0
Описание var description должно показывать имя “переменной” (а не ее значение) всякий раз, когда ее вводят, поэтому
descriptionAccumulator = symbol
Свойство var program, добавленное на лекции, в данном не нуждается в изменении для поддержания “переменных”, так как переменная выступает как обычная операция
Остальные пункты Задания 2 абсолютно идентичны тому, что изложено выше.
Код можно посмотреть на Github для Xcode 7 и Swift 2.2.
Если вы установили Xcode 8, то для Swift 2.3 код находится на Github, а для Swift 3 — также на Github.