Как я могу расширить типизированные массивы в Swift?

203

Как я могу расширить Swift's Array<T>или T[]печатать с помощью пользовательских функциональных утилит?

Просмотр API документов Swift показывает, что методы Array являются расширением T[], например:

extension T[] : ArrayType {
    //...
    init()

    var count: Int { get }

    var capacity: Int { get }

    var isEmpty: Bool { get }

    func copy() -> T[]
}

При копировании и вставке одного и того же источника и попытках любых вариантов, таких как:

extension T[] : ArrayType {
    func foo(){}
}

extension T[] {
    func foo(){}
}

Не удается построить с ошибкой:

Номинальный тип T[]не может быть продлен

Использование полного определения типа завершается неудачно с помощью Use of undefined type 'T':

extension Array<T> {
    func foo(){}
}

И это также терпит неудачу с Array<T : Any>и Array<String>.

Любопытно, что Swift позволяет мне расширить нетипизированный массив с помощью:

extension Array {
    func each(fn: (Any) -> ()) {
        for i in self {
            fn(i)
        }
    }
}

Который это позволяет мне звонить с:

[1,2,3].each(println)

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

extension Array {
    func find<T>(fn: (T) -> Bool) -> T[] {
        var to = T[]()
        for x in self {
            let t = x as T
            if fn(t) {
                to += t
            }
        }
        return to
    }
}

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

["A","B","C"].find { $0 > "A" }

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

["A","B","C"].find { ($0 as String).compare("A") > 0 }

Кто-нибудь знает, как правильно создать типизированный метод расширения, который действует как встроенные расширения?

mythz
источник
Проголосовал, потому что и сам не могу найти ответ. Видим тот же extension T[]бит при щелчке Command по типу Array в XCode, но не видим способа реализовать его без ошибки.
имя пользователя tbd
@usernametbd FYI только что нашел, похоже, решение было удалить <T>из сигнатуры метода.
Мифз

Ответы:

296

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

class HighScoreEntry {
    let score:Int
}

extension Array where Element == HighScoreEntry {
    func sort() -> [HighScoreEntry] {
      return sort { $0.score < $1.score }
    }
}

Попытка сделать это с помощью struct или typealias выдаст ошибку:

Type 'Element' constrained to a non-protocol type 'HighScoreEntry'

Обновить :

Чтобы расширить типизированные массивы не-классами, используйте следующий подход:

typealias HighScoreEntry = (Int)

extension SequenceType where Generator.Element == HighScoreEntry {
    func sort() -> [HighScoreEntry] {
      return sort { $0 < $1 }
    }
}

В Swift 3 некоторые типы были переименованы:

extension Sequence where Iterator.Element == HighScoreEntry 
{
    // ...
}
Эндрю Шрайбер
источник
1
Компилятор сообщает, что «SequenceType» был переименован в «Sequence»
sandover
Почему вы не использовали Iterator.Element в типе возвращаемого значения [Iterator.Element]?
gaussblurinc
1
привет, вы можете объяснить функцию условного соответствия в 4.1? Что нового в 4.1? Мы могли бы сделать это в 2.2? Чего мне не хватает
osrl
Начиная с Swift 3.1, вы можете расширять массивы с помощью неклассов со следующим синтаксисом: extension Array, где Element == Int
Giles
63

Через некоторое время, пробуя разные вещи, решение, кажется, удаляет <T>из подписи, например:

extension Array {
    func find(fn: (T) -> Bool) -> [T] {
        var to = [T]()
        for x in self {
            let t = x as T;
            if fn(t) {
                to += t
            }
        }
        return to
    }
}

Который теперь работает как задумано, без ошибок сборки:

["A","B","C"].find { $0.compare("A") > 0 }
mythz
источник
1
Кстати, то, что вы определили здесь, функционально эквивалентно существующей filterфункции:let x = ["A","B","C","X”].filter { $0.compare("A") > 0 }
Palimondo
4
Понимаю. Двойная фильтрация мне кажется довольно глючной ... Но она все еще считает, что filterона функционально эквивалентна вашей find, то есть результат функции тот же. Если у вашего фильтра есть побочные эффекты, вам наверняка не понравятся результаты.
Палимондо
2
@Palimondo Точно, фильтр по умолчанию имеет непредсказуемое поведение, тогда как приведенный выше поиск подразумевает, что он работает как ожидалось (и почему он существует). Это не является функционально эквивалентным, если он выполняет замыкание дважды, что потенциально может привести к изменению области видимости переменных (это была ошибка, с которой я столкнулся, отсюда и вопрос о его поведении). Также обратите внимание, что в вопросе конкретно упоминается желание заменить встроенный Swift filter.
Мифз
4
Похоже, мы спорим об определении слова функционал . Привычно, в функциональной парадигмы программирования , где filter, mapи reduceфункции исходят из функции выполняются для их возвращаемых значений. Напротив, eachфункция, которую вы определили выше, является примером функции, выполняемой для ее побочного эффекта, потому что она ничего не возвращает. Я думаю, мы можем согласиться с тем, что текущая реализация Swift не идеальна, и в документации ничего не говорится о ее характеристиках времени выполнения.
Палимондо
24

Расширить все типы:

extension Array where Element: Comparable {
    // ...
}

Расширить некоторые типы:

extension Array where Element: Comparable & Hashable {
    // ...
}

Расширить определенный тип:

extension Array where Element == Int {
    // ...
}
Дмитрий
источник
8

У меня была похожая проблема - я хотел расширить общий массив с помощью метода swap (), который должен был принимать аргумент того же типа, что и массив. Но как вы определяете универсальный тип? Я обнаружил методом проб и ошибок, что ниже работает:

extension Array {
    mutating func swap(x:[Element]) {
        self.removeAll()
        self.appendContentsOf(x)
    }
}

Ключом к этому было слово «Элемент». Обратите внимание, что я нигде не определял этот тип, он, кажется, автоматически существует в контексте расширения массива и ссылается на тип элементов массива.

Я не уверен на 100%, что там происходит, но думаю, что это, вероятно, потому, что «Элемент» - это связанный тип массива (см. «Связанные типы» здесь https://developer.apple.com/library/ios/documentation /Swift/Conceptual/Swift_Programming_Language/Generics.html#//apple_ref/doc/uid/TP40014097-CH26-ID189 )

Однако я не вижу никакой ссылки на это в описании структуры Array ( https://developer.apple.com/library/prerelease/ios/documentation/Swift/Reference/Swift_Array_Structure/index.html#//apple_ref/swift / struct / s: Sa ) ... так что я все еще немного неуверен.

Дэниел Ховард
источник
1
Arrayявляется универсальным типом: Array<Element>(см. swiftdoc.org/v2.1/type/Array ), Elementявляется заполнителем для содержащегося типа. Например: var myArray = [Foo]()означает, что myArrayбудет содержать только тип Foo. Fooв этом случае "отображается" на родовой заполнитель Element. Если вы хотите изменить общее поведение массива (через расширение), вы должны использовать общий заполнитель, Elementа не какой-либо конкретный тип (например, Foo).
Дэвид Джеймс
5

Использование Swift 2.2 : я столкнулся с подобной проблемой при попытке удалить дубликаты из массива строк. Мне удалось добавить расширение для класса Array, которое делает именно то, что я искал.

extension Array where Element: Hashable {
    /**
     * Remove duplicate elements from an array
     *
     * - returns: A new array without duplicates
     */
    func removeDuplicates() -> [Element] {
        var result: [Element] = []
        for value in self {
            if !result.contains(value) {
                result.append(value)
            }
        }
        return result
    }

    /**
     * Remove duplicate elements from an array
     */
    mutating func removeDuplicatesInPlace() {
        var result: [Element] = []
        for value in self {
            if !result.contains(value) {
                result.append(value)
            }
        }
        self = result
    }
}

Добавление этих двух методов в класс Array позволяет мне вызывать один из двух методов в массиве и успешно удалять дубликаты. Обратите внимание, что элементы в массиве должны соответствовать протоколу Hashable. Теперь я могу сделать это:

 var dupes = ["one", "two", "two", "three"]
 let deDuped = dupes.removeDuplicates()
 dupes.removeDuplicatesInPlace()
 // result: ["one", "two", "three"]
Джеймс
источник
Это также может быть достигнуто с помощью let deDuped = Set(dupes), который вы можете вернуть в неразрушающем методе, который toSet
вызывается
@alexpyoung вы бы испортили порядок массива, если бы сделали Set ()
Дэнни Ван
5

Если вы хотите узнать о расширении Arrays и других типах кода проверки классов в этом репозитории github https://github.com/ankurp/Cent

Начиная с Xcode 6.1 синтаксис для расширения массивов выглядит следующим образом

extension Array {
    func at(indexes: Int...) -> [Element] {
        ... // You code goes herer
    }
}
Encore PTL
источник
1
@Rob Обновил URL
Encore PTL
3

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

extension CollectionType {
    func filter(@noescape includeElement: (Self.Generator.Element) -> Bool) -> [Self.Generator.Element]
}

Это не расширение для Array, а для CollectionType, поэтому тот же метод применяется к другим типам коллекций. @noescape означает, что переданный блок не покинет область действия функции фильтра, что обеспечивает некоторую оптимизацию. Я с большой буквы S - это класс, который мы расширяем. Self.Generator - это итератор, который выполняет итерацию по объектам в коллекции, а Self.Generator.Element - это тип объектов, например, для массива [Int?] Self.Generator.Element будет Int ?.

В общем, этот метод фильтра может быть применен к любому CollectionType, ему нужен блок фильтра, который берет элемент коллекции и возвращает Bool, а также возвращает массив исходного типа. Итак, объединяя это, вот метод, который я считаю полезным: он объединяет карту и фильтр, беря блок, который отображает элемент коллекции в необязательное значение, и возвращает массив тех необязательных значений, которые не равны nil.

extension CollectionType {

    func mapfilter<T>(@noescape transform: (Self.Generator.Element) -> T?) -> [T] {
        var result: [T] = []
        for x in self {
            if let t = transform (x) {
                result.append (t)
            }
        }
        return result
    }
}
gnasher729
источник
2
import Foundation

extension Array {
    var randomItem: Element? {
        let idx = Int(arc4random_uniform(UInt32(self.count)))
        return self.isEmpty ? nil : self[idx]
    }
}
Лешек Зарна
источник
0

( Swift 2.x )

Вы также можете расширить массив, чтобы он соответствовал протоколу, содержащему blue-rpints для методов универсального типа, например, протокол, содержащий ваши пользовательские функциональные утилиты для всех элементов универсального массива, соответствующих некоторому ограничению типа, скажем, протоколу MyTypes. Преимущество этого подхода в том, что вы можете писать функции, принимающие универсальные аргументы массива, с ограничением, что эти аргументы массива должны соответствовать вашему протоколу утилит пользовательских функций, скажем, протоколу MyFunctionalUtils.

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


Мы начнем с протоколов MyTypesдля использования в качестве ограничения типа; расширять типы, которые вы хотите вписать в ваши дженерики с помощью этого протокола (пример ниже расширяет основные типы IntиDouble , а также пользовательский тип MyCustomType)

/* Used as type constraint for Generator.Element */
protocol MyTypes {
    var intValue: Int { get }
    init(_ value: Int)
    func *(lhs: Self, rhs: Self) -> Self
    func +=(inout lhs: Self, rhs: Self)
}

extension Int : MyTypes { var intValue: Int { return self } }
extension Double : MyTypes { var intValue: Int { return Int(self) } }
    // ...

/* Custom type conforming to MyTypes type constraint */
struct MyCustomType : MyTypes {
    var myInt : Int? = 0
    var intValue: Int {
        return myInt ?? 0
    }

    init(_ value: Int) {
        myInt = value
    }
}

func *(lhs: MyCustomType, rhs: MyCustomType) -> MyCustomType {
    return MyCustomType(lhs.intValue * rhs.intValue)
}

func +=(inout lhs: MyCustomType, rhs: MyCustomType) {
    lhs.myInt = (lhs.myInt ?? 0) + (rhs.myInt ?? 0)
}

Протокол MyFunctionalUtils(содержит чертежи наших дополнительных утилит функций универсального массива), а затем, расширение массиваMyFunctionalUtils ; реализация метода (-ов) с синей печатью:

/* Protocol holding our function utilities, to be used as extension 
   o Array: blueprints for utility methods where Generator.Element 
   is constrained to MyTypes */
protocol MyFunctionalUtils {
    func foo<T: MyTypes>(a: [T]) -> Int?
        // ...
}

/* Extend array by protocol MyFunctionalUtils and implement blue-prints 
   therein for conformance */
extension Array : MyFunctionalUtils {
    func foo<T: MyTypes>(a: [T]) -> Int? {
        /* [T] is Self? proceed, otherwise return nil */
        if let b = self.first {
            if b is T && self.count == a.count {
                var myMultSum: T = T(0)

                for (i, sElem) in self.enumerate() {
                    myMultSum += (sElem as! T) * a[i]
                }
                return myMultSum.intValue
            }
        }
        return nil
    }
}

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

  1. Показано неявное утверждение о том, что параметры массива соответствуют протоколу «MyFunctionalUtils», посредством ограничения типа элементов массива на «MyTypes» (функция bar1).

  2. Явно показывает, что параметры массива соответствуют протоколу «MyFunctionalUtils» (функцияbar2 ).

Тест и примеры следующие:

/* Tests & examples */
let arr1d : [Double] = [1.0, 2.0, 3.0]
let arr2d : [Double] = [-3.0, -2.0, 1.0]

let arr1my : [MyCustomType] = [MyCustomType(1), MyCustomType(2), MyCustomType(3)]
let arr2my : [MyCustomType] = [MyCustomType(-3), MyCustomType(-2), MyCustomType(1)]

    /* constrain array elements to MyTypes, hence _implicitly_ constraining
       array parameters to protocol MyFunctionalUtils. However, this
       conformance is not apparent just by looking at the function signature... */
func bar1<U: MyTypes> (arr1: [U], _ arr2: [U]) -> Int? {
    return arr1.foo(arr2)
}
let myInt1d = bar1(arr1d, arr2d) // -4, OK
let myInt1my = bar1(arr1my, arr2my) // -4, OK

    /* constrain the array itself to protocol MyFunctionalUtils; here, we
       see directly in the function signature that conformance to
       MyFunctionalUtils is given for valid array parameters */
func bar2<T: MyTypes, U: protocol<MyFunctionalUtils, _ArrayType> where U.Generator.Element == T> (arr1: U, _ arr2: U) -> Int? {

    // OK, type U behaves as array type with elements T (=MyTypes)
    var a = arr1
    var b = arr2
    a.append(T(2)) // add 2*7 to multsum
    b.append(T(7))

    return a.foo(Array(b))
        /* Ok! */
}
let myInt2d = bar2(arr1d, arr2d) // 10, OK
let myInt2my = bar2(arr1my, arr2my) // 10, OK
dfri
источник
-1
import Foundation

extension Array {

    func calculateMean() -> Double {
        // is this an array of Doubles?
        if self.first is Double {
            // cast from "generic" array to typed array of Doubles
            let doubleArray = self.map { $0 as! Double }

            // use Swift "reduce" function to add all values together
            let total = doubleArray.reduce(0.0, combine: {$0 + $1})

            let meanAvg = total / Double(self.count)
            return meanAvg

        } else {
            return Double.NaN
        }
    }

    func calculateMedian() -> Double {
        // is this an array of Doubles?
        if self.first is Double {
            // cast from "generic" array to typed array of Doubles
            var doubleArray = self.map { $0 as! Double }

            // sort the array
            doubleArray.sort( {$0 < $1} )

            var medianAvg : Double
            if doubleArray.count % 2 == 0 {
                // if even number of elements - then mean average the middle two elements
                var halfway = doubleArray.count / 2
                medianAvg = (doubleArray[halfway] + doubleArray[halfway - 1]) / 2

            } else {
                // odd number of elements - then just use the middle element
                medianAvg = doubleArray[doubleArray.count  / 2 ]
            }
            return medianAvg
        } else {
            return Double.NaN
        }

    }

}
Дурул Далканат
источник
2
Эти downcasts ( $0 as! Double) борются против системы типов Swift, а также, на мой взгляд, побеждают цель вопроса OP. Делая это, вы теряете любой потенциал для оптимизации компилятора для вычислений, которые вы на самом деле хотите сделать, а также загрязняете пространство имен Array бессмысленными функциями (зачем вам хотеть видеть .calculateMedian () в массиве UIViews, или что-нибудь, кроме Double в этом отношении?). Существует лучший способ.
Эфемер
попробуйextension CollectionType where Generator.Element == Double {}
эфемер