Skip to main content

[From C# to Swift] 10. Properties

Learning Swift from a C# Perspective

Swift : Properties

Stored Properties
#

1. Core Concepts
#

  • Concept Explanation: This is the most basic form of property, used to store values of constants or variables in an instance. This concept is similar to Fields in C#, but Swift properties have a higher degree of integration.
  • Key Syntax: var (variable), let (constant).
  • Official Note:

If you create an instance of a structure (Structure) and assign it to a constant (let), you cannot modify any of the 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 Analysis
#

Documentation Source Code:

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 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 immutable.

3. C# Developer Perspective
#

Concept Mapping:

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

C# Comparison Code:

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; // Compiler error, C# also prevents modification

Key Difference Analysis:

  • Syntax: Swift’s distinction using let and var keywords is very intuitive. C# requires the readonly modifier.
  • Behavior: C# Properties ({ get; set; }) technically have methods behind them, whereas Swift Stored Properties are more like direct memory access (though unified in syntax). The biggest difference lies in Swift’s forced initialization: all Stored Properties must be assigned a value during the initialization phase (init) or provide a default value; otherwise, compilation fails. C# allows fields to retain default values (0 or null).

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 initialized first. In such cases, you can use lazy so that the property is initialized only when “accessed for the first time.”
  • Key Syntax: lazy var
  • Official Note:

Lazy properties must always 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, and therefore cannot be declared as lazy. Note: If a lazy property is accessed by multiple threads simultaneously and the property has not yet been initialized, there is no guarantee that the property will be initialized only once (it is not Thread-safe).

2. Example Analysis
#

Documentation Source Code:

class DataImporter {
    /* Assume this class takes a significant 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")
// 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 actually created only when the code calls manager.importer for the first time.

3. C# Developer Perspective
#

Concept Mapping:

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

C# Comparison Code:

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

Key Difference Analysis:

  • Syntax: Swift’s lazy keyword is supported at the language level, making the syntax extremely concise. C# requires declaring Lazy<T> and accessing it via .Value.
  • Behavior: C#’s Lazy<T> is Thread-safe by default, whereas Swift’s lazy is not Thread-safe. This is a significant behavioral pitfall; C# developers must be extra careful when using Swift lazy in concurrent scenarios.

Computed Properties
#

1. Core Concepts
#

  • Concept Explanation: These properties do not store values directly but provide a getter and an optional setter to indirectly access or calculate values of other properties.
  • Key Syntax: get, set, newValue (default parameter name for setter).

2. Example Analysis
#

Documentation Source Code:

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: newValue can be used 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 the parameter name is not specified in the setter, newValue is available by default; if the getter has only one line of code, return can be omitted.

3. C# Developer Perspective
#

Concept Mapping:

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

C# Comparison Code:

struct Rect {
    public Point Origin;
    public Size Size;

    public Point Center {
        get {
            // C# can also use expression-bodied members
            return new Point(Origin.X + (Size.Width / 2), Origin.Y + (Size.Height / 2));
        }
        set {
            // C# setter implicit parameter name is fixed as value
            Origin.X = value.X - (Size.Width / 2);
            Origin.Y = value.Y - (Size.Height / 2);
        }
    }
}

Key Difference Analysis:

  • Syntax: Very similar. 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; }. Swift allows omitting the get { } wrapper entirely if there is only a getter, simply writing the content within 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 to update UI or modify other variables in tandem.
  • Key Syntax: willSet (before setting), didSet (after setting).
  • Official Note:

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

2. Example Analysis
#

Documentation Source Code:

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 completes, didSet is executed; at this point, the property holds the new value, and the old value is accessible via oldValue (default name).

3. C# Developer Perspective
#

Concept Mapping:

  • C# has no direct syntax sugar for this. To achieve the same effect, you must expand an Auto-implemented Property into a full property with a _backingField and write the logic manually in the setter.

C# Comparison Code:

class StepCounter {
    private int _totalSteps = 0;
    public int TotalSteps {
        get { return _totalSteps; }
        set {
            // willSet logic
            Console.WriteLine($"About to set totalSteps to {value}");
            
            int oldValue = _totalSteps;
            _totalSteps = value;
            
            // didSet logic
            if (_totalSteps > oldValue) {
                Console.WriteLine($"Added {_totalSteps - oldValue} steps");
            }
        }
    }
}

Key Difference Analysis:

  • Syntax: Swift’s willSet/didSet is very elegant and does not require manual management of a backing field. C# is more verbose, which is why C# developers often rely on INotifyPropertyChanged or AOP frameworks to handle such requirements.
  • Behavior: Swift’s observers are triggered 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., numerical limits, UserDefaults storage, Thread-safety locks). Through the @WrapperName syntax, this logic can be reused.
  • Key Syntax: @propertyWrapper, wrappedValue, projectedValue ($).

2. Example Analysis
#

Documentation Source Code:

@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) 
// Prints "12", because it is capped by the wrapper

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

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"

3. C# Developer Perspective
#

Concept Mapping:

  • There is no direct equivalent syntax 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, whereas Swift Property Wrappers are active, directly intervening in the property’s getter/setter logic.
  • Logically, this is more like AOP (Aspect-Oriented Programming) or syntax sugar for the Decorator Pattern.

Key Difference Analysis:

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

Type Properties
#

1. Core Concepts
#

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

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

2. Example Analysis
#

Documentation Source Code:

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

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

3. C# Developer Perspective
#

Concept Mapping:

  • Corresponds to C# static members.

C# Comparison Code:

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 Difference 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 static polymorphism capabilities that are more flexible than C#.
  • Thread Safety: C# static field initialization usually relies on the Static Constructor to guarantee execution order, but access is not necessarily Thread-safe (manual locking is required). Swift documentation explicitly guarantees that the initialization of static stored properties is Thread-safe.