Pour trier un tableau Swift local selon plusieurs champs, comparez chaque clé par ordre de priorité et passez à la suivante uniquement en cas d’égalité. Résolvez séparément les valeurs de repli facultatives, puis définissez où placer les valeurs manquantes. Cet exemple trie des étiquettes de formes fictives pour une liste sur un ordinateur de bureau Mac.

Définir les règles de tri avant d’écrire la fermeture

Supposons qu’une liste sur ordinateur de bureau contienne des étiquettes de formes avec un numéro de groupe, une position préférée facultative, une position de secours facultative et un titre. La tâche consiste à produire un ordre d’affichage prévisible à partir de ces valeurs en mémoire.

Deux décisions nécessitent des règles distinctes. Une valeur de repli détermine quelle valeur représente un élément : utilisez sa position préférée si elle est présente, sinon sa position de secours. Un critère de départage compare un autre champ uniquement lorsque deux éléments ont la même position effective. L’opérateur de coalescence des valeurs nil de Swift, ??, permet de prendre la première décision en sélectionnant la valeur déballée ou la valeur de repli. Opérateurs de base — Opérateur de coalescence des valeurs nil

Utilisez ce contrat explicite pour l’exemple :

Priorité Clé Règle
1 Groupe Les nombres les plus petits en premier
2 Position effective preferredPosition ?? backupPosition
3 Placement des positions manquantes Les positions manquantes suivent les positions présentes dans chaque groupe
4 Titre Comparaison ascendante des chaînes Swift en cas d’égalité des positions
5 Identifiant Les identifiants les plus petits en premier lorsque toutes les clés précédentes sont à égalité

Le placement des positions manquantes fait partie de la comparaison des positions ; ce n’est pas un champ indépendant qui l’emporte sur le groupe. Un élément du groupe zéro précède toujours un élément du groupe un, même si le premier n’a pas de position.

La comparaison des titres utilise volontairement l’ordre habituel des chaînes Swift pour ce calcul fictif. Conservez ce choix si vous remplacez ensuite la fermeture par des comparateurs Foundation.

Écrire un tri complet à plusieurs champs avec une gestion explicite des valeurs nil

Le programme complet suivant utilise la bibliothèque standard Swift et ne nécessite aucun import. Exécutez-le dans un environnement compatible avec Swift sur votre Mac. Toutes les entrées sont définies dans le programme ; le calcul n’utilise ni entrée externe ni stockage persistant. L’opération de tri renvoie un nouveau tableau avec sorted(by:). Documentation d’Apple sur sorted(by:)

struct ShapeItem {
    let id: Int
    let group: Int
    let preferredPosition: Int?
    let backupPosition: Int?
    let title: String

    var effectivePosition: Int? {
        preferredPosition ?? backupPosition
    }
}

func comesBefore(_ lhs: ShapeItem, _ rhs: ShapeItem) -> Bool {
    if lhs.group != rhs.group {
        return lhs.group < rhs.group
    }

    switch (lhs.effectivePosition, rhs.effectivePosition) {
    case let (.some(left), .some(right)):
        if left != right {
            return left < right
        }
    case (.some(_), .none):
        return true
    case (.none, .some(_)):
        return false
    case (.none, .none):
        break
    }

    if lhs.title != rhs.title {
        return lhs.title < rhs.title
    }

    return lhs.id < rhs.id
}

let items = [
    ShapeItem(id: 10, group: 1, preferredPosition: nil,
              backupPosition: 2, title: "Circle"),
    ShapeItem(id: 20, group: 1, preferredPosition: 1,
              backupPosition: 9, title: "Square"),
    ShapeItem(id: 30, group: 1, preferredPosition: nil,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 40, group: 1, preferredPosition: 2,
              backupPosition: 0, title: "Arc"),
    ShapeItem(id: 50, group: 0, preferredPosition: nil,
              backupPosition: nil, title: "Dot"),
    ShapeItem(id: 60, group: 1, preferredPosition: 2,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 70, group: 1, preferredPosition: 0,
              backupPosition: 8, title: "Zero")
]

