On this page

9. Classes, data and members

A class is an ordinary CLR reference type with identity and potentially mutable state. A data type is a record class with construction-only stored members and value equality. A class kind reuses a class header at compile time without introducing an intermediate runtime type.

9.1 Classes, visibility and stored members

CS-CLASS-001. A top-level class with omitted accessibility is internal. Class members and actions with omitted accessibility are public. Ordinary stored-member shape follows effective visibility: exactly private defaults to a field; public, protected and internal default to properties. Explicit field or property selects the shape where legal. An ordinary inferred public property is readable and writable.

public class Customer
    Name as String = ""
    Email as String = ""
    CreditLimit as Decimal with private set = 0
    private approvalCount as Integer = 0

    action ApproveLimit(amount as Decimal)
        require amount >= 0
        CreditLimit = amount
        approvalCount += 1
    end action
end class

CS-CLASS-004. Common source accessibilities are public, private, protected and internal. Composite C# accessibilities are not authored DirectSharp modifiers; imported members with such metadata retain their real effective access. Static changes association with the type rather than an instance and does not change field/property inference. self denotes the current instance; instance state is unavailable where no instance receiver exists.

CS-CLASS-005. An ordinary mutable member without an initializer receives its normal DirectSharp type default. A readonly member must be deliberately assigned exactly once on every valid construction path, through a declaration initializer or construction-time assignment. The implicit type default does not satisfy that obligation. Readonly protects replacement of a stored value, not mutation of an object referenced by that value.

A restricted setter such as with private set remains usable throughout the object's lifetime by code with access. It is weaker than readonly, which forbids later assignment even within the declaring class.

9.2 Properties and backing storage

CS-CLASS-006. Custom accessors require property ... end property. value is the incoming setter value; field is the current property's generated backing storage. field_of(Name) accesses a property's backing storage elsewhere in its declaring class and intentionally bypasses accessors. It is invalid for a computed property without backing storage and grants no external access or exemption from readonly assignment rules.

public class CustomerName
    property Name as String
        get
            return field
        end get
        set
            field = value.Trim()
        end set
    end property

    property DisplayName as String
        get
            return Name.ToUpperInvariant()
        end get
    end property
end class

A getter-only computed property is valid. A setter-only property is not supported; use an action for write-only behavior. Direct recursive accessor definitions must be diagnosed rather than generating an accessor that immediately calls itself forever. An explicit backing-storage operation is different from calling the getter/setter.

Optional plural groups such as public properties ... end properties and private readonly fields ... end fields apply their modifiers to each declaration. Groups do not nest. Contradictory modifiers and modifiers inapplicable to the declaration kind are errors. Every grouped declaration can also be written independently.

Empty parentheses on a property read

CS-CLASS-011. A readable non-indexed property accepts an empty-parentheses read, such as customer.Name(), as a fallback interpretation of customer.Name. The same applies to static properties. Ordinary applicable method/action invocation takes precedence; only when no such call is selected may the empty parentheses denote a property read. Ordinary callable/delegate-value invocation retains precedence where that syntax unambiguously invokes the returned value. A selected property read evaluates its getter once and retains its exceptions, side effects and CS-ACCESS-001–002 null propagation. Nonempty parentheses do not create a parameterized property; indexer declarations instead use the square-bracket form defined below. Assignments retain ordinary property syntax; customer.Name() = value is not another setter spelling. This tolerance introduces no double-parentheses invocation rule.

The following action fragment assumes a Customer with a readable String Name property and no applicable Name call:

let plain = customer.Name
let tolerant = customer.Name()

Each line is a separate read and executes any reached getter once. It need not observe the same value if that getter or another actor changes state. The formatter can prefer the ordinary property spelling. If a member instead resolves to an imported method, the method's contract applies; adding empty parentheses is not a way to force a First/Last helper read over a selected native LINQ call.

Parameterized properties: indexers

