Параметризованный @PropertyWrapper с параметрами, отличными от параметров по умолчанию в автоматически синтезируемом методе инициализации?

v1. Нет параметров: ✅ работает должным образом

Обычно я могу создать обрезанную оболочку следующим образом:

@propertyWrapper
struct Trimmed {
  private(set) var value: String = ""

  var wrappedValue: String {
    get { value }
    set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
  }

  init(wrappedValue: String) {
    self.wrappedValue = wrappedValue
  }
}

struct Post {
  @Trimmed
  var title: String
  @Trimmed
  var body: String = ""
}

let post = Post(
  title: "  title  ",
  body: "  body  "
)
post.title == "title"
post.body == "body"

Обратите внимание, как он безупречно работает как для параметров без значений по умолчанию (например, title), так и для параметров со значениями по умолчанию (например, body).


v2. Один параметр: ❌ Не компилируется

Теперь представьте, что я не хочу жестко кодировать .whitespacesAndNewlines, а вместо этого разрешаю разработчику указать это значение:

@propertyWrapper
struct Trimmed2 { // ????
  private(set) var value: String = ""
  let characterSet: CharacterSet // ????

  var wrappedValue: String {
    get { value }
    set { value = newValue.trimmingCharacters(in: characterSet) } // ????
  }

  init(
    wrappedValue: String,
    characterSet: CharacterSet // ????
  ) {
    self.characterSet = characterSet
    self.wrappedValue = wrappedValue
  }
}

struct Post2 {
  @Trimmed2(characterSet: .whitespaces) // ❌ Missing argument for parameter 'wrappedValue' in call
  var title: String
  @Trimmed2(characterSet: .whitespaces)
  var body: String = ""
}
 

Первая проблема, с которой я столкнулся, заключается в том, что title, параметр без значения по умолчанию, не компилируется. Это требует, чтобы я добавил значение wrappedValue.


v3. Укажите значения свойств по умолчанию: ⚠️ Потребители могут не указывать параметр

Самый простой способ исправить эту ошибку компилятора - присвоить ей значение по умолчанию, например body:

struct Post3 {
  @Trimmed2(characterSet: .whitespaces)
  var title: String = ""
  @Trimmed2(characterSet: .whitespaces)
  var body: String = ""
}

let post3 = Post3(
  // ⚠️ Undesirable since `title` can now be left out of the constructor
  body: "  body  "
) // ????
post3.title == "" // ⚠️
post3.body == "body"

Однако теперь я потерял возможность заставить потребителей предоставлять значение title через автосинтезированный конструктор.


v4. По умолчанию wrappedValue: ⚠️ PropertyWrapper, доступный для потребителей

Если вместо того, чтобы указать значение по умолчанию, я соблюдаю исходное сообщение об ошибке и предлагаю wrappedValue, title теперь снова требуется, но это имеет гораздо более серьезные проблемы.

struct Post4 {
  @Trimmed2(wrappedValue: "", characterSet: .whitespaces)
  var title: String
  @Trimmed2(characterSet: .whitespaces)
  var body: String = ""
}

let post4 = Post4(
  title: .init(wrappedValue: "  title  ", characterSet: .decimalDigits), // ⚠️ PropertyWrapper exposed to consumers
  body: "  body  ")

post4.title == "  title  " // ⚠️ Whitespace no longer removed
post4.body == "body"

Более серьезная проблема заключается в том, что Trimmed теперь доступен потребителям, поэтому они не могут просто предоставить значение String, и, что еще хуже, они могут изменить поведение структуры (например, путем предоставления другого characterSet).


v5. Предоставление пользовательской инициализации: ⚠️ Больше не будет автоматически синтезироваться инициализация.

Один из способов решить все эти проблемы - не полагаться на самосинтезируемый init, а вместо этого использовать свой собственный. Чтобы устранить синтаксическую ошибку в версии 2, также необходимо указать значение по умолчанию для title. Это можно сделать так же, как это делается для body (например, var title: String = ""), или путем добавления значения по умолчанию в Trimmed.wrappedValue. Оба они функционально эквивалентны.

