The beauty of Swift Language — Property Wrapper

👍
· Aug 26, 2023 · 5 mins read
The beauty of Swift Language — Property Wrapper

In essence, a property of an object in OO languages can access to its backed storage directly or can have getter /setter to access to the backed storage indirectly. For the latter case, we can write logic needed in the getter/setter. To develop this idea further, what if we need to reuse the logic in getter/setter for other properties of this object and even properties of other objects? Proper Wrapper in Swift is invented for this purpose. Even though there’s no such concept in C# and Kotlin, but we can achieve the similar goal in those languages as well, which will be discussed in this article.

Swift

In this section, we will write a property wrapper for a Score object below. Its property math is Int type. However, as we all know, the score of math is within a range (like from 0 to 100) and its value should not go outside of this range. If we just define them as Int type, we might have wrong score for this subject(like -10 or 120 etc.). To avoid this kind of data error, we can add some check logic in setter like below:

struct Score {  
    private var _math: Int = 0  
    var math: Int {  
        get {  
            _math  
        }  
        set {  
            _math = max(min(newValue, 100), 0)  
        }  
    }  
}  
  
var myScore = Score()  
myScore.math = 120  
print(myScore.math) // will print out 100

This works well as we fixed the possible data error using max(min(newValue, 100), 0) in setter . However, if we need more properties to store scores of other subjects, we need to repeat this boilerplate code for each subject. Property Wrapper comes to rescue like below.

struct Score {  
    @ScoreRange var math: Int  
    @ScoreRange var physics: Int  
    @ScoreRange var chemistry: Int  
}  
  
@propertyWrapper  
struct ScoreRange {  
    private var score = 0  
    var wrappedValue: Int {  
        get {  
            score  
        }  
        set {  
            score = max(min(newValue, 100), 0)  
        }  
    }  
}  
  
var myScore = Score()  
myScore.math = 120  
print(myScore.math) // will print out 100  
myScore.physics = 120  
print(myScore.physics) // will print out 100  
myScore.chemistry = 120  
print(myScore.chemistry) // will print out 100

In the above code, we define ScoreRange Property Wrapper and hold the setter logic and apply this wrapper to all the properties of the Score struct to achieve the code reuse purpose.

In actual code, we can write a more powerful property wrapper using generic to make it more flexible like below:

struct Score {  
    @Ranged(0...100) var math: Int = 0  
    @Ranged(0...120) var physics: Int = 0  
    @Ranged(0...150) var chemistry: Int = 0  
}  
  
@propertyWrapper  
struct Ranged<T: Comparable> {  
    let range: ClosedRange<T>  
    var value: T  
  
    init(wrappedValue: T, \_ range: ClosedRange<T>) {  
        self.value = max(min(wrappedValue, range.upperBound), range.lowerBound)  
        self.range = range  
    }  
  
    var wrappedValue: T {  
        get {  
            value  
        }  
        set {  
            value = max(min(newValue, range.upperBound), range.lowerBound)  
        }  
    }  
}  
  
var myScore = Score()  
myScore.math = 200  
print(myScore.math) // will print out 100  
myScore.physics = 200  
print(myScore.physics) // will print out 120  
myScore.chemistry = 200  
print(myScore.chemistry) // will print out 150

More explanation about Swift property wrapper can be found in this article where also explained the Projected Value of Swift Property Wrappers.

C#

There’s no Property wrapper concept predefined in C#. The getter/setter might be the best choice if we want to add extra logic on getting/setting value to a property, hence it’s not directly supported to reuse the logic like the solution Property Wrapper offers in Swift. The most closest concept to it is [Attribute](https://learn.microsoft.com/en-us/dotnet/standard/attributes/) , which allows us to attach metadata to properties that can be extracted using runtime reflection services.

Please note that, regarding the functionality in the example code in Swift section to set a ranged value to a property, ASP.NET has defined a RangeAttribute Class to offer similar function.

Kotlin

Delegated property is a powerful feature in Kotlin that can be used for reusing common logic for properties. For this we just need to create a delegate class and let this class handle the common logic for the properties. The equivalent implementation of ScoreRange in Kotlin could look below:

import kotlin.reflect.KProperty  
  
class Score {  
    var math: Int by ScoreRange(0)  
    var physics: Int by ScoreRange(0)  
    var chemistry: Int by ScoreRange(0)  
}  
  
class ScoreRange(private var value: Int = 0) {  
    operator fun getValue(thisRef: Any?, property: KProperty<\*>): Int {  
        return value  
    }  
    operator fun setValue(thisRef: Any?, property: KProperty<\*>, value: Int) {  
        this.value = minOf(100, maxOf(0, value))  
    }  
}  
  
  
fun main() {  
    val score = Score()  
    score.math = 120  
    println(score.math) // will print out 100  
    score.physics = 120  
    println(score.physics) // will print out 100  
    score.chemistry = 120  
    println(score.chemistry) // will print out 100  
}

Of course, we can also implement its generic version Ranged like below:

import kotlin.reflect.KProperty  
  
class Score {  
    var math: Int by Ranged(0, minValue = 0, maxValue = 100)  
    var physics: Int by Ranged(0, minValue = 0, maxValue = 120)  
    var chemistry: Int by Ranged(0, minValue = 0, maxValue = 150)  
}  
  
class Ranged<T: Comparable<T>>(private var value: T, private val minValue: T, private val maxValue: T) {  
    operator fun getValue(thisRef: Any?, property: KProperty<\*>): T {  
        return value  
    }  
    operator fun setValue(thisRef: Any?, property: KProperty<\*>, value: T) {  
        this.value = minOf(maxValue, maxOf(minValue, value))  
    }  
}  
  
fun main() {  
    val score = Score()  
    score.math = 200  
    println(score.math) // will print out 100  
    score.physics = 200  
    println(score.physics) // will print out 120  
    score.chemistry = 200  
    println(score.chemistry) // will print out 150  
}

Conclusion

By comparison, we can clearly see that, while C# doesn’t have native solution to reuse common logic for properties, both Swift and Kotlin have native solutions to it and it’s called Property Wrapper in Swift and Delegate Property in Kotlin.

Please also note that, because of the design differences, while they can be used for common logics for properties, these language features are not completely equal. Like Swift property wrapper has projected value that is widely used in SwiftUI while [Delegate Property](https://kotlinlang.org/docs/delegated-properties.html) in Kotlin has other use cases too.

Comments

Sharing is caring!