CS-CLASS-012. An indexer is a parameterized property, declared with property [parameters] as Type. Its one or more typed parameters are enclosed in square brackets; as Type specifies the property value type. Accessors use get ... end get and, when writable, set ... end set. The declaration closes with end property. There is no indexer keyword and no action-based indexer declaration.

class CustomerCollection
    virtual property [index as integer] as Customer
        get
            return customers[index]
        end get

        set
            customers[index] = value
        end set
    end property
end class

Customer is an application type, and customers is the collection's application-supplied instance storage. The getter produces a Customer; the setter's contextual value is the incoming Customer. The index parameters are available to the corresponding accessor bodies. The storage and its population policy are separate from the indexer declaration.

let customer = collection[0]
collection[0] = new Customer()

CS-CLASS-013. A getter-only indexer is read-only through that contract. Providing get and set makes it read/write, subject to their accessibility. Ordinary property accessibility and accessor rules apply; the design does not add setter-only properties. Indexers may have multiple parameters and may be overloaded by parameter signature. Parameter names or a different result type alone do not establish a distinct parameter signature. Access must select an accessible, applicable, unambiguous indexer.

For example, this read-only two-parameter declaration assumes customersByRegion is application storage indexed first by region and then by position:

property [region as string, index as integer] as Customer
    get
        return customersByRegion[region][index]
    end get
end property

Its access spelling is directory[region, index]. Separate overloads such as [index as integer] and [customerId as Global.System.Guid] can return the same Customer type while selecting different parameter signatures. Reads require a getter; writes require an accessible setter with a compatible incoming value. Indexing retains the existing strict receiver/index-access rules; it does not acquire ordinary non-indexed property's absence-propagating read behavior. Accessor exceptions propagate.

CS-CLASS-014. Accessible indexers are inherited. Indexer declarations support virtual, override and abstract with ordinary CLR property accessibility, signature, accessor and overriding constraints. A virtual indexer can be replaced by a compatible override; an abstract indexer requires an implementation in a concrete descendant. An indexer override replaces the corresponding inherited contract, rather than introducing an action or choosing dispatch from the reference's static type alone.

class CustomerCollection
    virtual property [index as integer] as Customer
        get
            return customers[index]
        end get
    end property
end class

class CachedCustomerCollection
    inherits CustomerCollection

    override property [index as integer] as Customer
        get
            return cache[index]
        end get
    end property
end class

Here customers and cache are application-supplied instance storage. With let collection as CustomerCollection = new CachedCustomerCollection(), a read of collection[0] invokes the overridden getter on CachedCustomerCollection. The static CustomerCollection reference still determines the accessible contract; the runtime instance determines virtual dispatch. These indexer modifier spellings do not replace the existing action declaration syntax in chapter 10.

CS-CLASS-015. Interfaces may declare indexer contracts, including their parameter types, value type and required readable/writable surface. A compatible implementation is accessible through the interface using the same bracket syntax, and must satisfy the interface and ordinary property contracts. Existing explicit interface implementation intent remains applicable; declaring an indexer does not create a new implicit interface mapping policy.

Indexer declarations compile to standard .NET indexer properties and get/set accessors. C# can consume exported indexers, and DirectSharp can consume ordinary compatible C# indexers. The source form remains a property, including for interface contracts and polymorphic dispatch.

Representative C# for the read/write declaration above:

class CustomerCollection
{
    public virtual Customer this[int index]
    {
        get { return customers[index]; }
        set { customers[index] = value; }
    }
}

The illustrative backing storage is supplied by the application. A read-only declaration omits the setter; a derived declaration uses a compatible C# override, and multiple parameters remain in the native indexer parameter list. No invented validation action or wrapper is part of the representation.

Reject missing/invalid parameter or value types, duplicate parameter signatures, ambiguous or inaccessible access, invalid accessors, assignment through a read-only contract, incompatible setter values, and invalid overrides or unfulfilled abstract/interface obligations. Diagnostics must identify the source declaration or access and the conflicting contract when applicable. Abstract/interface accessor-contract spelling and the exact indexer interface mapping form require reconciliation with the existing declaration grammar before implementation; the behavioral contracts above do not supply additional shorthand syntax.