@propertyWrapper
struct Trimmed5 {
  private(set) var value: String = ""
  let characterSet: CharacterSet

  var wrappedValue: String {
    get { value }
    set { value = newValue.trimmingCharacters(in: characterSet) }
  }

  init(
    wrappedValue: String = "", // ????
    characterSet: CharacterSet
  ) {
    self.characterSet = characterSet
    self.wrappedValue = wrappedValue
  }
}

struct Post5 {
  @Trimmed5(characterSet: .whitespaces)
  var title: String
  @Trimmed5(characterSet: .whitespaces)
  var body: String = ""

  init(title: String, body: String = "") {
    self.title = title
    self.body = body
  }
}

let post5 = Post5(title: "  title  ", body: "  body  ")
post5.title == "title"
post5.body == "body"

Однако мне интересно, есть ли способ, чтобы параметризованный PropertyWrapper + без аргументов по умолчанию + автоматически синтезируемые конструкторы хорошо работали вместе.


Если у меня есть параметризованный PropertyWrapper, как мне заставить потребителя предоставить ему значение в его автосинтезируемом конструкторе?

(например, как мне заставить v2 скомпилировать без нежелательных побочных эффектов?)


Примечание. Первоначальный вопрос заключался в том, чтобы значения свойств по умолчанию для параметризованных значений работали должным образом, но после некоторых исследований выяснилось, что основная проблема связана с упомянутой выше. Таким образом вопрос был упрощен.


person Senseful    schedule 31.07.2020    source источник
comment
На данный момент это кажется невозможным, и есть предложение изменить это: forum.swift.org/t/   -  person New Dev    schedule 31.07.2020


Ответы (2)


Вы можете попробовать следующее:

@propertyWrapper
struct Trimmed {
    private var value: String?
    private let defaultValue: String
    private let characterSet: CharacterSet

    var wrappedValue: String {
        get { value ?? defaultValue }
        set { value = newValue.trimmingCharacters(in: characterSet) }
    }

    init(value: String? = nil, defaultValue: String = "", characterSet: CharacterSet = .whitespaces) {
        if let value = value {
            self.value = value.trimmingCharacters(in: characterSet)
        }
        self.defaultValue = defaultValue
        self.characterSet = characterSet
    }
}

ИЗМЕНИТЬ

Судя по вашим многократным изменениям в вопросе, проблема не в оболочке свойств, а также в структуре.

Вы хотите, чтобы оба @Trimmed были постоянными в структуре, но в противном случае параметризовались. Таким образом, вы можете либо ограничить изменение переменных сообщения (с помощью private(set)), либо создать собственный @Trimmed только с определенными полями.

struct Post {
    @Trimmed(characterSet: .whitespaces)
    private(set) var title: String

    @Trimmed(defaultValue: "")
    private(set) body: String
    
    init(title: String, body: String) {
        self.title = .init(title)
        self.body = .init(body)
    }
}

