Протоколы Swift с ассоциированными типами (PATs) или требованиями Self не могут функционировать в качестве первоклассных экзистенциальных типов (например, [MyProtocol]), поскольку компилятор не имеет конкретной метаданных типов, необходимых для построения таблиц свидетелей для ассоциированных типов на этапе компиляции. Это ограничение препятствует хранению экземпляров непосредственно в гетерогенных коллекциях, так как расположение в памяти для ассоциированных типов варьируется среди типов, которые соответствуют протоколу. Разработчики решают эту проблему с помощью паттернов стирания типов, реализуя упаковочные оболочки, которые используют таблицы свидетелей протокола или основанную на замыканиях диспетчеризацию, чтобы унифицировать доступ к интерфейсу, скрывая сложность ассоциированного типа.
При проектировании кроссплатформенного медиадвижка нашей команде нужен был PlaylistController, способный управлять разнообразными аудиокодеками — включая MP3, AAC и FLAC — каждый из которых реализует протокол Playable с ассоциированным типом Buffer, представляющим декодированные аудио образцы. Ассоциированный Buffer значительно отличался между форматами: не сжатые данные PCM для FLAC против сжатых пакетов для MP3, создавая несовместимые макеты памяти, которые мешали стандартному полиморфному хранению.
Один из подходов использует обобщенную специализацию через Playlist<T: Playable>, ограничивая всю коллекцию одним конкретным типом. Это устраняет накладные расходы службы во время выполнения и позволяет агрессивные оптимизации компилятора, такие как инлайнинг. Однако этот подход полностью жертвует полиморфизмом, не позволяя пользователям смешивать треки MP3 и FLAC в одной и той же структуре плейлиста.
В качестве альтернативы разработчики могут использовать встроенные экзистенциальные контейнеры Swift через синтаксис [any Playable], доступный в современном Swift. Хотя это поддерживает гетерог Storage, доступ к ассоциированному типу Buffer требует ручного открытия экзистенциалов на каждом месте вызова, создавая многословный шаблон и вынуждая выделение памяти для больших типов значений. Кроме того, потеря информации о конкретном типе мешает компилятору деvirtualизировать вызовы метода, что вводит ощутимые накладные расходы в плотных циклах обработки аудио.
Оптимальное разрешение реализует ручную упаковку стирания типов под названием AnyPlayable, использующую основанные на замыканиях таблицы свидетелей для делегирования методов play() и stop(). Эта оболочка хранит конкретный экземпляр в контейнере на основе класса или экзистенциальном буфере, скрывая сложность ассоциированного типа, в то время как предоставляет унифицированный интерфейс. Хотя это вводит накладные расходы на индикацию, сопоставимые с виртуальной диспетчеризацией, это успешно абстрагирует различия в реализации буфера и поддерживает истинные гетерогенные коллекции без сложности приведения типов во время выполнения.
Мы выбрали подход с упаковкой стирания типов, потому что медиаприложения в корне требуют смешивания различных кодеков в объединенных плейлистах, и накладные расходы на виртуальную диспетчеризацию остаются незначительными по сравнению с задержкой I/O при потоковой передаче аудио. Реализация позволила бесшовную интеграцию проприетарных форматов DRM со стандартными кодеками без изменения архитектуры Controller. В конечном итоге это сохранило безопасность типов на этапе компиляции во время инициализации треков, обеспечивая гибкость во время выполнения, необходимую для библиотек контента, составленных пользователями.
Вопрос 1: Почему мы не можем просто использовать as! any Playable, чтобы привести конкретные типы к экзистенциалам, когда дело касается ассоциированных типов?
Swift запрещает использовать протоколы с ассоциированными типами в качестве голых экзистенциалов, поскольку экзистенциальный контейнер требует фиксированного по размеру встроенного хранилища (обычно три слова), в то время как ассоциированные типы могут требовать произвольно больших объемов памяти. Когда ассоциированный тип Buffer представляет 512-байтный декодированный кадр для FLAC, но 4-байтный индекс пакета для MP3, экзистенциал не может уместить оба типа внутри без знания конкретного типа на этапе компиляции. Соответственно, компилятор вводит стирание типов или обобщенные ограничения для обеспечения безопасности памяти, предотвращая сбои во время выполнения из-за повреждения стека или переполнения буфера.
Вопрос 2: Как типы неоднозначных результатов в Swift 5.1 (some Collection) отличаются от коробок стирания типов по производительности и эволюции API?
Неоднозначные типы возвращаемых значений используют обратные обобщения и компиляцию. оптимизация, позволяя компилятору сохранять полную информацию о конкретном типе, при этом скрывая детали реализации от вызывающих. Это избегает штрафов виртуальной диспетчеризации и стоимости выделения памяти, присущих ручным коробкам стирания типов. Тем не менее, неоднозначные типы требуют, чтобы основной тип оставался фиксированным в точке возврата (за исключением нескольких неоднозначных результатов согласно SE-0368), тогда как коробки стирания типов допускают динамическое изменение конкретных типов в пределах одного и того же контейнера во время выполнения, жертвуя производительностью ради полиморфной гибкости.
Вопрос 3: Какие угрозы управления памятью возникают, когда коробки стирания типов захватывают само-ссылочные протоколы (например, протоколы с методами, возвращающими Self) в многопоточных средах?
Коробки стирания типов часто используют упаковки на основе класса или захваты замыканий для хранения конкретных экземпляров. Когда протокол требует возвращения Self или использует ассоциированные типы, ссылающиеся на Self, коробка должна сохранять идентичность типа через семантику ссылок, создавая потенциальные циклы удержания, если конкретный тип хранит обратную ссылку на коробку. В параллельных контекстах несколько потоков, изменяющих упакованное состояние, могут вызвать состояния гонки на подсчете ссылок или внутренних буферах. Разработчики должны убедиться, что оболочка правильно соответствует Sendable, обычно реализуя изоляцию Actor или неизменяемую семантику значений внутри коробки, тем самым предотвращая гонки данных и сохраняя абстракцию интерфейса с типами стирания.