
Learning Swift from a C# Perspective
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 TypeLogic 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 Propertycorresponds to C#’s Field or Auto-implemented Property. - Swift’s
letcorresponds to the C#readonlykeyword.
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 modificationKey Difference Analysis:
- Syntax: Swift’s distinction using
letandvarkeywords is very intuitive. C# requires thereadonlymodifier. - 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
lazyso 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 aslazy. Note: If alazyproperty 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 executedLogic 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
lazykeyword is supported at the language level, making the syntax extremely concise. C# requires declaringLazy<T>and accessing it via.Value. - Behavior: C#’s
Lazy<T>is Thread-safe by default, whereas Swift’slazyis not Thread-safe. This is a significant behavioral pitfall; C# developers must be extra careful when using Swiftlazyin concurrent scenarios.
Computed Properties #
1. Core Concepts #
- Concept Explanation: These properties do not store values directly but provide a
getterand an optionalsetterto 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/setblocks).
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
valueas the implicit parameter for the setter, while Swift usesnewValue(though Swift allows you to customize this name, e.g.,set(newCenter) { ... }). - Read-only Properties: C# uses
{ get; }. Swift allows omitting theget { }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:
willSetanddidSetobservers 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 stepsLogic 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
_backingFieldand 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/didSetis very elegant and does not require manual management of a backing field. C# is more verbose, which is why C# developers often rely onINotifyPropertyChangedor 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
@WrapperNamesyntax, 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 wrapperLogic 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$Stateto 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#,
staticmembers cannot be overridden via inheritance. However, in Swift, if you declare a computed type property using theclasskeyword within a Class, subclasses canoverrideit. This provides static polymorphism capabilities that are more flexible than C#. - Thread Safety: C#
staticfield 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 ofstaticstored properties is Thread-safe.