Comment étendre des tableaux tapés dans Swift?

203

Comment puis-je étendre Swift Array<T>ou T[]taper avec des utilitaires fonctionnels personnalisés?

La navigation dans les documents de l'API de Swift montre que les méthodes Array sont une extension de T[], par exemple:

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

    var count: Int { get }

    var capacity: Int { get }

    var isEmpty: Bool { get }

    func copy() -> T[]
}

Lorsque vous copiez et collez la même source et essayez des variantes telles que:

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

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

Il ne parvient pas à générer avec l'erreur:

Le type nominal T[]ne peut pas être étendu

L'utilisation de la définition de type complète échoue avec Use of undefined type 'T', c'est-à-dire:

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

Et cela échoue également avec Array<T : Any>et Array<String>.

Curieusement Swift me permet d'étendre un tableau non typé avec:

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

Avec quoi il me permet d'appeler:

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

Mais je ne peux pas créer une extension de type générique appropriée car le type semble être perdu lorsqu'il passe par la méthode, par exemple en essayant de remplacer le filtre intégré de Swift par :

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
    }
}

Mais le compilateur le traite comme non typé où il permet toujours d'appeler l'extension avec:

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

Et lorsqu'il est exécuté avec un débogueur indique que le type est, Swift.Stringmais c'est une erreur de construction pour essayer d'y accéder comme une chaîne sans le transtyper en Stringpremier, c'est-à-dire:

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

Quelqu'un sait-il quelle est la bonne façon de créer une méthode d'extension typée qui agit comme les extensions intégrées?

mythz
la source
A voté parce que je ne trouve pas non plus de réponse moi-même. Voir le même extension T[]bit lorsque vous cliquez sur le type de tableau dans XCode, mais ne voyez aucun moyen de l'implémenter sans obtenir d'erreur.
nom d'utilisateur tbd
@usernametbd FYI vient de le trouver, il semble que la solution était de supprimer <T>de la signature de la méthode.
mythz

Réponses:

296

Pour étendre des tableaux typés avec des classes , ce qui suit fonctionne pour moi (Swift 2.2 ). Par exemple, trier un tableau typé:

class HighScoreEntry {
    let score:Int
}

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

Essayer de le faire avec une structure ou des typealias donnera une erreur:

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

Mise à jour :

Pour étendre des tableaux typés avec des non-classes, utilisez l'approche suivante:

typealias HighScoreEntry = (Int)

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

Dans Swift 3, certains types ont été renommés:

extension Sequence where Iterator.Element == HighScoreEntry 
{
    // ...
}
Andrew Schreiber
la source
1
le compilateur signale que «SequenceType» a été renommé «Sequence»
sandover
Pourquoi vous n'avez pas utilisé Iterator.Element dans le type de retour [Iterator.Element]?
gaussblurinc
1
salut, pouvez-vous expliquer la fonction de conformité conditionnelle dans 4.1? Quoi de neuf dans 4.1? On pourrait faire ça en 2.2? Qu'est-ce que je manque
osrl
Depuis Swift 3.1, vous pouvez étendre des tableaux avec des non-classes avec la syntaxe suivante: extension Array where Element == Int
Giles
63

Après avoir essayé différentes choses, la solution semble supprimer le <T>de la signature comme:

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
    }
}

Qui fonctionne maintenant comme prévu sans erreurs de construction:

["A","B","C"].find { $0.compare("A") > 0 }
mythz
la source
1
BTW Ce que vous avez défini ici est fonctionnellement équivalent à la filterfonction existante :let x = ["A","B","C","X”].filter { $0.compare("A") > 0 }
Palimondo
5
@Palimondo Non, ce n'est pas le cas, le filtre intégré exécute les rappels deux fois .
mythz
4
Je vois. Le double filtrage me semble plutôt bogué ... Mais il reste que le filterest fonctionnellement équivalent au vôtre find, c'est-à-dire que le résultat de la fonction est le même. Si votre fermeture de filtre a des effets secondaires, vous pourriez ne pas aimer les résultats, c'est certain.
Palimondo
2
@Palimondo Exactement, le filtre par défaut a un comportement inattendu alors que l'implémentation ci-dessus fonctionne comme prévu (et pourquoi il existe). Ce n'est pas fonctionnellement équivalent s'il exécute la fermeture deux fois, ce qui peut potentiellement muter les variables de portée (ce qui s'est avéré être un bug que j'ai rencontré, d'où la question sur son comportement). Notez également que la question mentionne spécifiquement vouloir remplacer le Swift intégré filter.
mythz
4
Nous semblons nous disputer sur la définition du mot fonctionnel . Usuellement, en paradigme de programmation fonctionnelle où les filter, mapet les reducefonctions proviennent, les fonctions sont exécutées pour leurs valeurs de retour. En revanche, la eachfonction que vous définissez ci-dessus est un exemple de fonction exécutée pour son effet secondaire, car elle ne renvoie rien. Je suppose que nous pouvons convenir que l'implémentation actuelle de Swift n'est pas idéale et que la documentation ne dit rien sur ses caractéristiques d'exécution.
Palimondo
24

Étendez tous les types:

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

Étendez certains types:

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

Étendez un type particulier :

extension Array where Element == Int {
    // ...
}
Dmitry
la source
8

J'ai eu un problème similaire - je voulais étendre le tableau général avec une méthode swap (), qui était censée prendre un argument du même type que le tableau. Mais comment spécifiez-vous le type générique? J'ai trouvé par essais et erreurs que ce qui suit fonctionnait:

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

La clé était le mot «élément». Notez que je n'ai défini ce type nulle part, il semble exister automatiquement dans le contexte de l'extension du tableau et faire référence à quel que soit le type des éléments du tableau.

Je ne suis pas sûr à 100% de ce qui se passe là-bas, mais je pense que c'est probablement parce que «Element» est un type associé du tableau (voir «Types associés» ici https://developer.apple.com/library/ios/documentation /Swift/Conceptual/Swift_Programming_Language/Generics.html#//apple_ref/doc/uid/TP40014097-CH26-ID189 )

Cependant, je ne vois aucune référence à cela dans la référence de structure Array ( https://developer.apple.com/library/prerelease/ios/documentation/Swift/Reference/Swift_Array_Structure/index.html#//apple_ref/swift / struct / s: Sa ) ... donc je suis encore un peu incertain.

Daniel Howard
la source
1
Arrayest un type générique: Array<Element>(voir swiftdoc.org/v2.1/type/Array ), Elementest un espace réservé pour le type contenu. Par exemple: var myArray = [Foo]()signifie que myArrayne contiendra que du type Foo. Foodans ce cas est "mappé" à l'espace réservé générique Element. Si vous souhaitez modifier le comportement général d'Array (via l'extension), vous utiliserez l'espace réservé générique Elementet non aucun type concret (comme Foo).
David James
5

Utilisation de Swift 2.2 : J'ai rencontré un problème similaire en essayant de supprimer les doublons d'un tableau de chaînes. J'ai pu ajouter une extension sur la classe Array qui fait exactement ce que je cherchais à faire.

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
    }
}

L'ajout de ces deux méthodes à la classe Array me permet d'appeler l'une des deux méthodes sur un tableau et de supprimer correctement les doublons. Notez que les éléments du tableau doivent être conformes au protocole Hashable. Maintenant, je peux le faire:

 var dupes = ["one", "two", "two", "three"]
 let deDuped = dupes.removeDuplicates()
 dupes.removeDuplicatesInPlace()
 // result: ["one", "two", "three"]
James
la source
Cela pourrait également être accompli avec let deDuped = Set(dupes), que vous pourriez retourner dans une méthode non destructive appelée toSettant que vous êtes d'accord avec le changement de type
alexpyoung
@alexpyoung vous gâcheriez l'ordre du tableau si vous faites Set ()
Danny Wang
5

Si vous souhaitez en savoir plus sur l'extension des tableaux et d'autres types de code de contrôle de construction dans les classes dans ce dépôt github https://github.com/ankurp/Cent

Depuis Xcode 6.1, la syntaxe pour étendre les tableaux est la suivante

extension Array {
    func at(indexes: Int...) -> [Element] {
        ... // You code goes herer
    }
}
Encore PTL
la source
1
@Rob Mise à jour de l'URL
Encore PTL
3

