Использование Классов, Структур и Перечислений

04 апреля 2016

Возвращаясь во времена, когда был только Objective-C, инкапсуляция была ограничена работой с классами. Тем не менее, на современных iOS и в программировании для Mac есть три варианта: в Swift есть enums (перечисления или энумы), structs (структуры) и classes (классы).

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

Цель данного туториала в следующем:

  • Предоставить вам опыт использования перечислений, структур и классов.
  • Выработать интуитивное понимание, когда их использовать.
  • Объяснить как работает каждый из них.

Данное руководство предполагает, что вы уже немного знакомы со Swift и объектно-ориентированным программированием.

Все о типах

Три основных преимущества Swift - это его безопасность, скорость и простота.

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

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

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

Ключ к достижению этого - система типов в Swift:

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

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

Есть, конечно, еще и Bool, Int, UInt, Float, Double, Character, String, Array, Set, Dictionary, Optional и др., о которых можно подумать как об основных типах. Тем не менее, они фактически построены из именованных типов и являются частью стандартной библиотеки Swift.

Этот туториал акцентирует внимание на так называемых именованных типах моделей, которые состоят из enum, struct и class.

Фигуры с Scalable Vector Graphics (SVG) (Масштабируемой Векторной Графикой)

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

SVG является векторным форматом изображений на основе XML для 2D-графики. Спецификация, разработанная W3C в 1999 году, открытого стандарта. (https://developer.mozilla.org/en-US/docs/Web/SVG)

Приступим

Создайте новый плейграунд в Xcode ( File\New\Playground…), назовите его Shapes и выберите платформу OS X. Нажмите Next, чтобы выбрать папку, где сохраните проект, а затем Create для сохранения файла. Очистите полностью файл, а затем вставьте следующее:


import Foundation

Нашей задачей является возможность отрисовки фигур формата XML :



 

Поверьте, ответ в браузере или во view WebKit выглядит лучше. :]

Нам нужен способ для отображения цвета. SVG использует тип цвета CSS3, который может быть указан в качестве имени, RGB или HSL. Полную спецификацию можно найти здесь: http://www.w3.org/TR/css3-color/.

Чтобы использовать цвет в SVG, вы определяете его как атрибут части вашего рисунка. Например, fill = 'Gray'. Чтобы реализовать это в Swift, то можно просто использовать let fill = "Gray".

Использовать String легко и продуктивно, но есть недостатки:

  1. Это чревато ошибками. Любые строки, которые не являются частью спецификации цвета, будут компилироваться, но не правильно отображаться во время выполнения. Например, если "Grey" написать через "е", то код работать не будет.
  2. Автозаполнение не предложит вам подходящие названия цветов.
  3. Когда вы передаете цвет в качестве параметра, то не всегда очевидно, что строка является цветом.

Перечисления - ваше спасение

Использование пользовательского типа решит эту проблему. Если вы пришли из Cocoa Touch, вам может прийти в голову реализовать инкапсулированный класс как UIColor. И хотя использование этого класса может и сработать, но Swift дает больше возможностей для определения модели.

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

Как вариант можно его реализовать вот так:


enum ColorName {
  case Black
  case Silver
  case Gray
  case White
  case Maroon
  case Red
  // ... и так далее ... 
}

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

На перечисления, которые явно указывают тип хранения, ссылаются как на RawRepresentable, потому что они автоматически принимают протокол RawRepresentable.

Таким образом, вы можете указать тип ColorName как String и присвоить значение каждому конкретному кейсу, например, вот так:


enum ColorName : String {
  case Black  = "Black"
  case Silver = "Silver"
  case Gray   = "Gray"
  case White  = "White"
  case Maroon = "Maroon"
  case Red    = "Red"
  // ... и так далее ... 
}

Тем не менее, Swift ведет себя особенно с перечислениями со String отображением. Если вы не уточняете, чему равен кейс, то компилятор автоматически делает String таким же, как имя кейса. Это означает, что вам нужно всего лишь написать имя кейса:


enum ColorName : String {
  case Black
  case Silver
  case Gray
  case White
  case Maroon
  case Red
  // ... и так далее ... 
}

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

Добавьте следующий код в конец вашего плейграунда:


enum ColorName : String {
  case Black, Silver, Gray, White, Maroon, Red, Purple, Fuchsia, Green, Lime, Olive, Yellow, Navy, Blue, Teal, Aqua
}