9.3 Constructors and initialization order

CS-CLASS-002. An explicit constructor uses constructor(parameters) ... end constructor, with public accessibility by default. Ordinary parameters do not automatically become members. Declaring any constructor suppresses the implicit parameterless constructor. If no constructor is declared, an implicit public parameterless constructor is supplied when readonly requirements are satisfied by declaration initialization and the base-construction requirements permit it. Constructors are not inherited.

CS-CLASS-003. Omitting calls base() invokes an accessible parameterless base constructor. If none exists, explicit base delegation is required. Within one type, declaration initializers run in source order once per object; an initializer may read an earlier initialized member but cannot read a later uninitialized member. Failure in an initializer, delegated/base constructor, generated assignment or body propagates and returns no usable object. No automatic cleanup action repairs a half-constructed instance.

CS-CLASS-007. calls base(arguments...) and calls constructor(arguments...) are constructor delegation. At most one target is permitted. Delegation precedes constructor-local declarations and executable body statements. Same-class chains are permitted, but cycles are errors. Their terminal constructor performs the base-construction path; declaration initializers are not rerun for each link. Delegating bodies continue after the target returns.

For a terminal constructor, the conceptual order is object storage, current-type member initializers, base construction, generated promoted-property assignments, then ordinary body statements. Each base construction follows the corresponding sequence recursively. This order does not make partially initialized derived state safe to observe. Calling an overridable instance member during construction warrants a warning about not-yet-initialized derived state.

public class Customer
    readonly Id as Guid
    Name as String = ""

    constructor(id as Guid, name as String)
        self.Id = arguments.id
        self.Name = arguments.name
    end constructor
end class

Case-insensitive naming can make a bare member/parameter name ambiguous. self.Name explicitly selects the member; arguments.name explicitly selects the parameter. The compiler must not resolve the ambiguity using identifier case or declaration order.

9.4 Promoted constructor inputs and named construction

CS-CLASS-008. In a class with exactly one declared constructor, a constructor-local with properties ... end properties block declares members, adds corresponding effective constructor parameters and assigns them before ordinary body statements. Ordinary parameters precede promoted parameters, which retain declaration order. Required parameters must precede optional ones. Duplicate effective names, including case-insensitive ordinary/promoted collisions, are errors.

public class Customer
    constructor()
        with properties
            readonly Id as Guid
            Name as String = ""
        end properties
    end constructor
end class

let customer = new Customer() with
    id: Guid.NewGuid()
    name: "Ana"
end with

The named block is part of overload selection and input evaluation before construction. id: and name: are inputs; they are not setter calls performed after creation.

CS-CLASS-009. Constructor-local with properties, with fields and with needs are declaration-placement conveniences contributing to the final class. They are forbidden when the class declares more than one constructor, including delegating overloads. Values introduced by promoted-property assignment cannot be read by earlier delegation, even through arguments qualification. A readonly promoted member cannot be assigned again after its generated assignment. Use an ordinary parameter and explicit member assignment when transformation is required.

CS-CLASS-010. new respects type abstractness, constructor accessibility, complete effective signatures and normal overload compatibility. Unknown/repeated names, missing required inputs and ambiguous constructors are errors. as new Type(arguments) combines an explicit declared type with immediate construction. Target-typed new() and source-level static/async constructors are not supported. Plain return may complete a constructor early only after its definite-assignment obligations are satisfied; a constructor cannot return a value. Async creation uses an ordinary explicit factory action.

For post-construction operations, use ordinary assignment or a receiver scope:

let customer = use new Customer()
    .Name = "Ana"
    .Email = "ana@example.com"
end use

This fragment assumes the earlier mutable Customer declaration with an implicit parameterless constructor. It modifies the newly created object through the same operation rules as any existing receiver.

9.5 Data records

