Создание собственной коллекции с протоколами в Swift

07 апреля 2021

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

Array, Dictionary и Set часто используемые типы коллекций, которые идут в составе стандартной библиотекой в Swift. Но что, если они не предоставляют все необходимое для вашего приложения прямо из коробки? Не беспокойтесь, ведь вы можете создавать свои собственные коллекции, используя протоколы стандартной библиотеки Swift!

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

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

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

В этом туториале вы узнаете как:

  • Применять эти протоколы: Hashable, Sequence, Collection, CustomStringConvertible, ExpressibleByArrayLiteral и ExpressibleByDictionaryLiteral.
  • Создавать кастомные инициализации для своих коллекций.
  • Улучшать свои коллекции с помощью кастомных методов.

Заметка

Этот туториал направлен на работу с Swift 5.0. Предыдущие версии не будут компилироваться из-за серьезных изменений в стандартной библиотеке Swift.

Приступим к работе

Начните с загрузки материалов проекта. Затем откройте файл Bag.playground в папке starter.

Заметка

Если вам будет так удобнее, вы можете создать свой собственный Xcode Playground, удалив весь код, чтобы начать с нуля.

Создание структуры Bag

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

struct Bag {
}

Ваша структура Bag является дженериком, имеющим Hashable элементы. Требование Hashable элементов позволяет сравнивать и хранить уникальные значения с временной сложностью O(1). Это означает, что независимо от размера контента, Bag будет работать с постоянной скоростью. Также обратите внимание, что вы используете struct, который навязывает семантику значений так, как это делает Swift для стандартных коллекций.

Bag схож с Set тем, что не хранит повторяющиеся значения. Различие в том, что Bag, в отличие от Set, ведет подсчет повторяющихся значений.

Приведем аналогию со списком покупок в магазине. Если нам нужно, например, взять с собой две бутылки воды, то вы не будете писать “бутылка воды” дважды, вместо этого, вы напишете “2 бутылки воды”.

Чтобы смоделировать это, добавьте следующие свойства Bag:

// 1
fileprivate var contents: [Element: Int] = [:]

// 2
var uniqueCount: Int {
  return contents.count
}

// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

Это основные свойства, нужные для Bag. Вот, что они делают:

  1. contents использует Dictionary в качестве внутренней структуры данных. Это прекрасно работает, потому что Bag обеспечивает уникальные ключи, которые вы будете использовать для хранения элементов. Значением для каждого элемента является его количество. Обратите внимание, что вы помечаете это свойство как fileprivate для того, чтобы скрыть внутреннюю работу Bag от внешнего мира.
  2. uniqueCount возвращает количество уникальных предметов, игнорируя их индивидуальные количества. Например, Bag с 4 апельсинами и 3 яблоками вернет значение 2 для uniqueCount.
  3. totalCount возвращает общее количество элементов в Bag. В приведенном выше примере значение totalCount будет равно числу 7.

Добавим методы редактирования

Теперь вы реализуете несколько методов для редактирования содержимого Bag.

Добавление метода add

Добавьте следующий метод под только что добавленными в код свойствами:

// 1
mutating func add(_ member: Element, occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0,
    "Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

Давайте разберемся, что делают эти свойства:

  1. add(_:occurrences:) предоставляет возможность добавить элементы в Bag. Он использует два параметра: универсальный тип Element и опциональное количество вхождений. Вы помечаете метод mutating для того, чтобы можно было изменять переменную contents.
  2. precondition(_:_:) требует не менее одного вхождения. Если это условие не выполняется, String, следующий за этим условием, отобразится в отладчике (Debugger).
  3. Этот раздел проверяет, существует ли элемент уже в Bag. Если это так, то счет увеличивается. Если это не так, он создает новый элемент.

Заметка

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

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

Реализация метода Remove

Добавьте следующее после add(_:occurrences:):

mutating func remove(_ member: Element, occurrences: Int = 1) {
  // 1
  guard 
    let currentCount = contents[member],
    currentCount >= occurrences 
    else {
      return
  }

  // 2
  precondition(occurrences > 0,
    "Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

Вы наверное заметили, что remove(_:occurrences:) использует те же параметры, что и add(_:occurrences:).

Давайте разберем, как он работает:

  1. Во-первых, он проверяет, существует ли элемент и имеет ли он по крайней мере то количество вхождений, которое удаляет вызывающий объект. Если это не так, метод возвращается.
  2. Затем он удостоверяется, что число вхождений, которые нужно удалить, больше 0.
  3. Наконец, он проверяет, больше ли текущее количество элементов, чем число вхождений для удаления. Если больше, то он устанавливает новый счетчик элемента, вычитая количество вхождений, которые нужно удалить из текущего счета. Если не больше, чем currentCount, и occurrences равны, то он полностью удаляет элемент.

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

Однако, не всё потеряно. Swift предоставляет инструменты, необходимые для преобразования Bag в коллекцию. Вам нужно лишь соответствовать некоторым протоколам.

Соответствие протоколам

В Swift протокол определяет набор свойств и методов, которые должны быть реализованы в объекте, подписанным под этот протокол. Чтобы подписать тип под протокол, добавьте двоеточие после определения class или struct, а затем напишите имя протокола, требования которого хотите реализовать. После того, как вы указали на соответствие протоколу, реализуйте необходимые переменные и методы вашего для объекта. После этих действий ваш объект будет соответствовать протоколу.

Вот простой пример. В настоящее время объекты Bag предоставляют мало информации на боковой панели результатов.

Добавьте следующий код в конец Playground`а (вне структуры), чтобы увидеть Bag в действии:

var shoppingCart = Bag()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

Затем нажмите Command-Shift-Enter, чтобы начать выполнение.

Создастся новый Bag, в котором будут некоторые фрукты. Если взглянуть на Playground Debugger, можно увидеть тип объекта без какого-либо его содержимого.

Соответствие CustomStringConvertible

К счастью, Swift предоставляет протокол CustomStringConvertible для этой ситуации. Добавьте следующее сразу после закрывающей скобки Bag:

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

Соответствие CustomStringConvertible требует реализации свойства с именем description. Это свойство возвращает текстовое представление конкретного экземпляра.

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

Нажмите Command-Shift-Enter, чтобы запустить проект.

Взгляните на недавно улучшенную отладочную информацию для shoppingCart:

Отлично. Теперь, добавляя функциональность в Bag, вы можете проверить его содержимое.

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

Создание инициализаторов

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

Добавьте следующий код в конец Playground`а, но обратите внимание, что он еще не будет компилироваться:

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary,
  "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary)
precondition(dictionaryBag.contents == dataDictionary,
  "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1],
  "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

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

Добавьте следующие методы чуть ниже totalCount внутри реализации Bag:

// 1
init() { }

// 2
init(_ sequence: S) where
  S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init(_ sequence: S) where
  S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

Рассмотрим то, что только что добавили:

  1. Во-первых, вы создали пустой инициализатор. Вы должны добавить его при определении дополнительных init методов.
  2. Затем Вы добавили инициализатор, который принимает все, что соответствует протоколу Sequence, где элементы этой последовательности совпадают с элементами протокола Bag. Это охватывает типы Array и Set. Вы перебираете пройденное последовательно и добавляете каждый элемент по одному за раз.
  3. После этого вы добавили аналогичный инициализатор, однако, он, в отличие от предыдущего, принимает кортежи типа (Element, Int). Примером этого может служить Dictionary. Здесь вы перебираете каждый элемент в последовательности и добавляете заданное количество.

Нажмите Command-Shift-Enter чтобы запустить проект еще раз. Обратите внимание, что код, который вы добавили в нижней части ранее, теперь работает.

Инициализация Коллекций

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

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

Чтобы увидеть это, сначала добавьте следующий код в конец вашего Playground`а:

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

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary,
  "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary,
  "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

Приведенный выше код является примером инициализации с использованием литералов Array и Dictionary, а не объектов.

Теперь, чтобы заставить их работать, добавьте следующие два расширения чуть ниже расширения CustomStringConvertible:

// 1
extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

// 2
extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}
  1. ExpressibleByArrayLiteral используется для создания Bag из литерала array. Здесь вы используете инициализатор, созданный ранее, и передаете его в коллекцию elements.
  2. ExpressiblebyDictionaryLiteral делает то же самое, но для литералов Dictionary. Map преобразует элементы в именованные кортежи, ожидаемые инициализатором.

Понимание кастомных коллекций

Теперь вы узнали достаточно, чтобы понять, что такое кастомная коллекция на самом деле. Объект коллекции, который вы определяете, должен соответствовать протоколам Sequence и Collection.

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

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

  • map(_:) возвращает массив результатов после преобразования каждого элемента в последовательности с использованием предоставленного замыкания.
  • filter(_:) возвращает массив элементов, удовлетворяющих предоставленному предикату замыкания.
  • sorted(by:) возвращает массив элементов в последовательности, отсортированной на основе предоставленного предиката замыкания.

Это самая малость из всех возможностей. Чтобы просмотреть все методы, доступные в Sequence, ознакомьтесь с документацией Apple по протоколу Sequence.

Применение Non-destructive итераций

Одно предостережение: последовательность не требует, чтобы соответствующие типы были non-destructive. Это означает, что после итерации нет никакой гарантии, что будущие итерации начнутся с самого начала. Это огромная проблема, если вы планируете выполнять итерацию над своими данные более одного раза.

Чтобы обеспечить non-destructive итерацию, ваш объект должен соответствовать протоколу Collection.

Collection наследуется от Indexable и Sequence.

Главное отличие состоит в том, что коллекция - это последовательность, которую можно пройти несколько раз и к которой можно получить доступ по индексу.

Вы получите множество методов и свойств просто соответствуя Collection. Вот некоторые примеры:

  • isEmpty возвращает логическое значение, указывающее, является ли коллекция пустой или нет.
  • first возвращает первый элемент в коллекции.
  • count возвращает количество элементов в коллекции.

Узнайте больше, прочитав документацию от Apple по протоколу Collection.

Соответствие протоколу Sequence

Наиболее распространенным действием, выполняемым c коллекциями, является перебор ее элементов. Например, добавьте в конец Playground`а следующее:

for element in shoppingCart {
  print(element)
}

Как и в случае с Array и Dictionary, вы должны иметь возможность провести цикл через Bag. Компиляции не будет, потому что в настоящее время Bag не соответствует Sequence.

Нужно это исправить.

Соответствие Sequence

Добавьте следующее сразу после ExpressibleByDictionaryLiteral:

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

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

  1. Мы создали typealias названный Iterator как DictionaryIterator. Sequence требует этого, чтобы знать, как вы повторяете свою последовательность. DictionaryIterator - это тип, который объекты Dictionary используют для перебора своих элементов. Вы используете этот тип, потому что Bag хранит свои базовые данные в Dictionary.
  2. Мы определили makeIterator() как метод, возвращающий Iterator для пошагового выполнения каждого элемента последовательности.
  3. Мы возвратили итератор, делегировав функцию makeIterator() на contents, которое само соответствует Sequence.

Это всё, что нужно, чтобы Bag соответствовал Sequence.

Теперь вы можете перебирать каждый элемент Bag и получать подсчет для каждого объекта. Добавьте следующее в конец Playground`а после предыдущего цикла for-in:

for (element, count) in shoppingCart {
  print("Element: \(element), Count: \(count)")
}

Нажмите Command-Shift-Enter, чтобы запустить проект. Откройте консоль Playground`а, и вы увидите распечатку элементов и их количество в последовательности.

Рассмотрим преимущества Sequence

Возможность итерации через Bag позволяет использовать множество полезных методов, реализуемых Sequence. Добавьте следующее в конец Playground`а, чтобы увидеть некоторые из них в действии:

// Находим все элементы с количеством более 1
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(
  moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Ожидаем, что moreThanOne будет таким [(\"Banana\", 2)]")

// Получаем массив элементов без указания их количества
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(
  itemList == ["Orange", "Banana"] ||
    itemList == ["Banana", "Orange"],
  "Ожидаем, что itemList будет таким [\"Orange\", \"Banana\"] or [\"Banana\", \"Orange\"]")

// Находим общее количество элементов в Bag
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3,
  "Ожидаем, что numberOfItems будет равен 3")

// Получаем сортированный по убыванию количества каждого элемента массив
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(
  sorted.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

Нажмите Command-Shift-Enter, чтобы запустить проект и посмотреть на это в действии.

Все эти методы полезны при работы с последовательностями.

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

Улучшаем Sequence

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

Однако, Swift снова приходит к нам на помощь. Он предоставляет нам AnyIterator для того, чтобы скрыть базовый итератор.

Замените реализацию расширения Sequence следующей:

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

В этом расширении для Sequence вы:

  1. Определили Iterator как соответствующий AnyIterator вместо DictionaryIterator. Затем, как и раньше, вы создаете makeIterator() для возврата Iterator.
  2. Создали iterator, вызвав функцию makeIterator() для contents. Эта переменная понадобится вам для следующего шага.
  3. Обернули iterator в новый объект AnyIterator, чтобы переслать его метод next(). Метод next() - это то, что вызывается на итераторе для получения следующего объекта в последовательности.

Нажмите Command-Shift-Enter для запуска. Вы заметите пару ошибок:

Прежде, мы использовали DictionaryIterator с именами кортежей key и value. Мы также скрыли DictionaryIterator от внешнего мира и переименовали имена кортежей на element и count.

Для того, чтобы исправить ошибки, поменяйте key и value на element и count соответственно. Запустите, и ваши prediction блоки пройдут так же, как и раньше.

Теперь никто не будет знать, что всю тяжелую работу за вас делает Dictionary!

Наконец, пришло время Collection!

Соответствие протоколу Collection

Давайте вспомним, что Collection - это последовательность, доступ к которой можно получить по индексу и пройти по ней несколько раз.

Чтобы соответствовать Collection, вам необходимо следующее:

  • startIndex и endIndex: они определяют границы коллекции и предоставляют начальные точки для трансверсального (поперечного) отображения.
  • subscript(position:), позволяющий получить доступ к любому элементу коллекции с помощью индекса. Этот доступ должен выполняться в O(1) временной сложности.
  • index(after:): Возвращает индекс сразу после пройденного индекса.

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

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

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position), "out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

Это довольно просто. Смотрите:

  1. Объявите тип Index, определенный в Collection как DictionaryIndex. Вы передадите эти индексы contents.
  2. Возвратите начальный и конечный индексы из contents.
  3. Используйте условие precondition для принудительного применения допустимых индексов. Вы возвращаете значение из contents этого индекса в виде нового кортежа.
  4. Возвращает значение index(after:), вызываемого contents.

Просто добавив эти свойства и методы, вы создали полностью рабочую коллекцию!

Протестируем вашу коллекцию

Добавьте следующий код в конец Playground`а, чтобы протестировать некоторые новые функции:

// Получим первый элемент Bag ("сумки", тут игра слов)
let firstItem = shoppingCart.first
precondition(
  (firstItem!.element == "Orange" && firstItem!.count == 1) ||
  (firstItem?.element == "Banana" && firstItem?.count == 2),
  "Ожидаем, что первый элемент в корзине будет (\"Orange\", 1) or (\"Banana\", 2)")

// Проверяем пустая ли у нас корзина
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,
  "Ожидаем, что корзина не будет пустой")

// Получаем число уникальных элементов корзины
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,
  "Ожидаем, что корзина будет иметь 2 уникальных элемента")

// Находим первый элемент "Banana"
let bananaIndex = shoppingCart.indices.first { 
  shoppingCart[$0].element == "Banana"
}!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2,
  "Ожидаем, что banana будет иметь значение (\"Banana\", 2)")

Снова запустите. Потрясающе!

Уже хорошо, но можно еще лучше!

Улучшаем Collection

Пользователям Bag нужно использовать объекты DictionaryIndex для доступа к элементам в коллекции.

Вы можете легко исправить это. Добавьте следующее после расширения Collection:

// 1
struct BagIndex {
  // 2
  fileprivate let index: DictionaryIndex

  // 3
  fileprivate init(
    _ dictionaryIndex: DictionaryIndex) {
    self.index = dictionaryIndex
  }
}

В коде описанном выше вы:

  1. Определили новый универсальный тип BagIndex. Как и с Bag для этого требуется универсальный тип, который является Hashable для использования с Dictionary.
  2. Создали базовые данные для типа индекса DictionaryIndex. BagIndex - это просто оболочка, которая скрывает истинный индекс от внешнего мира.
  3. Создали инициализатор, который принимает DictionaryIndex для хранения.

Теперь Вам нужно подумать о том, что требует Collection, чтобы Index был сопоставим, дабы позволить сравнивать два индекса для выполнения операций. Следовательно, BagIndex должен утвердить Comparable.

Добавьте следующее расширение сразу после BagIndex:

extension BagIndex: Comparable {
  static func ==(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func  Bool {
    return lhs.index < rhs.index
  }
}

Логика здесь проста - вы используете эквивалентные методы DictionaryIndex, чтобы вернуть правильное значение.

Обновление BagIndex

Теперь вы готовы к тому, чтобы обновить Bag для того, чтобы использовать BagIndex. Замените расширение коллекции следующим:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position), "out of bounds") // 3 let dictionaryElement = contents[position.index] return (element: dictionaryElement.key, count: dictionaryElement.value) } func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