Теперь у вас есть пользовательский тип первого класса и все, что с ним связано. Например,


let fill = ColorName.Grey   // ERROR: Цвета с опечаткой в названии не будут компилироваться! Отлично!
let fill = ColorName.Gray   // Будет работать автодополнение! Круть!

Связанные значения

ColorName подходит для именованного цвета, но я думаю вы помните, что CSS цвета имеют несколько вариантов отображения: именованные, RGB, HSL и другие. Как же их смоделировать?

Перечисления в Swift прекрасно подходят для моделирования того, что имеет одно из ряда вариантов отображения, например цвет CSS, и каждый кейс перечислений может быть объединен со своими собственными данными. Эти данные называются associated values (связанные значения).

Определите цвет CSSColor, используя перечисление, добавив следующее в ваш плейграунд:


enum CSSColor {
  case Named(ColorName)
  case RGB(UInt8, UInt8, UInt8)
}

Этим определением, вы придаете модели CSSColor один из двух кейсов.

  1. CSSColor может быть Name, и в этом случае связанные данные это значение ColorName
  2. CSSColor может быть RGB, в этом случае связанные данные это три UInt8 (0-255) цифры для красного, зеленого и синего цветов.

Обратите внимание, что мы для краткости опускаем RGBA, HSL и HSLA кейсы.

Протоколы и методы с перечислениями

Вы хотите иметь возможность напечатать несколько экземпляров CSSColor.

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

Ключом к взаимодействию со стандартной библиотекой Swift является принятие стандартных протоколов библиотеки.

Добавьте следующее расширение для CSSColor в вашем плейграунде:


extension CSSColor : CustomStringConvertible {
  var description: String {
    switch self {
    case .Named(let colorName):
      return colorName.rawValue
    case .RGB(let red, let green, let blue):
      return String(format: "#%02X%02X%02X", red,green,blue)
    }
  }
}

Это заставит CSSColor соответствовать CustomStringConvertible. И это как раз то, что сообщает Swift, что наш тип, CSSColor, может быть преобразован в строку, а вот как он должен быть преобразован мы определяем в вычисляемом свойстве description.

В этой реализации, self прогоняем через оператор switch, для того, чтобы определить, является ли базовая модель именованным или RGB типом. В каждом случае вы преобразуете цвет в требуемый формат строки для этого кейса. Именованный кейс просто возвращает имя строки, в то время как в RGB кейс возвращает красный, зеленый и синий цвет в требуемом формате.

Добавьте следующее в ваш плейграунд:


let color1 = CSSColor.Named(.Red)
let color2 = CSSColor.RGB(0xAA, 0xAA, 0xAA)
print("color1 = \(color1), color2 = \(color2)") // prints color1 = Red, color2 = #AAAAAA

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

Заметка

 

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

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

Инициализаторы с перечислениями

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

Добавьте это расширение в ваш плейграунд:


extension CSSColor {
  init(gray: UInt8) {
    self = .RGB(gray, gray, gray)
  }
}

Добавьте следующее в плейграунд:


let color3 = CSSColor(gray: 0xaa)
print(color3)  //  prints #AAAAAA

Пространства имен с перечислениями

Именованные типы могут выступать в качестве пространства имен для соблюдения организованности и сведения сложности к минимуму. Вы создали ColorName и CSSColor, и все же ColorName будет использоваться только в контексте CSSColor.

Но ведь было бы здорово, если бы вы могли скрывать ColorName внутри модели CSSColor?

А ведь вы можете! Удалите ColorName из вашего плейграуда и замените его следующим кодом:


extension CSSColor {
    enum ColorName : String {
        case Black, Silver, Gray, White, Maroon, Red, Purple, Fuchsia, Green, Lime, Olive, Yellow, Navy, Blue, Teal, Aqua
    }
}

Это переместит ColorName в расширение CSSColor. Теперь ColorName спрятан, а внутренний тип определен как CSSColor.

Заметка

 

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

Тем не менее, если вы получаете сообщение об ошибке в вашем плейграунде о том, что ColorName необъявленного типа, переместите расширение чуть ниже вашего определения перечислений CSSColor, чтобы устранить ошибку плейграунда.

Иногда плейграунды чувствительны к упорядочению определений, даже если это на самом деле и не важно. :]