J'ai jeté un coup d'œil aux en-têtes de bibliothèque standard de Swift 2, et voici le prototype de la fonction de filtrage, ce qui rend très évident comment rouler le vôtre.

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

Ce n'est pas une extension d'Array, mais de CollectionType, donc la même méthode s'applique aux autres types de collection. @noescape signifie que le bloc transmis ne quittera pas la portée de la fonction de filtrage, ce qui permet certaines optimisations. Le moi avec un S majuscule est la classe que nous étendons. Self.Generator est un itérateur qui parcourt les objets de la collection et Self.Generator.Element est le type des objets, par exemple pour un tableau [Int?] Self.Generator.Element serait Int ?.

Dans l'ensemble, cette méthode de filtrage peut être appliquée à n'importe quel CollectionType, elle a besoin d'un bloc de filtre qui prend un élément de la collection et renvoie un Bool, et elle retourne un tableau du type d'origine. Donc, pour mettre cela ensemble, voici une méthode que je trouve utile: il combine la carte et le filtre, en prenant un bloc qui mappe un élément de collection à une valeur facultative, et renvoie un tableau de ces valeurs facultatives qui ne sont pas nulles.

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
la source
2
import Foundation

extension Array {
    var randomItem: Element? {
        let idx = Int(arc4random_uniform(UInt32(self.count)))
        return self.isEmpty ? nil : self[idx]
    }
}
Leszek Zarna
la source
0

( Swift 2.x )

Vous pouvez également étendre le tableau pour qu'il soit conforme à un protocole contenant des plans directeurs pour les méthodes de type générique, par exemple, un protocole contenant vos utilitaires fonctionnels personnalisés pour tous les éléments de tableau génériques conformes à une contrainte de type, par exemple le protocole MyTypes. L'avantage de cette approche est que vous pouvez écrire des fonctions en prenant des arguments de tableau génériques, avec une contrainte que ces arguments de tableau doivent être conformes à votre protocole d'utilitaires de fonctions personnalisés, par exemple le protocoleMyFunctionalUtils .

Vous pouvez obtenir ce comportement soit implicitement, en saisissant les éléments du tableau sous contrainte MyTypes, soit --- comme je vais le montrer dans la méthode que je décris ci-dessous ---, de manière assez claire et explicite, en laissant votre en-tête de fonctions de tableau générique montrer directement que les tableaux d'entrée conforme à MyFunctionalUtils.


Nous commençons par les protocoles MyTypesà utiliser comme contrainte de type; étendre les types que vous souhaitez adapter à vos génériques par ce protocole (l'exemple ci-dessous étend les types fondamentaux Intet Doubleainsi qu'un type personnalisé 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)
}

Protocole MyFunctionalUtils(contenant les plans de nos utilitaires de fonctions de tableau génériques supplémentaires) et par la suite, l'extension de Array par MyFunctionalUtils; mise en œuvre de méthodes imprimées en bleu:

/* 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
    }
}

Enfin, des tests et deux exemples montrant une fonction prenant des tableaux génériques, avec respectivement les cas suivants

  1. Montrant l' assertion implicite que les paramètres du tableau sont conformes au protocole «MyFunctionalUtils», via le type contraignant les éléments du tableau à «MyTypes» (fonction bar1).

  2. Montrant explicitement que les paramètres du tableau sont conformes au protocole 'MyFunctionalUtils' (fonction bar2).

Le test et les exemples suivants:

/* 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
la source
-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
        }

    }

}
Durul Dalkanat
la source
2
Ces downcasts ( $0 as! Double) luttent contre le système de type de Swift et vont également à l'encontre du but de la question du PO, à mon avis. En faisant cela, vous perdez tout potentiel d'optimisation du compilateur pour les calculs que vous voulez réellement faire, et vous polluez également l'espace de noms d'Array avec des fonctions sans signification (pourquoi voudriez-vous voir .calculateMedian () dans un tableau d'UIViews, ou d'autre chose que Double d'ailleurs?). Il y a un meilleur moyen.
éphémère
tryextension CollectionType where Generator.Element == Double {}
ephemer