Каждый комментарий отмечает некоторые изменения. Вот, что они собой представляют:

  1. Заменяется Index с DictionaryIndex на BagIndex.
  2. Создается новый BagIndex из contents как для startIndex, так и для endIndex.
  3. Используется BagIndex для доступа и возврата элемента из contents.
  4. Получается значение DictionaryIndex от contents, используя свойство BagIndex и создает новый BagIndex, используя это значение.

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

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

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

Slice - это представление подпоследовательности элементов в коллекции. Он позволяет выполнять действия с определенной подпоследовательностью элементов без создания копии. Slice хранит ссылку на базовую коллекцию, из которой он создается. Slice делит индексы со своей базовой коллекцией, сохраняя ссылки на начальные и конечные индексы, чтобы отметить диапазон подпоследовательностей. Slice имеет пространственную сложность O(1), поскольку он непосредственно ссылается на свою базовую коллекцию.

Чтобы увидеть, как это работает, добавьте следующий код в конец Playground`а:

// 1
let fruitBasket = Bag(dictionaryLiteral:
  ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst()

// 3
if let fruitMinIndex = fruitSlice.indices.min(by:
  { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let basketElement = fruitBasket[fruitMinIndex]
  let sliceElement = fruitSlice[fruitMinIndex]
  precondition(basketElement == sliceElement,
    "Ожидаем, что basketElement и sliceElement будут одним и тем же элементом")
}

Запустите снова. В приведенном выше коде вы:

  1. Создали корзину с фруктами, состоящую из четырех различных фруктов.
  2. Удалили первое представление фруктов. На самом деле это просто создает новое представление slice в корзине фруктов, исключая первый элемент, который вы удалили, вместо того, чтобы создавать совершенно новый объект Bag. В строке результатов вы заметите, что здесь используется тип Slice<Bag>.
  3. Нашли индекс наиболее реже встречающихся плодов из тех, что остались.
  4. Доказали, что можете использовать индекс как из базовой коллекции, так и из slice для извлечения одного и того же элемента, даже если вы вычисляли индекс из slice.

Заметка

Slice может показаться немного менее полезным для хэш-основанных коллекций, таких как Dictionary и Bag, потому что его порядок не определен каким-либо значимым образом. Array, с другой стороны, является отличным примером типа коллекции, где slice играет огромную роль в выполнении операций подпоследовательности.

Поздравляю! Теперь вы профессионал в “коллекционировании”!

Оригинал статьи

Содержание