Перечисления могут быть созданы в виде чистого пространства имен, которые пользователи не смогут случайно создать, используя экземпляр. Например, вам скоро понадобится математическая константа pi, для выполнения некоторых вычислений. И пока вы будете использовать фреймворк Foundation и его M_PI, параллельно вы будете определять свою собственную константу, для того, чтобы сохранить все на столько портативным, насколько это возможно. (Вот и мы и пришли к Arduino микро-контроллеру!)

Добавьте следующий код в конец вашего плейграунда:


enum Math {
  static let pi = 3.1415926535897932384626433832795028841971694
}

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

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

Переоценка перечислений (энумов)

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

До сих пор вы использовали перечисления для моделирования цветов CSS. Это хорошо работает, потому что цвета CSS - это понятная, фиксированная спецификация W3C.

Перечисления отлично подходят для выбора элементов из списка известных понятий, таких, как дни недели, лиц на монетах. Это не удивительно, что в Swift опционалы реализованы в помощью перечислений с состоянием . None или . Some с соответствующим значением.

С другой стороны, если вы хотите, чтобы CSSColor был расширяемым пользователем до других моделей цветового пространства, не определенных в спецификации W3C, то перечисление не является предпочтительной абстракцией.

Которая, кстати, приведет вас к следующему именованному типу модели в Swift, Structures (структурам). :]

Использование структур

Если вы хотите, чтобы у ваших пользователей была возможность определять свои собственные фигуры внутри SVG, то использование enum не самый хороший выбор для определения типов формы.

Новые кейсы enum не смогут быть добавлены позже в расширение. Значит остается либо class или struct.

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


protocol Drawable {
  func draw(context: DrawingContext)
}

Протокол определяет, что значит Drawable. У него есть метод draw, который рисует то, что называется DrawingContext.

Говоря о DrawingContext - это просто еще один протокол. Добавьте его в свой плейграунд следующим образом:


protocol DrawingContext {
  func draw(circle: Circle)
  // ... другие примитивы ...
}

DrawingContext умеет рисовать чистые геометрические типы: Circle (круг), Rectangle (прямоугольник) и другие примитивные фигуры. Обратите внимание на то, что, собственно, технология рисования не определена, но вы можете ее реализовать через - SVG, HTML5 Canvas, Core Graphics, OpenGL, Metal и т.д.

Вы готовы определить круг, который принимает протокол Drawable. Добавьте это в ваш плейграунд:


struct Circle : Drawable {
  var strokeWidth = 5
  var strokeColor = CSSColor.Named(.Red)
  var fillColor = CSSColor.Named(.Yellow)
  var center = (x: 80.0, y: 160.0)
  var radius = 60.0
 
  // Соответствие протоколу Drawable.
 
  func draw(context: DrawingContext) {
    context.draw(self)
  }
}

В структуре вы группируете свойства хранения. Здесь вы реализовали следующие свойства:

  • strokeWidth: Ширину линии.
  • strokeColor: Цвет линии.
  • fillColor: Цвет заливки круга.
  • center: Центральную точку круга.
  • radius: Радиус окружности.

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

Ссылочный тип и тип значения

Типы значений работают как отдельные и отличные сущности.

Квинтэссенциальный тип значения является Int, так как именно он так работает в большинстве языков программирования. Если вы хотите знать, как именно работает тип значения, то задайте вопрос: "Что бы сделал Int?" Например:

Для Int:


var a = 10
var b = a
a = 30     // b все еще равен 10.
a == b     // false

Для Circle (определенный с помощью структур):


var a = Circle()
a.radius = 60.0
var b = a
a.radius = 1000.0  // b.radius все еще равен 60.0

Если вы сделали свой круг из типа класса, он получил бы ссылочную семантику. Это означает, что он ссылается на основной общий объект.

Для Circle, определенного с помощью класса:


var a = Circle()  // a class based circle
a.radius = 60.0
var b = a
a.radius = 1000.0  // b.radius also becomes 1000.0

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

Модель Прямоугольника

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


struct Rectangle : Drawable {
  var strokeWidth = 5
  var strokeColor = CSSColor.Named(.Teal)
  var fillColor = CSSColor.Named(.Aqua)
  var origin = (x: 110.0, y: 10.0)
  var size = (width: 100.0, height: 130.0)
 
  func draw(context: DrawingContext) {
    context.draw(self)
  }
}

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