let ordered = items.sorted(by: comesBefore)
let expectedIDs = [50, 70, 20, 40, 60, 10, 30]

precondition(ordered.map(\.id) == expectedIDs)
precondition(ordered.count == items.count)
precondition(items.reversed().sorted(by: comesBefore).map(\.id)
             == expectedIDs)

print(ordered.map(\.id))

Résultat attendu :

[50, 70, 20, 40, 60, 10, 30]

Le programme et l’ordre attendu ont été examinés, mais n’ont pas été exécutés ici, car l’environnement de recherche ne dispose pas de compilateur Swift. Les préconditions incluses vous permettent de vérifier le résultat lors de l’exécution en local.

Pourquoi chaque exemple se trouve à cette position

L’élément 50 arrive en premier, car le groupe zéro est prioritaire sur la position. L’élément 70 montre ensuite que zéro est une position préférée valide : sa valeur de secours, huit, n’est pas utilisée. L’élément 20 utilise sa position préférée, égale à un.

Les éléments 40, 60 et 10 ont tous une position effective égale à deux. Les deux titres Arc précèdent Circle, et les identifiants départagent les éléments 40 et 60. L’élément 30 arrive en dernier, car ses deux champs de position sont manquants dans le groupe un.

Cela répond aussi à une question fréquente sur les valeurs de repli : l’élément 40 n’utilise pas sa position de secours égale à zéro lorsque sa position préférée est à égalité avec celle d’un autre élément. Selon ce contrat, la valeur de secours ne remplace qu’une valeur préférée absente. Si la valeur de secours devait aussi départager les éléments, il faudrait ajouter une règle de comparaison.

Garantir la validité du comparateur en cas d’égalité

Swift exige que le prédicat de tri forme un ordre strict faible. Concrètement, un élément ne doit jamais se précéder lui-même, les décisions d’ordre doivent rester cohérentes d’une comparaison à l’autre, et les éléments équivalents doivent former des groupes cohérents. Apple documente ces exigences pour sorted(by:). Exigences relatives au prédicat

L’exemple vérifie si une clé antérieure diffère avant de renvoyer le résultat de la comparaison. Il ne compare les titres que lorsque le groupe et la position effective sont à égalité, puis compare les identifiants uniquement si les titres sont également à égalité. Lorsque les identifiants et toutes les clés précédentes sont égaux, la comparaison stricte finale renvoie false.

Évitez de joindre des comparaisons de champs indépendants avec ||. Un élément pourrait avoir un groupe antérieur mais une position ultérieure, tandis qu’un autre présenterait la combinaison inverse. Un prédicat fondé sur OR pourrait alors indiquer que chaque élément précède l’autre. Le traitement séquentiel des égalités évite cette contradiction.

Évitez aussi d’utiliser <= comme comparaison finale : des valeurs égales renverraient true, ce qui enfreint la règle selon laquelle un élément ne peut pas se précéder lui-même. Faites dépendre chaque comparaison uniquement des valeurs fournies et des règles de tri fixes.

Choisir entre des égalités stables et un identifiant final

Le tri documenté de Swift est stable : les éléments que le prédicat considère comme équivalents conservent leur ordre relatif d’origine. Si c’est le comportement souhaité, omettez la comparaison finale des identifiants et renvoyez false lorsque les titres sont à égalité. Garantie de stabilité du tri

Cet exemple utilise plutôt des identifiants fictifs uniques pour départager les égalités restantes. Par conséquent, inverser l’ordre des entrées de l’exemple produit toujours la même séquence d’identifiants. Cette propriété découle de ce contrat et des valeurs de l’exemple. Si les identifiants sont répétés en même temps que toutes les autres clés, ces éléments restent équivalents et conservent leur ordre d’origine.

Exprimer les mêmes règles avec des comparateurs de chemins de clés Foundation

Foundation fournit KeyPathComparator pour comparer des propriétés et sorted(using:) pour appliquer une séquence de comparateurs. Le premier comparateur définit l’ordre principal ; les suivants départagent les égalités. Ces API sont disponibles à partir de macOS 12. KeyPathComparator, sorted(using:)

