Видеокурсы по изучению языка программирования Swift. Подробнее

Оговорка where

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

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

Так же бывает полезно определять требования для связанных типов. Чтобы сделать это, вы определяете оговорку where, как часть списка параметров типа. Оговорка where позволяет вам требовать, чтобы связанный тип, соответствовал определенному протоколу, и/или чтобы конкретные параметры типа и связанные типы были одними и теми же. Вы пишите оговорку where, поместив ключевое слово where сразу после списка параметров типа, за которым следует одно или более ограничений для связанных типов, и/или один или более отношений равенства между типами и связанными типами.

В примере ниже определяем универсальную функцию allItemMatch, которая проверяет, чтобы увидеть содержат ли два экземпляра Container одни и те же элементы в одной и той же последовательности. Функция возвращает значение типа Bool, то есть, если у нас все элементы и их последовательность совпадает, то функция возвращает true, если нет - false.

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

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
        
        // Проверяем одинаковое ли количество элементов находится в контейнерах.
        if someContainer.count != anotherContainer.count {
            return false
        }
        
        // Проверяем все ли значения попарно равны.
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }
        
        // Все элементы совпадают, так что возвращаем true.
        return true
}

Эта функция принимает два аргумента someContainer и anotherContainer. Аргумент someContainer имеет тип C1, аргумент anotherContainer имеет тип C2. И C1 и C2 являются заполнителями имен типов для двух контейнеров, которые будут определены, когда будет вызвана функция.

Список типов параметров функции размещает следующие требования на два параметров типа:

  • C1 должен соответствовать протоколу Container (C1: Container)
  • C2 должен соответствовать протоколу Container (C2: Container)
  • Item для C1 должен быть тем же, что и Item для C2 (C1.Item == C2.Item)
  • Item для C1 должен соответствовать протоколу Equatable (C1.Item: Equatable)

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

Эти требования означают:

  • someContainer является контейнером типа C1.
  • anotherContainer является контейнером типа C2.
  • someContainer и anotherContainer содержат значения одного типа
  • Элементы в someContainer могут быть проверены при помощи оператора неравенства (!=), чтобы увидеть, что они отличаются друг от друга.

Третье и четвертое требование комбинируются так, чтобы элементы в anotherContainer так же могли бы быть проверены оператором !=, потому что они в точности одного и того же типа, что и в someContainer.

Эти требования позволяют функции allItemsMatch(_:_:) сравнивать два контейнера, даже если они являются контейнерами разного типа.

Функция allItemsMatch(_:_:) начинается с проверки количества элементов в этих контейнерах. Если они содержат разное количество элементов, то эти контейнеры уже не могут быть одинаковыми, функция возвращает false.

После проведения этой проверки, функция перебирает все элементы в someContainer при помощи for-in цикла и полуоткрытого оператора диапазона (..<). Для каждого элемента someContainer функция проверяет равенство элемента соответствующему элементу в контейнере anotherContainer. Если два элемента не равны друг другу, то эти два контейнера не считаются одинаковыми, функция возвращает false.

Если цикл закончился без каких-каких либо несоответствий элементов, то два контейнера считаются одинаковыми, и функция возвращает true.

Вот как выглядит функция allItemsMatch(_:_:) в действии:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
 
var arrayOfStrings = ["uno", "dos", "tres"]
 
if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// Выведет "All items match."

Пример выше создает экземпляр Stack для хранения значений типа String и добавляет три значения на стек. Так пример создает экземпляр Array, который инициализируется литералом массива, содержащего три одинаковые строки в стеке. Даже тогда стек и массив имеют разные типы, но оба они соответствую протоколу Container, и оба они содержат одинаковый тип значений. Тем не менее вы можете вызвать функцию allItemsMatch(_:_:) с этими двумя контейнерами в качестве своих аргументов. В примере выше функция allItemsMatch(_:_:) корректно извещает нас, что все элементы этих двух контейнеров одинаковые.

Расширения с оговоркой where

Вы так же можете использовать оговорку  where в расширениях. В примере ниже у нас есть раширение для структуры Stack из прошлого примера, где мы добавляем метод  isTop(_:).

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

Новый метод isTop(_:) сначала проверяет, что наш стек не пустой, а затем сравнивает верхний элемент стека с данным нам item. Если вы попытаетесь сделать то же самое без универсальной where, то у вас будут проблемы. Реализация  isTop(_:) использует оператор ==, но определеине Stack не требует того, чтобы элементы могли сравниваться, так что написание оператора  == вызовет ошибку компиляции. Использование универсальной  where позволяет вам добавить новое требование к расширению, так что расширение добавляет метод isTop(_:) только в том случае, если элементы в Stack реализуют Equatable.

Вот как будет выглядеть наш метод isTop(_:) в действии:

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Выведет "Top element is tres."

Если вы попытаетесь вызвать метод isTop(_:) в стеке, то те, элементы, которые не реализуют протокол Equatable вызовут ошибку компиляции:

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

Вы можете использовать универсальную where с расширениями протоколов. Пример ниже расширяет протокол Container из прошлого примера, добавляя ему новый метод startsWith(_:).

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

 Метод startsWith(_:) сначала проверяет на наличие элементов в контейнере, а затем проверяет не равен ли данный элемент первому элементу контейнера. Новый метод startsWith(_:) может быть использован с любым типом, который реализует протокол Container, включая стеки, массивы, использованные ранее, элементы контейнера соответствуют протоколу Equatable.

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Выведет "Starts with something else."

Универсальная оговорка where в примере выше требует, чтобы Item соответствовал протоколу Container, но вы так же можете использовать where для указания конкретного типа для Item. Например:

extension Container where Item == Double {
    func average() -> Double {
        var sum = 0.0
        for index in 0..<count {
            sum += self[index]
        }
        return sum / Double(count)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Выведет "648.9"

Этот пример добавляет контейнерам метод average(), у которых Item имеет тип Double. Он итерирует по элементам контейнера, суммируя их, и делит сумму на их общее количество. Он явно преобразует count из Int в Double что позволяет нам проводить деление чисел с плавающей точкой.

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

Связанные типы с универсальной оговоркой where

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

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
    
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

Универсальная оговорка where в Iterator требует, чтобы итератор должен поддерживать тот же самый тип элементов, что и тип элементов контейнера, не смотря на тип самого итератора. Метод makeIterator() предоставляет доступ к итератору контейнера.

Для протокола, который наследуется от другого протокола, вы можете добавить ограничение в унаследованный связанный тип, путем включения универсальной оговорки where в объявление протокола. Например следующий код объявляет протокол ComparableContainer, который требует, чтобы Item соответствовал протоколу Comparable:

protocol ComparableContainer: Container where Item: Comparable { }

Универсальные сабскрипты

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

extension Container {
    subscript<Indices: Sequence>(indices: Indices) -> [Item]
        where Indices.Iterator.Element == Int {
            var result = [Item]()
            for index in indices {
                result.append(self[index])
            }
            return result
    }
}

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

  • Универсальный параметр Indeces в угловых скобках, должен быть типа, который реализует протокол Sequence из стандартной библиотеки.
  • Сабскрипт принимает один параметр, indeces, который является экземпляром типа Indeces.
  • Универсальаня оговорка where требует, чтобы итератор последовательности мог перемещаться по элементам типа Int. Это гарантирует, что индексы последовательности того же самого типа, что и индексы, которые использовались в контейнере.

Беря все это во внимание, ограничения, которые описаны выше означают, что значения, передаваемые для параметра indeces, должны быть последовательностью элементов типа Int.

 

Swift: 
4.0