protocol DrawingContext {
  func draw(circle: Circle)
  func draw(rectangle: Rectangle)
  // ... другие примитивы ...
}

Circle и Rectangle принимают протокол Drawable. Они переносят фактическую работу на что-то, что соответствует протоколу DrawingContext.

Теперь пришло время сделать конкретную модель, которая нарисует изображение в SVG стиле. Добавьте это в ваш плейграунд:


final class SVGContext : DrawingContext {
 
  private var commands: [String] = []
 
  var width = 250
  var height = 250
 
  // 1
  func draw(circle: Circle) {
    commands.append("")
  }
 
  // 2
  func draw(rectangle: Rectangle) {
    commands.append("")
  }
 
  var SVGString: String {
    var output = ""
    for command in commands {
      output += command
    }
    output += ""
    return output
  }
 
  var HTMLString: String {
    return "" + SVGString + ""
  }
}

SVGContext - это класс, который обертывает частный массив командных строк. В разделах 1 и 2 вы подписываетесь под протокол DrawingContext, а методы рисования просто добавлены в строку с правильным XML для рендера фигуры.

И, наконец, вам нужен тип документа, который сможет содержать множество Drawable объектов, поэтому добавьте следующее в ваш плейграунд:


struct SVGDocument {
  var drawables: [Drawable] = []
 
  var HTMLString: String {
    let context = SVGContext()
    for drawable in drawables {
      drawable.draw(context)
    }
    return context.HTMLString
  }
 
  mutating func append(drawable: Drawable) {
    drawables.append(drawable)
  }
}

Здесь, HTMLString - это вычисляемое свойство в SVGDocument, которое создает SVGContext и возвращает HTMLString из контекста.

Покажите мне немного SVG

Как насчет того, чтобы наконец-то нарисовать SVG? Добавим следующее в плейграунд:


var document = SVGDocument()
 
let rectangle = Rectangle()
document.append(rectangle)
 
let circle = Circle()
document.append(circle)
 
let HTMLString = document.HTMLString
print(HTMLString)

Это создаст круг и прямоугольник по умолчанию и поместит их в документ, а затем он выведет XML.

Теперь давайте сделаем наглядным SVG. Добавьте следующие строки в конце плейграунда:


import WebKit
import XCPlayground
let view = WKWebView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
view.loadHTMLString(HTMLString, baseURL: nil)
XCPlaygroundPage.currentPage.liveView = view

Это небольшой фокус плейграунда и он настраивает веб-просмотр для просмотра SVG. Нажмите Command-Option-Return, чтобы увидеть это веб-вью в assistant editor.

Использование классов

До сих пор вы использовали комбинацию структур (типов значения) и протоколы для реализации Drawable моделей.

Теперь пришло время поиграть с классами. Они позволят определить базовые классы и производные классы. Более традиционный объектно-ориентированный подход к вопросу фигур - это создать базовый класс Shape с методом draw().

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

А в коде это будет выглядеть как следующий блок - это просто для справки, так что не добавляйте его в свой плейграунд:


class Shape {
  var strokeWidth = 1
  var strokeColor = CSSColor.Named(.Black)
  var fillColor = CSSColor.Named(.Black)
  var origin = (x: 0.0, y: 0.0)
  func draw(context: DrawingContext) { fatalError("not implemented") }
}
 
class Circle : Shape {
  override init() {
    super.init()
    strokeWidth = 5
    strokeColor = CSSColor.Named(.Red)
    fillColor = CSSColor.Named(.Yellow)
    origin = (x: 80.0, y: 80.0)
  }
 
  var radius = 60.0
  override func draw(context: DrawingContext) {
    context.draw(self)
  }
}
 
class Rectangle : Shape {
  override init() {
    super.init()
    strokeWidth = 5
    strokeColor = CSSColor.Named(.Teal)
    fillColor = CSSColor.Named(.Aqua)
    origin = (x: 110.0, y: 10.0)
  }
 
  var size = (width: 100.0, height: 130.0)
  override func draw(context: DrawingContext) {
    context.draw(self)
  }
}

Чтобы сделать объектно-ориентированное программирование более безопасным, Swift ввел ключевое слово override. Это нужно для того, чтобы вы, программисты, понимали, когда вы что-то переопределяете.

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

Тем не менее, в этом объектно-ориентированном подходе есть свои недостатки.

