Les protocoles Swift avec des types associés (PATs) ou des exigences Self ne peuvent pas fonctionner comme des types existentiels de première classe (par exemple, [MyProtocol]) car le compilateur n'a pas les métadonnées de type concret requises pour construire des tables de témoins pour les types associés à la compilation. Cette limitation empêche les collections hétérogènes de stocker des instances directement, car la structure mémoire pour les types associés varie selon les types qui s'y conforment. Les développeurs résolvent cette contrainte à l'aide de motifs d'effacement de type, en mettant en œuvre des wrappers de boxing qui utilisent des tables de témoins de protocole ou un dispatch basé sur des closures pour homogénéiser l'accès à l'interface tout en encapsulant la complexité sous-jacente des types associés.
Lors de l'architecture d'un moteur multimédia multiplateforme, notre équipe avait besoin d'un PlaylistController capable de gérer des codecs audio divers, notamment MP3, AAC et FLAC, chacun implémentant un protocole Playable avec un type Buffer associé représentant des échantillons audio décodés. Le Buffer associé différait considérablement entre les formats : des données PCM non compressées pour FLAC contre des paquets compressés pour MP3, créant des structures mémoire incompatibles qui empêchaient un stockage polymorphe standard.
Une approche consiste à utiliser une spécialisation générique via Playlist<T: Playable>, contraignant toute la collection à un seul type concret. Cela élimine les frais généraux d'appels à l'exécution et permet des optimisations agressives du compilateur comme l'inlining. Cependant, cette approche sacrifie entièrement le polymorphisme, empêchant les utilisateurs de mélanger des pistes MP3 et FLAC au sein de la même structure de playlist.
Une autre option consiste à tirer parti des conteneurs existentiels natifs de Swift via la syntaxe [any Playable] disponible dans le Swift moderne. Bien que cela supporte le stockage hétérogène, l'accès au type Buffer associé nécessite d'ouvrir manuellement les existences à chaque point d'appel, créant un code verbeux et forçant l'allocation sur le tas pour de grands types valeur. De plus, la perte d'informations de type concret empêche le compilateur de dévirtualiser les appels de méthodes, introduisant des surcharges mesurables dans des boucles de traitement audio serrées.
La solution optimale met en œuvre une boîte d'effacement de type manuelle nommée AnyPlayable utilisant des tables de témoins basées sur des closures pour déléguer les méthodes play() et stop(). Ce wrapper stocke l'instance concrète dans un conteneur basé sur des classes ou un tampon existentiel, masquant la complexité du type associé tout en exposant une interface uniforme. Bien que cela introduise des frais généraux d'indirection comparables à ceux du dispatch virtuel, cela abstrait avec succès les différences d'implémentation des buffers et prend en charge de vraies collections hétérogènes sans complexité de cast à l'exécution.
Nous avons choisi l'approche du wrapper d'effacement de type car les applications multimédias nécessitent fondamentalement de mélanger divers codecs au sein de playlists unifiées, et les frais généraux du dispatch virtuel restent négligeables par rapport à la latence I/O dans le streaming audio. L'implémentation a permis une intégration transparente des formats DRM propriétaires avec des codecs standard sans modifier l'architecture du Controller. En fin de compte, cela a maintenu la sécurité des types à la compilation lors de l'initialisation des pistes tout en offrant la flexibilité d'exécution essentielle pour les bibliothèques de contenu créées par les utilisateurs.
Question 1 : Pourquoi ne pouvons-nous pas simplement utiliser as! any Playable pour caster des types concrets en existences lorsque des types associés sont impliqués ?
Swift interdit l'utilisation de protocoles avec des types associés comme des existences nues car le conteneur existentiel nécessite un stockage fixe de taille en ligne (typiquement trois mots), tandis que les types associés peuvent exiger des empreintes mémoire de taille arbitraire. Lorsque le type Buffer associé représente une image décodée de 512 octets pour FLAC mais un index de paquet de 4 octets pour MP3, l'existentiel ne peut pas accueillir les deux en ligne sans connaître le type concret à la compilation. Par conséquent, le compilateur impose un effacement de type ou des contraintes génériques pour garantir la sécurité mémoire, empêchant les plantages à l'exécution dus à la corruption de la pile ou aux débordements de tampon.
Question 2 : En quoi les types de résultats opaques de Swift 5.1 (some Collection) diffèrent-ils des boîtes d'effacement de type en matière de performance et d'évolution de l'API ?
Les types de résultats opaques utilisent des génériques inversés et une spécialisation à la compilation, permettant au compilateur de conserver des informations de type concret complètes tout en cachant les détails d'implémentation aux appelants. Cela évite les pénalités de dispatch virtuel et les coûts d'allocation sur le tas inhérents aux boîtes d'effacement de type manuelles. Cependant, les types opaques exigent que le type sous-jacent reste fixe au point de retour (sauf SE-0368 pour les résultats opaques multiples), tandis que les boîtes d'effacement de type permettent une variation dynamique des types concrets au sein du même conteneur à l'exécution, échangeant ainsi la performance pour la flexibilité polymorphe.
Question 3 : Quels dangers de gestion de mémoire émergent lorsque les boîtes d'effacement de type capturent des protocoles autoréférentiels (par exemple, des protocoles avec des méthodes retournant Self) dans des environnements multithread ?
Les boîtes d'effacement de type emploient souvent des wrappers basés sur des classes ou des captures de closures pour stocker des instances concrètes. Lorsque le protocole exige de retourner Self ou utilise des types associés référents à Self, la boîte doit préserver l'identité de type à travers des sémantiques de référence, créant des cycles de conservation potentiels si le type concret détient une référence inverse vers la boîte. Dans des contextes concurrents, plusieurs threads modifiant l'état de la boîte peuvent déclencher des conditions de course sur le compteur de références ou les tampons internes. Les développeurs doivent s'assurer que le wrapper se conforme correctement à Sendable, généralement en mettant en œuvre une isolation Actor ou des sémantiques de valeur immuable au sein de la boîte, afin de prévenir les courses de données tout en maintenant l'abstraction de l'interface effacée.