On this page

Values, variables and types

A value has a type; a local variable has a name and a current value. DirectSharp checks these relationships before the program runs. In an order application, a customer identifier, a quantity and an amount may all be numbers, but they serve different purposes. Choosing types deliberately makes those purposes easier to follow.

let / default: declare and initialize values

public class InvoiceMath
    public static action Calculate(
        quantity as Integer,
        unitPrice as Decimal,
        discountRate as Decimal
    )
        returns Decimal
        require quantity > 0
        require unitPrice >= 0
        require discountRate >= 0 and discountRate <= 1

        let subtotal = quantity * unitPrice
        let discount = subtotal * discountRate
        let total = subtotal - discount

        return total
    end action
end class

Every let supplies an initializer. The compiler infers the type of subtotal, discount and total from their operands. It does not leave a newly declared local waiting for an assignment on some later path.

Integer is useful for counts. Decimal is useful for decimal financial calculations. An integer parameter does not become a Decimal simply because a particular call happens to supply a small number. Ordinary type and conversion rules still apply.

Use an explicit type when inference would choose a different one or when the type communicates a policy:

public class ExchangeCalculation
    public static action ConvertToLocal(amount as Decimal)
        returns Decimal
        let exchangeRate as Decimal = 61.5
        return amount * exchangeRate
    end action
end class

The name ConvertToLocal is application code, not a language conversion operation. The fixed rate keeps this example focused on types; real currency conversion needs a rate source, effective date and rounding policy.

readonly / assignment: control what can change

Locals are mutable unless declared readonly. Reassignment keeps the variable's type:

public class RunningTotal
    public static action Sum(
        amounts as Global.System.Collections.Generic.IEnumerable <of Decimal>
    )
        returns Decimal
        require amounts is not nothing

        let total as Decimal = 0
        for_each amount in amounts
            total += amount
        end for_each
        return total
    end action
end class

The loop visits the supplied sequence without removing its elements. total += amount updates the running total. The iteration binding itself is readonly: reassigning amount is not how you replace an element in the source collection.

A readonly local prevents rebinding, rather than freezing everything reachable through it:

public class PendingCustomers
    public static action Create()
        let readonly names = new Global.System.Collections.Generic.List <of String>()
        names.Add("Ana")
        names.Add("Boris")

        Global.System.Console.WriteLine(names.Count)
    end action
end class

names keeps pointing to this List, while the List's own API can still add items. This distinction matters when sharing collections between components.

lazy: compute a value when first needed

Declare let names = lazy BuildNames(customers) when an index should be built only if the action actually needs it. BuildNames is an ordinary application action returning a dictionary. The first normal use of names evaluates the complete initializer; subsequent uses share its cached value. There is no source .Value access and no automatic background start.

The binding is assign-once. A mutable captured variable is read when initialization runs, and an initialization failure is cached rather than retried. Once-only initialization does not freeze the returned object. The optional report example shows the complete class and explains input/resource lifetimes.

nothing / ??: handle absent values

nothing means absence. default asks for the effective default of a known type. A String default is an empty string, while an ordinary class reference commonly defaults to absence.

public class ContactLabels
    public static action Display(email as String)
        returns String
        if email is nothing then
            return "No address supplied"
        end if

        if email is whitespace then
            return "Address is blank"
        end if

        return email.Trim()
    end action
end class

An absent address and a blank address can require different feedback. Calling Trim only after checking absence makes the receiver's availability explicit.

Use ?? when a fallback represents the application's intent:

public class DeliveryNotes
    public static action Format(note as String)
        returns String
        let visibleNote = note ?? "No delivery instructions"
        return "Delivery note: {{visibleNote}}"
    end action
end class

The source expression is evaluated once, and the fallback is evaluated only if it is needed. Substituting a display message here does not mutate the caller's value.

Default means the type's effective default, which can itself be absence:

let customer = existingCustomer ?? default

For a Customer class with no visible custom default, a missing reference stays nothing. The expression is valid and preserves a present reference. It does not create a Customer. If the application needs an instance, supply an explicit new Customer construction or a valid custom default; ?? default follows the normal default-resolution rule.

Nullable value types use normal .NET Nullable <of T> types. DirectSharp arithmetic can substitute the effective default for a missing supported numeric operand without changing the stored value. When missing data has business meaning, express that meaning before calculating:

public class RecordedDiscounts
    public static action Apply(
        amount as Decimal,
        discount as Global.System.Nullable <of Decimal>
    )
        returns Decimal
        require amount >= 0
        if discount is nothing then
            return amount
        end if

        require discount.Value >= 0 and discount.Value <= amount
        return amount - discount.Value
    end action
end class

You can also let an absent initializer select a nullable local:

let recordedAmount as Decimal = nothing
recordedAmount = 125
recordedAmount = nothing

RecordedAmount is a real Nullable <of Decimal> throughout. A local initialized as nonnullable Decimal keeps that type and cannot later receive nothing. For parameters and results, write Nullable <of Decimal> explicitly when absence belongs in that contract. References such as String can already hold nothing.

An imported API receives the actual argument you pass. DirectSharp arithmetic does not turn every absent value into zero at a method call, in serialization or in a comparison.

Use ?? throw when absence must fail at a particular expression:

let customer = LoadCustomer(id) ?? throw
customer = nothing

The application action returns a Customer reference. The initializer checks its result immediately. That check adds no permanent rule to the local: the later assignment is allowed for this mutable reference type. If a later operation requires presence, check it there with require or another ?? throw expression. Ordinary type and readonly restrictions still apply.

Use let from to name several values from an object without repeating its source expression:

let from LoadCustomer(id)
    name = .Name
    email as String = .Email
end let

This fragment assumes an application LoadCustomer action returning a present Customer with Name and Email properties. The action is called once; both reads use that captured customer, with Name read before Email. Each extraction has an explicit local name, and an explicit type is optional. This is ordinary object access, not a copy or an ownership scope.

Let from requires a present source and fails before extraction if it is nothing. When a missing customer should produce absent values, choose conditional extraction:

let_if from LoadCustomer(id)
    name = .Name
    email as String = .Email
end let_if

Global.System.Console.WriteLine(name ?? "Customer was not found")

The application supplies String properties. A missing source assigns nothing to both locals; it does not substitute empty strings or evaluate either getter. A present source extracts normally. An exception thrown by LoadCustomer or an evaluated getter still propagates. The declarations remain available after end let_if.

Value-type results use native CLR nullable types automatically, including when their type is written explicitly. With Integer Age and Decimal CreditLimit properties:

let_if from LoadCustomer(id)
    age as Integer = .Age
    creditLimit = .CreditLimit
end let_if

if age is not nothing then
    Global.System.Console.WriteLine(age.Value)
end if

Age is Nullable <of Integer> and creditLimit is Nullable <of Decimal>. Their types stay nullable when a customer is found; they do not change with the runtime branch. If the customer is missing, both hold nothing rather than zero. Use a presence check, or choose an explicit fallback with ??, before calling an API that requires a non-nullable value.

operators: arithmetic, comparison and assignment

= compares values in a condition and assigns a value in an assignment statement. <> means unequal. and and or combine conditions with short-circuit evaluation. Put checks in the order needed to make subsequent access safe.

Ordinary / produces fractional division for integer operands: 3 / 2 has the value 1.5. Use div when you want an integral whole-number quotient and mod when you want the remainder. 3.5 div 2 is a Long with value 1: divide first, then convert the quotient toward zero. Result conversion follows normal CLR checked/unchecked behavior. Numeric compatibility and overflow still depend on the operand types and the selected operation.

Assignment chains such as first = second = Calculate() support simple writable locals and parameters only. The call runs once and assignments proceed from right to left. Use separate statements for fields, properties and indexed targets.

For an ordinary exchange, use swap(first, second). Both locations must be writable and have the same effective type. This avoids inventing a multiple-assignment form such as first, second = second, first. The helper works with ordinary reference and value types, not only native atomic numeric types. Explicit atomic swap(first, second) requests protected exchange; swap directly in a concurrent body uses that protection automatically. Consistent reads of shared pairs also need compatible coordination; see concurrency.

String comparisons written with language operators ignore case by default using ordinal comparison. Imported .NET methods retain their own contracts. For a protocol or identifier that requires exact casing, select an explicit .NET comparison:

public class ProtocolNames
    public static action IsExact(actual as String, expected as String)
        returns Boolean
        return Global.System.String.Equals(
            actual,
            expected,
            Global.System.StringComparison.Ordinal
        )
    end action
end class

For the full type-default and arithmetic rules, see types and operators.

Practice: values, defaults and absence

Predict absence, defaults and readonly mutation before reading the answer.

Previous: First application · Next: Conditions and loops