简体   繁体   中英

Change the value that is being set in variable's willSet block

I'm trying to sort the array that is being set before setting it but the argument of willSet is immutable and sort mutates the value. How can I overcome this limit?

var files:[File]! = [File]() {
    willSet(newFiles) {
        newFiles.sort { (a:File, b:File) -> Bool in
            return a.created_at > b.created_at
        }
    }
}

To put this question out of my own project context, I made this gist:

class Person {
    var name:String!
    var age:Int!

    init(name:String, age:Int) {
        self.name = name
        self.age = age
    }
}

let scott = Person(name: "Scott", age: 28)
let will = Person(name: "Will", age: 27)
let john = Person(name: "John", age: 32)
let noah = Person(name: "Noah", age: 15)

var sample = [scott,will,john,noah]



var people:[Person] = [Person]() {
    willSet(newPeople) {
        newPeople.sort({ (a:Person, b:Person) -> Bool in
            return a.age > b.age
        })

    }
}

people = sample

people[0]

I get the error stating that newPeople is not mutable and sort is trying to mutate it.

It's not possible to mutate the value inside willSet . If you implement a willSet observer, it is passed the new property value as a constant parameter.


What about modifying it to use didSet ?

 var people:[Person] = [Person]() { didSet { people.sort({ (a:Person, b:Person) -> Bool in return a.age > b.age }) } }

willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.

You can read more about property observers here https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html

You can also write a custom getter and setter like below. But didSet seems more convenient.

 var _people = [Person]() var people: [Person] { get { return _people } set(newPeople) { _people = newPeople.sorted({ (a:Person, b:Person) -> Bool in return a.age > b.age }) } }

It is not possible to change value types (including arrays) before they are set inside of willSet . You will need to instead use a computed property and backing storage like so:

var _people = [Person]()

var people: [Person] {
    get {
        return _people
    }
    set(newPeople) {
        _people = newPeople.sorted { $0.age > $1.age }
    }
}

Another solution for people who like abstracting away behavior like this (especially those who are used to features like C#'s custom attributes) is to use a Property Wrapper , available since Swift 5.1 (Xcode 11.0).

First, create a new property wrapper struct that can sort Comparable elements:

@propertyWrapper
public struct Sorting<V : MutableCollection & RandomAccessCollection>
    where V.Element : Comparable
{
    var value: V
    
    public init(wrappedValue: V) {
        value = wrappedValue
        value.sort()
    }
    
    public var wrappedValue: V {
        get { value }
        set {
            value = newValue
            value.sort()
        }
    }
}

and then assuming you implement Comparable -conformance for Person :

extension Person : Comparable {
    static func < (lhs: Person, rhs: Person) -> Bool {
        lhs.age < lhs.age
    }
    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.age == lhs.age
    }
}

you can declare your property like this and it will be auto-sorted on init or set:

struct SomeStructOrClass
{
    @Sorting var people: [Person]
}


// … (given `someStructOrClass` is an instance of `SomeStructOrClass`)

someStructOrClass.people = sample

let oldestPerson = someStructOrClass.people.last

Caveat: Property wrappers are not allowed (as of time of writing, Swift 5.7.1) in top-level code— they need to be applied to a property var in a struct, class, or enum.


To more literally follow your sample code, you could easily also create a ReverseSorting property wrapper:

@propertyWrapper
public struct ReverseSorting<V : MutableCollection & RandomAccessCollection & BidirectionalCollection>
    where V.Element : Comparable
{
    // Implementation is almost the same, except you'll want to also call `value.reverse()`:
    //   value = …
    //   value.sort()
    //   value.reverse()
}

and then the oldest person will be at the first element:

// …
    @Sorting var people: [Person]
// …

someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]

And even more directly, if your use-case demands using a comparison closure via sort(by:…) instead of implementing Comparable conformance, you can do that to:

@propertyWrapper
public struct SortingBy<V : MutableCollection & RandomAccessCollection>
{
    var value: V
    
    private var _areInIncreasingOrder: (V.Element, V.Element) -> Bool
    
    public init(wrappedValue: V, by areInIncreasingOrder: @escaping (V.Element, V.Element) -> Bool) {
        _areInIncreasingOrder = areInIncreasingOrder
        
        value = wrappedValue
        value.sort(by: _areInIncreasingOrder)
    }
    
    public var wrappedValue: V {
        get { value }
        set {
            value = newValue
            value.sort(by: _areInIncreasingOrder)
        }
    }
}
// …
    @SortingBy(by: { a, b in a.age > b.age }) var people: [Person] = []
// …

someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]

Caveat: The way SortingBy 's init currently works, you'll need to specify an initial value ( [] ). You can remove this requirement with an additional init (see Swift docs ), but that approach much less complicated when your property wrapper works on a concrete type (eg if you wrote a non-generic PersonArraySortingBy property wrapper), as opposed to a generic-on-protocols property wrapper.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM