We don't have "assign if different" in Swift other than writing it explicitly:
if a != b {
a = b
}
Which is not ideal and getting worse in case of complex expressions:
let b = expression(x, y, z)
if foo[123].bar[baz] != b {
foo[123].bar[baz] = b
}
Assigning the same value may be benign but in general case it can cause side effects (like didSet) and might be unwanted if the value is unchanged.
I wonder if there's a precedent of "assign if different" operator in other languages?
Is there some tradition for it established in third party libraries?
Would there be an interest to have it built into Swift or is it infrequently used case to bother optimising?
@IBInspectable public var firstColor: UIColor = .white {
didSet { updateGradient() }
}
@IBInspectable public var middleColor: UIColor? {
didSet { updateGradient() }
}
@IBInspectable public var lastColor: UIColor = .black {
didSet { updateGradient() }
}
// didSet here leads to unneeded side effects if the same color is assigned
Similar examples:
public var textAndTrailingImageSpacing: CGFloat = 8 {
didSet { textAndTrailingImageSpacingConstraint?.constant = textAndTrailingImageSpacing }
}
open override var isEnabled: Bool {
didSet {
if oldValue != isEnabled { updateAppearance() }
}
}
public var cornerRadius: CGFloat = 0 {
didSet { updateCorners(cornerRadius: cornerRadius) }
}
public var minSize = CGSize(width: 16, height: 16) {
didSet { invalidateIntrinsicContentSize() }
}
private var items: [String] = [] {
didSet { collectionView.reloadData() }
}
private var currentSelectedItemIndex: Int = 0 {
didSet {
if let points = model[uncheckedIndex: currentSelectedItemIndex]?.pointsForReview {
subtitleLabel.setText("Text", animated: true) // unwanted animation
}
}
}
private var currentIndex: Int = 0 {
// heavy operation
didSet { imageView.image = images[uncheckedIndex: currentIndex].grayscale() }
}
public var color: Color {
didSet { hasUpdate = true }
}
Non UI examples:
var phoneString: String? {
didSet {
phone = validated(ctnString: phoneString)
}
}
private(set) var state: State = .isUploading {
didSet { didChangeStateObserver(state) }
}
private var pages: [TutorialPage] = [] {
didSet { pages.forEach { $0.pageListener = self } }
}
private(set) var count: Int = 0 {
didSet {
Log("Count didSet with value: \(count)")
.category(.database).level(.info)
if count != oldValue {
onObjectsDidChange()
}
guard count > limit else { return }
do {
try cleanUp()
} catch {
Log("Unable to remove first element")
.category(.database).level(.error)
}
}
}
var userUUID: String? {
didSet {
configuration?.previousUserUUID = userUUID
onDidChange?()
}
}
var token: String? {
didSet {
tokenSaveDate = Date()
onDidChange?()
}
}
weak var delegate: MyDelegate? {
didSet {
guard let error = initialError else { return }
delegate?.logError(with: self, error: error)
}
}
My first inclination is that for an example like this, I would much rather see the logic to avoid the update in didSet, rather than where the value is being set:
didSet {
if val != oldValue { updateGradient() }
}
It's not always practical to do it on the "inside" (e.g. willSet / didSet in parent classes, all places need to be modified), or possible (e.g. setting a @Published var property). Plus the above example still stands:
let card = modifiedCard(location: recognizer.location(ofTouch: touchIndex, in: view))
if boards[boardIndex].cards[cardIndex] != card {
boards[boardIndex].cards[cardIndex] = card
}
Not being able to have more than two arguments with operators makes me think a function is best. Tuples as function arguments is too terrible.
public func assignOutput<Value>(
of makeValue: (Value, Value) -> Value,
with newValue: Value,
to value: inout Value
) {
value = makeValue(value, newValue)
}
var a = 1
assignOutput(of: max, with: 2, to: &a)
a // 2
assign(2, to: &a, if: <)
public func assign<Value>(
_ newValue: Value,
to value: inout Value,
if predicate: (Value, Value) -> Bool
) {
if predicate(value, newValue) {
value = newValue
}
}
infix operator =!=: AssignmentPrecedence
public func =!= <Value: Equatable>(value: inout Value, newValue: Value) {
assign(newValue, to: &value, if: !=)
}
func assign<T: Equatable>(_ a: inout T, _ b: T, `if` condition: (T, T) -> Bool) {
if condition(a, b) {
a = b
}
}
var a = 4
let b = 2
assign(&a, b, if: <)
ditto for !=
Heh, just realised @anon9791410 already suggested this.
How would this new operator avoid the hash calculation? Wouldn't it still need the hash in order to perform the lookup to see if there were a difference? If there are heavy side-effects, I'd rather see that closer to the side-effects rather than the assigner having to know those internal details.
Most of the time it is not expected (what is the definition of side-effect) to do something, when you get/set property's value. So doing something in willSet, didSet in most of the situations produces side-effected and inconsequential code. So if another programmer would like to modify your code he should learn first what you do in these observers (probably he will learn it seeing unexpected behaviour of the final program).
P.S. It makes it inconsequential because modifying properties in function bodies you should scroll up and see the block, that will be executed after getting/assigning value
One case where I've wanted to do something like this was when using Core Data. Assigning any value to a modeled property, even if it's the same value, results in the object being marked "dirty", and thus means that it will be included in the next save transaction. That results in more I/O traffic and higher memory usage, so we try to avoid re-assigning an equal value when possible.
I've written this helper, and it works well for me:
extension NSManagedObject {
public func updateIfNotEqual<T: Equatable>(
_ path: ReferenceWritableKeyPath<Self, T>,
_ newValue: T)
{
if self[keyPath: path] != newValue {
self[keyPath: path] = newValue
}
}
}
// Used like this:
person.updateIfNotEqual(\.name, updatedName)