Skip to main content

[From C# to Swift] 10. Properties

Stored Properties
#

1. Core Concepts
#

  • Concept Explanation: This is the most basic form of a property, used to store constant or variable values within an Instance.
  • Key Syntax: var, let
  • Note:

If you create an instance of a Structure and assign it to a constant (let), you cannot modify any of that instance’s properties, even if they were declared as variables (var). This is because structures are Value Types. Classes are Reference Types and are not subject to this limitation.

2. Example
#

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// The range represents integers 0, 1, 2
rangeOfThreeItems.firstValue = 6
// The range now represents integers 6, 7, 8

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)

//rangeOfFourItems.firstValue = 6 
// This line will report an error because rangeOfFourItems is a let (constant), and struct is a Value Type

Logic Explanation: This code demonstrates how to define mutable properties (firstValue) and immutable properties (length) within a Struct. It also shows that when a Struct instance itself is declared as a constant, its internal variable properties also become unmodifiable.

3. C# Comparison
#

Concept Correspondence:

  • Swift’s Stored Property corresponds to C#’s Field or Auto-implemented Property.
  • Swift’s let corresponds to the C# readonly keyword.

C# Example:

public struct FixedLengthRange {
    public int FirstValue;          // Similar to var
    public readonly int Length;     // Similar to let

    public FixedLengthRange(int first, int len) {
        FirstValue = first;
        Length = len;
    }
}
// C# Struct behavior
var range = new FixedLengthRange(0, 3);
range.FirstValue = 6; // Valid

// In C#, if a struct is declared as a readonly field, it behaves similarly to a Swift let instance
// private readonly FixedLengthRange rangeConst = new FixedLengthRange(0, 4);
// rangeConst.FirstValue = 6; // Compilation error; C# also prevents modification

Key Differences Analysis:

  • Syntax: Swift uses let and var keywords for distinction. C# requires the readonly modifier.
  • Behavior: C# Properties ({ get; set; }) are backed by methods, whereas Swift Stored Properties are more akin to direct memory access. The biggest difference lies in mandatory initialization: all Stored Properties in Swift must be assigned a value during the initialization phase (init) or provide a default value; otherwise, compilation fails.

Lazy Stored Properties
#

1. Core Concepts
#

  • Concept Explanation: Some properties require significant time to initialize (e.g., reading files, complex calculations) or depend on other properties being fully initialized first. In these cases, you can use lazy so that the property is not initialized until it is “accessed for the first time.”
  • Key Syntax: lazy var
  • Note:

Lazy properties must be declared as variables (var) because their initial value might not be retrieved until after instance initialization completes. Constant properties (let) must have a value before initialization completes, so they cannot be declared as lazy. Note: If a lazy property is accessed by multiple threads simultaneously when it has not yet been initialized, there is no guarantee that it will be initialized only once (it is not Thread-safe).

2. Example
#

class DataImporter {
    /* Assume this class takes a substantial amount of time to initialize */
    var filename = "data.txt"
}

class DataManager {
    lazy var importer = DataImporter()
    var data: [String] = []
}

let manager = DataManager()
manager.data.append("Some data")
// At this point, the DataImporter instance has not been created yet
print(manager.importer.filename)
// The importer property is created only when this line is executed

Logic Explanation: When DataManager is created, importer does not occupy memory or execute initialization. The DataImporter instance is truly generated only when the code calls manager.importer for the first time.

3. C# Comparison
#

Concept Correspondence:

  • C# does not have a direct keyword equivalent; the closest approach is using the Lazy<T> class.

C# Example:

public class DataManager {
    // C# must use the generic class Lazy<T>
    private Lazy<DataImporter> _importer = new Lazy<DataImporter>(() => new DataImporter());
    
    // Using Expression-bodied member to simplify syntax
    public DataImporter Importer => _importer.Value;
}

Key Differences Analysis:

  • Syntax: Swift’s lazy keyword is supported at the language level and is concise. C# requires declaring Lazy<T> and accessing via .Value.
  • Behavior: C#’s Lazy<T> is Thread-safe by default, whereas Swift’s lazy is not Thread-safe.

Computed Properties
#

1. Core Concepts
#

  • Concept Explanation: These properties do not store values directly. Instead, they provide a getter and an optional setter to indirectly retrieve or compute other property values.
  • Key Syntax: get, set, newValue (default parameter name for setter)

2. Example
#

struct Point { var x = 0.0, y = 0.0 }
struct Size { var width = 0.0, height = 0.0 }
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            // Shorthand Setter: You can use newValue directly here
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Logic Explanation: The center property does not exist in memory. Reading it executes the get block to calculate the center point; setting it executes the set block to reverse-calculate and modify the origin coordinates. Swift supports shorthand: if no parameter name is specified in the setter, newValue is available by default; if the getter has only one line of code, return can be omitted.

3. C# Comparison
#

Concept Correspondence:

  • Fully corresponds to C#’s Properties (get / set blocks).

C# Example:

struct Rect {
    public Point Origin;
    public Size Size;

