Silex v0.47.1 / Docs Canonical source ↗

Share an identity with a class

A class defines shared objects with automatic lifetimes. Passing, copying, or storing a class value preserves the identity of the same instance.

public class Player {
    private let name:str
    var health:int = 100

    init(name:str) {
        self.name = name
    }

    func damage(amount:int) {
        self.health -= amount
    }

    func description() str {
        return "$(self.name): $(self.health)"
    }
}

func main() {
    var player = Player("Ada")
    player.damage(10)
    print(player.description())
}

Use var for a direct class binding or a collection of classes, even when that binding never changes instance. A composed value, such as a structure that keeps a class in private storage, may instead remain in a let: the outer value stays immutable while the inner identity remains shared.

Forbid deep copies of an identity

Add nocopy when a class wraps a native or external resource that cannot be cloned safely:

public nocopy class DeviceHandle {
    private let native:uint
}

Passing, returning, or ordinarily assigning this class continues to share its identity and automatic lifetime. However, copy is rejected for the class and for any structure, enum, optional, or collection that can reach it. See copy or transfer a value to distinguish sharing, deep copying, and transfer.

Initialize a field at its declaration

The value after = may be an ordinary expression: a function call, construction, calculation, or cascade. Silex evaluates it in field order for each new instance, before the constructor body. Two instances therefore do not accidentally share a collection created by the same expression.

func initial_values() int[] { return [1] }

class Recorder {
    var values:int[] = initial_values()
}

func main() {
    var first = Recorder()
    var second = Recorder()
    first.values.append(2)

    print(first.values.count())  // 2
    print(second.values.count()) // 1
}

Use this form when the value depends on neither self nor a constructor argument: those names are not available in the expression. It can also prepare an object through a cascade without requiring a temporary optional.

Construct an instance

Without a custom constructor, initialize visible fields by name:

class Position {
    var x:int
    var y:int
}

func main() {
    var position = Position(x:2, y:3)
    var target = Position()
        ..x = position.x
        ..y = position.y

    position.x = 4

    print(position.x)
    print(target.x)
    print(target.y)
}

Here, the cascade configures a new instance from position. The program prints 4, 2, then 3: modifying position afterward does not modify target, because every call to Position() creates a distinct identity.

Declare init when construction must establish an invariant. As soon as one constructor exists, the automatic named-field initializer disappears. Every let field without a declared value and every var field whose type provides neither an intrinsic value nor construction without arguments then remains pending. The constructor must assign them on every normal path. An initialized field may be read; self as a whole cannot be used until every field is initialized.

struct Range {
    let minimum:float
    let maximum:float

    init(minimum:float, maximum:float) {
        self.minimum = minimum
        self.maximum = maximum
    }
}

class Gauge {
    var limits:Range

    init(minimum:float, maximum:float) {
        self.limits = Range(minimum, maximum)
    }
}

Inside init, an assignment such as self.field = value initializes the target field; it is not premature use of the complete object. A field that is already initialized may then be read or mutated while other fields remain pending. Loops may therefore prepare storage whose size depends on an argument:

class PreparedList {
    var values:int[]
    let requested_count:int

    init(count:int) {
        self.values = []
        var index = 0
        while index < count {
            self.values.append(index)
            index++
        }
        self.requested_count = count
    }
}

Reading a pending field or passing self as a complete object remains invalid. An assignment that occurs only inside a loop which may never execute also cannot establish initialization on every path; the field must receive a value on the loop's exit path.

The three forms complement one another: place an expression on the field for a value owned by each instance, use init when the value depends on its arguments, and retain the automatic named-field initializer when callers should freely provide the configuration. Declare T? only when absence is a real part of the model; it is not an intermediate state required by construction.

Add methods

Methods receive self implicitly and accept positional arguments, named arguments, and default values like ordinary functions. Silex infers whether a method modifies the instance; such a call requires var access to the shared identity.

Fields, constructors, and methods inherit class visibility. A modifier can restrict it without ever exceeding the type's visibility. Exact scopes are grouped in declaration visibility.

Compare identities

== compares the shared identity of two class values, not their fields. Duplication and borrowing rules are detailed in copy or transfer a value and borrow a value.

Source code never manipulates an object's address, reference count, allocation, or manual release. Inheritance, static members, generic specializations, and cleanup each have a dedicated contract.

Back to data types · Specialize a class through inheritance