CS-DATA-001. data ... end data declares a reference-type record class. Stored members expose getters and may be initialized only during construction. A declaration without an initializer is required during construction; a declaration with an initializer is optional to supply. Missing required inputs are errors rather than requests to fill them with CLR defaults. Ordinary actions, attributes, interfaces, generic parameters and abstract records are supported.

public data PaymentRequest
    Reference as String
    Amount as Decimal
    Currency as String = "EUR"
end data

let request = new PaymentRequest("INV-1042", 150)

Currency uses its initializer when omitted. Explicit nothing is an actual absence and does not mean “use the initializer.” data does not automatically construct a meaningful default record. An ordinary mutable class is appropriate for state that must be changed in place after construction.

CS-DATA-002. Record equality requires compatible concrete runtime record types and includes the selected stored state and inherited record state. String members use OrdinalIgnoreCase equality and corresponding hashes. Other members retain their ordinary applicable .NET equality contracts. A List or array member does not acquire recursive element equality merely because it belongs to a record. equals by may select the participating members; compare by is a separate optional ordering declaration. Equal values must produce compatible hashes.

CS-DATA-003. A data record may inherit another data record or compatible imported record class, including an abstract record, but not an ordinary non-record class. Derived construction must satisfy inherited required state as well as new required state. Abstract records cannot be constructed; concrete descendants must implement inherited abstract contracts.

CS-DATA-004. A generic stored member Value as T = nothing retains actual absence for value-type arguments. The generated closed generic representation may lift a value argument to Nullable, while each property's requiredness remains independent. In Start as T and End as T = nothing, Start remains required and cannot use absence to bypass its declaration even when both share a lifted argument. External generic .NET types are not automatically rewritten by this DirectSharp record policy.

CS-DATA-005. Serializer construction must distinguish omitted members from explicit null. A missing required property and null for a required reference property are errors. Missing optional properties use their initializer. Explicit null for an optional reference property remains null, including when its omitted initializer would be a nonempty String. Null for a non-nullable numeric member is an error. Ordinary serializer naming policy, unknown-member policy and attributes remain available. Record inheritance does not imply a discriminator or automatic polymorphic serialization.

The reference C# representation of the payment contract begins with a record and a constructor enforcing its required input:

public record PaymentRequest
{
    public string Reference { get; init; }
    public decimal Amount { get; init; }
    public string? Currency { get; init; }

    public PaymentRequest(string reference, decimal amount, string? currency = "EUR")
    {
        ArgumentNullException.ThrowIfNull(reference);
        Reference = reference;
        Amount = amount;
        Currency = currency;
    }
}

This representation illustrates construction and init-only storage. Full conformance also requires the DirectSharp String equality/hash policy and serializer enforcement; unmodified C# record-generated equality alone is insufficient.

9.6 Shallow copying

CS-COPY-001. copy_of source creates a distinct top-level instance and leaves the original unchanged. Copying preserves the supported source's concrete runtime type, inherited state and private state without rerunning its constructor or declaration initializers. It must not slice a derived object into a base object or regenerate a domain identifier. Referenced nested objects and mutable collections remain shared under shallow copying.

let snapshot = copy_of customer

Copying and equality are independent: two distinct copied record instances may compare equal; an ordinary class retains identity equality unless it declares another equality contract. Unsupported imported types must be diagnosed rather than using an unrelated clone method or serializer round trip. Deep copying is a library policy, not a v1.0 core-language primitive.

CS-COPY-002. Changes after copying use ordinary receiver-scoped operations on the new instance. copy_of source with ... end with is not a copy-override construct. with retains its named-input meaning for constructs that support it; it does not bypass readonly or data construction-only assignment restrictions. Evaluate the copy source once, create the shallow copy, then run the use body on that copy.

let revised = use copy_of customer
    .Name = "Ana"
end use

