Avatar ·

Как сделать memberwise инициализатор публичным по умолчанию для структур в Swift?

Всем привет! Я начал изучать Swift и столкнулся с проблемой, когда работаю со структурами (struct). По умолчанию Swift генерирует для структур memberwise инициализатор (поэлементный инициализатор), который позволяет создать экземпляр, передав значения для всех свойств. Но если я объявляю структуру с уровнем доступа `public`, memberwise инициализатор становится внутренним (internal), а не публичным. Из-за этого я не могу использовать структуру из другого модуля. Вот пример: ```swift // ModuleA.swift public struct User { public let name: String public let age: Int // memberwise инициализатор не объявлен явно } // ModuleB.swift (другой модуль) let user = User(name: "Alice", age: 30) // Ошибка: 'User' cannot be initialized with memberwise initializer ``` Я понимаю, что могу написать свой публичный инициализатор: ```swift public struct User { public let name: String public let age: Int public init(name: String, age: Int) { self.name = name self.age = age } } ``` Но это добавляет много лишнего кода, особенно если у структуры много свойств. Есть ли способ сделать memberwise инициализатор публичным по умолчанию? Может быть, есть какой-то атрибут или директива? Или это ограничение языка, и нужно всегда писать инициализатор вручную? Буду благодарен за подсказки! <<>>

Проблема: 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 %}

Это продвинутое решение, но оно полностью автоматизирует процесс.