Как сделать memberwise инициализатор публичным по умолчанию для структур в Swift?
Проблема: memberwise инициализатор и модификаторы доступа
Да, вы правы: в Swift memberwise инициализатор для public структуры автоматически получает уровень доступа internal. Это сделано намеренно — компилятор не может гарантировать, что все свойства структуры публичны, поэтому он генерирует инициализатор с минимально необходимым уровнем доступа. Однако есть несколько способов решить эту проблему.
Решение 1: Явное объявление публичного инициализатора
Самый простой и прямой способ — написать инициализатор вручную. Да, это добавляет код, но даёт полный контроль:
public struct User {
public let name: String
public let age: Int
public init(name: String, age: Int) {
self.name = name
self.age = age
}
}
Это надёжно и понятно. Если свойств много, можно использовать вспомогательные инструменты:
- Xcode Code Snippets — создайте сниппет для структуры с публичным инициализатором.
- SwiftGen — генерирует код автоматически.
- Рефакторинг — в Xcode есть опция "Generate Memberwise Initializer" (Editor > Refactor).
Решение 2: Использование протокола с публичным инициализатором
Можно определить протокол, который требует публичный инициализатор, а затем расширить структуру:
public protocol PubliclyInitializable {
init(name: String, age: Int)
}
public struct User: PubliclyInitializable {
public let name: String
public let age: Int
}
Но здесь вы всё равно должны явно реализовать инициализатор, иначе компилятор выдаст ошибку. Этот подход полезен, если у вас много структур с одинаковой сигнатурой.
Решение 3: Использование фабричного метода (не инициализатора)
Если вы не хотите писать инициализатор, можно сделать публичный статический метод, который возвращает экземпляр:
public struct User {
public let name: String
public let age: Int
public static func create(name: String, age: Int) -> User {
User(name: name, age: age) // Работает, так как internal инициализатор доступен внутри модуля
}
}
// В другом модуле:
let user = User.create(name: "Alice", age: 30) // Работает
Минус — вы теряете синтаксический сахар User(name:age:).
Решение 4: Использование атрибута @inlinable (Swift 5.9+)
Начиная с Swift 5.9, можно использовать атрибут @inlinable, чтобы сделать memberwise инициализатор публичным:
public struct User {
@inlinable
public init(name: String, age: Int) {
self.name = name
self.age = age
}
public let name: String
public let age: Int
}
@inlinable указывает компилятору встроить реализацию инициализатора в вызывающий код, что делает его доступным из других модулей. Однако это всё равно требует явного объявления инициализатора.
Итог
На данный момент в Swift нет способа автоматически сделать memberwise инициализатор публичным без явного объявления. Это ограничение языка, связанное с контролем доступа. Лучшая практика — писать инициализатор вручную. Если вы хотите минимизировать код, используйте фабричный метод или автоматическую генерацию через Xcode.
В будущих версиях Swift возможно появление атрибута вроде @publicMemberwiseInit, но пока это не реализовано.
Альтернативный подход: Использование value type с публичными свойствами и internal инициализатором
Если ваша структура используется только внутри одного модуля (например, в рамках одного фреймворка), вы можете объявить её как public, но оставить memberwise инициализатор internal. Это работает, если клиенты вашего модуля создают экземпляры только через фабричные методы или другие публичные API.
// ModuleA.swift
public struct User {
public let name: String
public let age: Int
// memberwise инициализатор — internal
}
// Внутри ModuleA:
func createDefaultUser() -> User {
User(name: "Default", age: 18) // Работает, так как мы внутри модуля
}
// ModuleB.swift
let user = User(name: "Bob", age: 25) // Ошибка: инициализатор internal
Этот подход полезен, когда вы хотите контролировать создание объектов и скрыть внутренние детали. Например, вы можете предоставить публичный инициализатор только с частью параметров или с валидацией.
Дополнение: Автоматическая генерация через Sourcery
Для больших проектов можно использовать инструмент Sourcery, который генерирует код на основе шаблонов. Вы можете написать шаблон, который автоматически создаёт публичный memberwise инициализатор для всех структур с модификатором public. Это экономит время и уменьшает количество ручного кода.
// Шаблон Sourcery (пример)
{% for type in types.structs where type.accessLevel == "public" %}
extension {{ type.name }} {
public init({{ type.storedVariables | map: "\($0.name): \($0.typeName)" | join: ", " }}) {
{% for var in type.storedVariables %}
self.{{ var.name }} = {{ var.name }}
{% endfor %}
}
}
{% endfor %}
Это продвинутое решение, но оно полностью автоматизирует процесс.
Ответов пока нет.
Войдите чтобы оставить ответ