This fragment assumes a supported Customer class with a writable Name property. On successful completion, revised is the new instance and the original customer's Name is unchanged. Referenced nested objects remain shared: mutating a nested shared object through the copy can also affect what the original observes. An inaccessible/readonly/init-only assignment remains an error. Exceptions from copying or later body operations propagate; no transactional rollback is added.

9.7 Class kinds

CS-KIND-001. A class kind is a compile-time class-header abstraction. It can contribute one optional exposed interface, one optional base class, implemented interfaces, attributes, a default concrete-class scope and dependency needs. It cannot contain ordinary actions, properties, fields, constructors or application implementation bodies.

CS-KIND-002. Kinds use ordinary namespace/import/project-reference lookup. Ambiguous unqualified names are errors. Generic definitions and applications use <of ...>. The resolved declaration, not the short name's spelling alone, determines the kind.

public class kind Validator <of T>
    exposes Global.FluentValidation.IValidator <of T>
    inherits Global.FluentValidation.AbstractValidator <of T>
    default scope internal
end class kind

class CustomerValidator is Validator <of Customer>
    constructor()
        RuleFor(.Name).NotEmpty().Length(2, 100)
        RuleFor(.Email).NotEmpty().EmailAddress()
    end constructor
end class

This example assumes a referenced FluentValidation assembly and a Customer with Name/Email String properties. The library methods are ordinary .NET APIs, not language validation keywords.

CS-KIND-003. Applying a kind substitutes its generic arguments into the concrete class's header. Expansion adds no intermediate CLR base type or runtime kind object. Normal base-class, interface, attribute, accessibility and constraint validity still applies. Incompatible explicit header information is an error; neither header silently overrides the other.

Kind-contributed attributes and attributes directly authored on the concrete class are both retained. Identical occurrences are not deduplicated. Normal CLR attribute applicability and AttributeUsage.AllowMultiple validation apply to the expanded class: a non-repeatable attribute appearing in both places is an error, while valid repeatable attributes retain both occurrences. Diagnostics identify the contributing kind and concrete-class locations.

CS-KIND-004. exposes names exactly one interface. A concrete applying class must be assignable to it, directly or through inheritance. In a type position, the kind denotes that exposed interface with generic substitution. A kind without exposes is legal in a class declaration but illegal in any type position. The compiler must not infer exposure from inherits/implements or synthesize a CLR kind type.

CS-KIND-005. A top-level kind is internal by default. Its own visibility is independent of concrete-class scope. Explicit class visibility takes precedence; otherwise its kind's default scope applies, or internal when none is declared. Public kind accessibility alone does not make concrete classes public.

CS-KIND-006. Contributed needs join the concrete class's declared and inherited DirectSharp dependency contract. Effective names are case-insensitively unique across the hierarchy, including private base needs. Dependencies are borrowed, and a kind supplies no automatic registration or lifetime policy. Provider and construction rules are defined in chapter 20.

CS-KIND-007. Diagnostics from kind contributions retain both the contributing declaration and applying class locations. A kind supplies no hidden behavior, validation engine or automatic service registration.

The corresponding validator is one ordinary C# class:

internal class CustomerValidator : FluentValidation.AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(customer => customer.Name).NotEmpty().Length(2, 100);
        RuleFor(customer => customer.Email).NotEmpty().EmailAddress();
    }
}

The exposed interface is already implemented through AbstractValidator. No runtime Validator class is emitted for the kind.

9.8 Required diagnostics

Reject invalid access/modifier combinations; readonly construction gaps or repeated assignments; forward initializer reads; nonexistent property backing storage; setter-only properties; direct accessor recursion; ambiguous member/parameter names; inaccessible or ambiguous constructors; delegation cycles; reads of promoted state during delegation; constructor-local declaration blocks with multiple constructors; invalid abstract construction; incomplete data input; unsupported reliable shallow copy; kind implementation bodies; multiple/noninterface exposes; a kind without exposure in a type position; conflicting contributed bases; and incompatible exposed contracts.


Specification contents · Previous: 8. Actions and callables · Next: 10. Inheritance and interfaces