Первую проблему вы заметите в реализации базового класса рисунка. Shape хочет избежать неправильного использования, поэтому он вызывает fatalError() и предупреждает производные классы, что они должны переопределить этот метод.

К сожалению, эта проверка происходит во время выполнения, а не во время компиляции.

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

В-третьих, это может быть сложно в будущем для определения базового класса.

Например, предположим, что вы хотите добавить Drawable тип Line. Для того чтобы работать с существующей системой, она должна наследовать от Shape, что немного неправильно.

Кроме того, ваш класс Line должен инициализировать свойство базового класса fillColor, и что не имеет смысла для линии.

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

Наконец, классы имеют эталонную семантику, которая обсуждалась ранее. В то время как Automatic Reference Counting (ARC) большую часть времени заботится о процессах, вы должны быть осторожны с тем, чтобы не ввести зацикленные ссылки, или в конечном счете все закончится утечкой памяти.

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

Для чего вообще использовать класс?

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

Для начинающих, они позволяют принимать прошедшие огонь и воду фреймворки, такие как Cocoa и Cocoa Touch.

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

Вычисляемые Свойства

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

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

Добавьте следующий код в конец вашего плейграунда:


extension Circle {
  var diameter: Double {
    get {
      return radius * 2
    }
    set {
      radius = newValue / 2
    }
  }
}

Это реализует новое вычисляемое свойство, которое основывается исключительно на радиусе. Когда вы получаете диаметр, он возвращает удвоенный радиус. При установке диаметра, он устанавливает радиус, деленный на 2. Проще простого!

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

Добавьте следующий код в только что добавленное расширение круга:


// Пример вычисляемого свойства, имеющего только геттер
var area: Double {
  return radius * radius * Math.pi
}
var perimeter: Double {
  return 2 * radius * Math.pi
}

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

Например, добавьте следующее в расширение Circle:


func shift(x: Double, y: Double) {
  center.x += x
  center.y += y
}

Этим мы пытаемся определить метод shift() на circle, который будет перемещать круг в пространстве. т.е. изменять центральную точку.

Но тогда выбрасывается следующее сообщение об ошибке на две строки, которые инкрементируют свойства center.x и center.y.


// ERROR: Left side of mutating operator has immutable type ‘Double'

Мы можем это исправить путем добавления ключевого слова mutating, например, вот так:


mutating func shift(x: Double, y: Double) {
  center.x += x
  center.y += y
}

Это сообщает Swift, что все OK, и что ваша функция меняет (мутирует) структуру.

Ретроактивное моделирование

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

Вот случай использования: Предположим, вы являетесь пользователем кода SVG, и хотите добавить свойства area и perimeter вашему Rectangle так же, как у Circle.

Давайте посмотрим, как это работает:


extension Rectangle {
  var area: Double {
    return size.width * size.height
  }
  var perimeter: Double {
    return 2 * (size.width + size.height)
  }
}

Ранее вы использовали extension для добавления методов к уже существующей модели, а теперь, вы будете формулировать эти методы в новом протоколе.

Добавьте это в ваш плейграунд:


protocol ClosedShapeType {
  var area: Double { get }
  var perimeter: Double { get }
}

Это даст вам официальный протокол.

Далее, вы сообщаете кругу и прямоугольнику принять этот протокол “задним числом”, добавив следующую строку в ваш плейграунд:


extension Circle: ClosedShapeType {}
extension Rectangle: ClosedShapeType {}

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

Добавим следующий код:


func totalPerimeter(shapes: [ClosedShapeType]) -> Double {
  return shapes.reduce(0) { $0 + $1.perimeter }
}
totalPerimeter([circle, rectangle])

Здесь используется reduce для вычисления суммы периметров.

Итог

Загрузите Конечный проект.

В этом туториале вы узнали о enum, struct и class - именованных типах моделей в Swift.

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

Тем не менее, они имеют и существенные различия.

Перечисления являются типами значений (value types) с набором кейсов, где каждый кейс может иметь различные соответствующие значения (associated values). Каждое значение типа перечисления представляет собой единственный кейс, как это определено в перечислении. У них не может быть свойств хранения.

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

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

Но в отличие от структур и перечислений, классы используют ссылки, aka sharing, семантику.

Что дальше?

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

Урок подготовил: Акулов Иван

Источник урока: Источник

Содержание