Pour obtenir une version Foundation équivalente, exposez deux clés de position : un rang indiquant si la valeur est manquante et une position numérique. Le rang place les positions manquantes à la fin. Une fois les rangs à égalité, une valeur numérique de remplacement pour les positions manquantes ne peut pas entrer en collision avec un zéro présent, puisque ces éléments ont déjà des rangs différents.

Ajoutez l’extension et le calcul suivants après le programme complet ci-dessus. Il s’agit d’un exemple dépendant qui utilise les définitions existantes de ShapeItem, items et expectedIDs. Il nécessite macOS 14 ou ultérieur, car il construit explicitement ComparableComparator avec init(order:). Disponibilité de l’initialiseur

import Foundation

extension ShapeItem {
    var missingPositionRank: Int {
        effectivePosition == nil ? 1 : 0
    }

    var numericPositionKey: Int {
        effectivePosition ?? 0
    }
}

if #available(macOS 14.0, *) {
    let comparators: [KeyPathComparator<ShapeItem>] = [
        KeyPathComparator(\ShapeItem.group, order: .forward),
        KeyPathComparator(\ShapeItem.missingPositionRank,
                          order: .forward),
        KeyPathComparator(\ShapeItem.numericPositionKey,
                          order: .forward),
        KeyPathComparator(
            \ShapeItem.title,
            comparator: ComparableComparator<String>(order: .forward)
        ),
        KeyPathComparator(\ShapeItem.id, order: .forward)
    ]

    let foundationOrdered = items.sorted(using: comparators)
    precondition(foundationOrdered.map(\.id) == expectedIDs)
    print(foundationOrdered.map(\.id))
} else {
    print(items.sorted(by: comesBefore).map(\.id))
}

Le comparateur de titres explicite conserve le choix antérieur d’utiliser l’ordre des chaînes Swift. Foundation indique que ComparableComparator utilise l’implémentation Comparable du type comparé, et que KeyPathComparator accepte un comparateur de propriété fourni. ComparableComparator, Initialiseur de comparateur de chemin de clé

Pour un ordre fixe, la fermeture rend chaque branche visible. Un tableau de comparateurs est utile si vous voulez représenter sous forme de valeurs l’ordre des clés prises en charge. Dans les deux approches, gardez la sélection de la valeur de repli dans la propriété calculée du modèle afin que les deux implémentations utilisent la même définition.

Adapter les règles sans en changer le sens

Pour trier les positions par ordre décroissant tout en laissant les positions manquantes à la fin, modifiez uniquement la comparaison entre deux positions présentes en utilisant left > right. Dans la version Foundation, inversez uniquement numericPositionKey ; laissez missingPositionRank dans le sens croissant. Inverser le résultat entier inverserait également les groupes, les titres, les identifiants et le placement des valeurs manquantes.

Si une chaîne facultative devient une clé de repli, déterminez séparément si une chaîne vide compte comme une valeur manquante. La coalescence des valeurs nil réagit à nil ; elle ne remplace pas une chaîne vide présente. Définissez toute règle de normalisation supplémentaire avant le tri, et utilisez la même clé effective partout. Comportement de la coalescence des valeurs nil

Gardez ce petit calcul synchrone. Le guide de concurrence de Swift distingue le tri synchrone ordinaire des points de suspension, et le prédicat documenté sorted(by:) est synchrone. Si une application a besoin de valeurs issues d’un traitement asynchrone, récupérez-les avant d’appeler ce trieur. Ajouter await dans la fermeture de comparaison n’est pas une extension prise en charge de sa signature. Concurrence, Signature de sorted(by:)

Avant d’adapter l’exemple à une autre liste de bureau, conservez des vérifications portant sur une clé préférée manquante, deux clés de position manquantes, un zéro valide, des positions effectives égales et des champs d’affichage entièrement à égalité. Décidez si la dernière égalité doit conserver l’ordre d’origine ou être départagée par un identifiant. Ces choix donnent à la liste un contrat de tri précis, facile à comprendre dans les deux implémentations.

Sources