let post = Post(
    title: "text",
    body: "body"
)
person pawello2222    schedule 31.07.2020
comment
Это та же проблема, что и в версии 4: (1) потребители должны предоставлять PropertyWrapper; и аналогичная проблема с v5: (2) characterSet должен быть предоставлен разработчиком. Одним из желаемых вариантов поведения является то, что оболочка свойств имеет значение по умолчанию для набора символов. - person Senseful; 31.07.2020
comment
@Senseful Пожалуйста, проверьте обновленную версию для (1) и (2). - person pawello2222; 31.07.2020
comment
@Senseful Я вижу, вы значительно изменили свой вопрос. Однако мой ответ остается в силе. Пожалуйста, проверьте, что вы ищете. - person pawello2222; 31.07.2020
comment
Вы больше не можете указывать значение по умолчанию для свойств в обновленной версии: struct Post7 { @Trimmed var body: String = "" } } Ошибка: Extra argument 'wrappedValue' in call - person Senseful; 31.07.2020
comment
@Senseful Но вы можете сделать это с параметром defaultValue, равным @Trimmed. - person pawello2222; 31.07.2020
comment
Последняя версия w / Post, похоже, будет работать в основном благодаря явному конструктору. Интересная идея с параметром default, чтобы обойти проблему с аргументами по умолчанию + аргументы без значений по умолчанию, которые ведут себя иначе. Однако мне все еще интересно, есть ли способ (1) избежать явного конструктора, (2) продолжать полагаться на реализацию значений по умолчанию в Swift. Похоже, что если у нас все в порядке с явным конструктором, нам в любом случае не понадобится defaultValue. Позвольте мне обновить сообщение, чтобы прояснить это. - person Senseful; 31.07.2020
comment
И да, вы правы, похоже, что это больше проблема со стороны использования, а не со стороны PropertyWrapper. В конце моего исходного сообщения я обновил его, чтобы указать, что, по моему мнению, более фундаментальная проблема заключается в том, что автоматически синтезированный init + PropertyWrapper со свойствами плохо сочетается друг с другом. Затем сообщение было помечено как недостаточно конкретное, поэтому я упростил его с учетом этого нового вывода, чтобы устранить этот флаг. Приносим извинения за то, что он не был обновлен до вашего ответа. Тем не менее, могут быть некоторые интересные самородки с этим defaultValue, которые вы предлагаете, которые могут помочь - person Senseful; 31.07.2020
comment
@Senseful Если вы решите использовать собственный init, вы можете указать ему параметры по умолчанию: init(title: String = "", body: String = ""). Тогда нет смысла делать @Trimmed var body: String = "", если вы планируете реализовать собственный init или если вы можете использовать параметр defaultValue оболочки свойства. Кажется, я ответил на твой вопрос. Если у вас все еще есть сомнения или не все было объяснено, возможно, вопрос действительно требует более пристального внимания. - person pawello2222; 31.07.2020
comment
Хорошее замечание о = "" в Post. Я могу поместить его в метод Trimmed.init, чтобы приблизиться к v2. Я обновил сообщение, чтобы отразить это. Однако я не вижу способа заставить defaultValue работать без этого специального метода инициализации. Если я попробую ваш код и удалю пользовательский метод init, я получу Post, который требует от потребителей предоставления Trimmed значений. Похоже, что для работы defaultValue требуется специальный метод инициализации. И если пользовательский метод инициализации уже предоставлен, мы могли бы также использовать более простой синтаксис Swift для предоставления значений по умолчанию (например, v5). - person Senseful; 31.07.2020
comment
@Senseful К сожалению, как вы видите в комментариях, то, что вы хотите, в настоящее время невозможно. Надеюсь, мой ответ помог как обходной путь :) - person pawello2222; 31.07.2020

Как упоминалось в , в настоящее время это не поддерживается, и существует предложение изменить это.

Один из способов решения проблемы - стереть все аргументы с помощью композиции. Это похоже на пример @UnitInterval, где для внутренних целей используется @Clamping.

Вот как это может выглядеть для Trimmed:

@propertyWrapper
struct Trimmed {
  private(set) var value: String = ""
  let characterSet: CharacterSet

  var wrappedValue: String {
    get { value }
    set { value = newValue.trimmingCharacters(in: characterSet) }
  }

  init(
    wrappedValue: String,
    characterSet: CharacterSet
  ) {
    self.characterSet = characterSet
    self.wrappedValue = wrappedValue
  }
}

@propertyWrapper
struct TrimmedWhitespace {
  @Trimmed(characterSet: .whitespaces)
  var wrappedValue: String = ""

  init(wrappedValue: String) {
    self.wrappedValue = wrappedValue
  }
}

struct Post {
  @TrimmedWhitespace
  var title: String
  @TrimmedWhitespace
  var body: String = ""
}

let post = Post(title: "  title  ", body: "  body  ")
post.title == "title"
post.body == "body"

Обратите внимание, как это работает так же, как v1, где аргумент со значением по умолчанию и аргумент без обоих работают, как ожидалось.

person Senseful    schedule 01.08.2020