    public Point Center {
        get {            
            return new Point(Origin.X + (Size.Width / 2), Origin.Y + (Size.Height / 2));
        }
        set {            
            Origin.X = value.X - (Size.Width / 2);
            Origin.Y = value.Y - (Size.Height / 2);
        }
    }
}

Key Differences Analysis:

  • Syntax: C# uses value as the implicit parameter for the setter, while Swift uses newValue (though Swift allows you to customize this name, e.g., set(newCenter) { ... }).
  • Read-only Properties: C# uses { get; }. In Swift, if there is only a getter, you can omit the get { } wrapper and write the content directly inside the braces.

Property Observers
#

1. Core Concepts
#

  • Concept Explanation: Allows you to monitor changes in property values. Code is triggered when a property is about to be set or has just been set. This is commonly used for updating UI or triggering changes in other variables.
  • Key Syntax: willSet (before setting), didSet (after setting)
  • Note:

The willSet and didSet observers of a superclass property are called when the property is set in a subclass initializer. However, when setting its own properties within a class’s own initializer (init), the observers are not called.

2. Example
#

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// Output: About to set totalSteps to 200
// Output: Added 200 steps

Logic Explanation: When totalSteps is assigned a value, the system first executes willSet; at this point, the property still holds the old value, and the new value is passed in as a parameter. After the assignment is complete, didSet is executed; at this point, the property holds the new value, and the old value is accessible via oldValue (default name).

3. C# Comparison
#

Concept Correspondence:

  • C# has no direct syntax equivalent.

Key Differences Analysis:

  • Syntax: Swift’s willSet/didSet is very concise. C# requires more verbose implementation, often relying on INotifyPropertyChanged to handle similar requirements.
  • Behavior: Swift’s observers trigger whenever the property is “set”, even if the new value is equal to the old value.

Property Wrappers
#

1. Core Concepts
#

  • Concept Explanation: Used to encapsulate property read/write logic (e.g., numeric constraints, UserDefaults storage, Thread-safety locks). Through the @WrapperName syntax, this logic can be reused.
  • Key Syntax: @propertyWrapper, wrappedValue, projectedValue ($)

2. Example
#

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
rectangle.height = 24
print(rectangle.height) 
// Output "12", because it was constrained by the wrapper

Logic Explanation: TwelveOrLess is a structure that defines a wrappedValue. When we prepend @TwelveOrLess to height, the compiler automatically forwards access to height to the wrappedValue getter/setter of TwelveOrLess. This allows validation logic to be written just once.

Projected Value ($): Swift also allows Wrappers to provide additional information (Projected Value).

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()


someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false".

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"

Logic Explanation: This code demonstrates that a Property Wrapper can not only manage the primary value (wrappedValue) but also expose auxiliary state by defining a projectedValue. Externally, you can directly access this projected value by prepending a $ symbol to the property name (e.g., $someNumber), allowing you to determine if the assignment just triggered the upper limit constraint.

3. C# Comparison
#

Concept Correspondence:

  • There is no direct syntax equivalent in C#.
  • Visually, it looks like C# Attributes ([Attribute]), but the behavior is completely different. C# Attributes are primarily Metadata waiting to be read via reflection; Swift Property Wrappers are active, directly intervening in the property’s getter/setter logic.

Key Differences Analysis:

  • Syntax: The $ symbol (Projected Value) is a concept unique to Swift, allowing developers to directly access auxiliary functionality exposed by the Wrapper (for example, the Binding mechanism in SwiftUI makes extensive use of $State to retrieve Bindings).

Type Properties
#

1. Core Concepts
#

  • Concept Explanation: Properties that belong to the “Type” itself rather than a single instance. Regardless of how many instances are created, there is only one copy of the type property.
  • Key Syntax: static (Struct/Enum/Class), class (Class only, allows subclass override)
  • Note:

Stored Type Properties must have a default value because the Type itself has no initializer. Furthermore, they are lazy by default (initialized only on first access) and are guaranteed to be Thread-safe (initialized only once).

2. Example
#

struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}

class SomeClass {
    static var storedTypeProperty = "Some value."
    // Using the class keyword allows subclasses to override this computed property
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

Logic Explanation: This code demonstrates how to define properties belonging to the type itself. The static keyword is used to define type properties in a Struct or Class. Notably, in SomeClass, overrideableComputedTypeProperty uses the class keyword instead of static, meaning this computed property allows subclasses to override it.

3. C# Comparison
#

Concept Correspondence:

  • Corresponds to static members in C#.

C# Example:

class SomeClass {
    public static string StoredTypeProperty = "Some value.";
    
    // C# static members cannot be overridden; this is the biggest difference from Swift
    public static int ComputedTypeProperty {
        get { return 1; }
    }
}

Key Differences Analysis:

  • Override Capability: In C#, static members cannot be overridden via inheritance. However, in Swift, if you declare a computed type property using the class keyword within a Class, subclasses can override it. This provides more flexible static polymorphism than C#.
  • Thread Safety: C# static field initialization usually relies on the Static Constructor to guarantee execution order, but it does not necessarily guarantee Thread-safety during access (manual locking may be required). Swift’s official documentation explicitly guarantees that the initialization of static stored properties is Thread-safe.