Objects and construction
Ordinary classes, interfaces, constructors and members are the foundation of application code. A DirectSharp class has ordinary CLR identity and can inherit an existing .NET class or implement an existing interface.
Stored members and constructors
public class Invoice
public Number as String = ""
public Total as Decimal = 0
public constructor(number as String, total as Decimal)
require number is not nothing
require number is not empty
require total >= 0
self.Number = arguments.number
self.Total = arguments.total
end constructor
end class
Public stored members infer CLR properties. Private stored members infer fields; protected and internal stored members infer properties unless explicitly declared as fields. Visibility does not merely annotate the same backend field shape. Explicit property/field intent takes precedence where supported by the declaration rules.
Reference C# for the sample:
public class Invoice
{
public string Number { get; set; } = "";
public decimal Total { get; set; } = 0m;
public Invoice(string number, decimal total)
{
if (number is null)
throw new global::System.ArgumentNullException(nameof(number), "Invoice number is required: number is not nothing");
if (number.Length == 0)
throw new global::System.ArgumentException("Invoice number is required: number is not empty", nameof(number));
if (total < 0)
throw new global::System.ArgumentException("Total must not be negative: total >= 0", nameof(total));
Number = number;
Total = total;
}
}
constructor ... end constructor is the construction declaration. A class with no explicit constructors uses its implicit-construction rules; an explicit empty constructor is still a real declaration and suppresses the implicit constructor.
Constructor arguments use ordinary call syntax:
let invoice = new Invoice(number: "INV-101", total: 125.00)
new ... with groups named construction inputs and needs bindings. It is not arbitrary executable post-construction configuration. Use an ordinary receiver scope when later operations are intended.
Read a property with empty parentheses
A readable non-indexed property tolerates empty parentheses when no applicable ordinary method/action call takes precedence. Thus customer.Name and customer.Name() can denote the same property read; the tolerance applies to static properties too. Each reached getter runs once and its failures propagate; missing read receivers still produce nothing.
This adds no parameterized property or setter spelling with parentheses. Callable-valued properties retain normal invocation semantics. The formatter can prefer customer.Name, and a selected native method retains its own contract. See CS-CLASS-011.
Inheritance and interface intent
An intended overridable base action uses overridable. A derived member declares its relationship explicitly with overrides base.Member. Calling the base implementation is a separate ordinary call.
class InvoicePrinter
public overridable action Print(invoice as Invoice)
Global.System.Console.WriteLine(invoice.Number)
end action
end class
class DetailedInvoicePrinter
inherits InvoicePrinter
public action Print(invoice as Invoice)
overrides base.Print
call base.Print(invoice)
Global.System.Console.WriteLine(invoice.Total)
end action
end class
The source intent corresponds to C# virtual and override. An exact match to an overridable member without the required relationship is an error rather than silent hiding. Ordinary overloads need no extra overload keyword.
A class can declare an interface relationship in its header and use member-level mappings such as implements IContract.Member. The short member form implements IContract requires exactly one compatible interface member. Interface aliases can shorten repeated relationship clauses without changing the interface identity.
A named implements IContract ... end implements body block instead supplies intent for same-name compatible actions/properties inside it; individual markers are unnecessary for those members. The block also declares the class's interface relationship. Renamed members use an explicit mapping, including block-local implements Member. Supporting helpers are allowed, but missing, incompatible or ambiguous requirements remain errors. Members outside the block need their own mappings. A grouped class-header interface list is a declaration list rather than an implementation body block.
Class kinds
A class kind packages repeated class-header information: a base class, interfaces, attributes, default visibility and applicable needs. It expands into the concrete class rather than creating an intermediate runtime class.
public class kind Validator <of T>
exposes Global.FluentValidation.IValidator <of T>
inherits Global.FluentValidation.AbstractValidator <of T>
default scope internal
end class kind
public class InvoiceValidator is Validator <of Invoice>
public constructor()
RuleFor(.Number).NotEmpty()
end constructor
end class
This declaration fragment requires a FluentValidation package and its extension-method namespace import. The concrete validator inherits AbstractValidator of Invoice. The kind does not become a CLR Validator class or a factory.
exposes establishes the kind-as-type contract:
let validator as Validator <of Invoice> = new InvoiceValidator()
Reference C# for this local:
global::FluentValidation.IValidator<Invoice> validator = new InvoiceValidator();
A kind without exposes may organize a class declaration but cannot be used as a parameter/local type. Diagnose that use instead of choosing Object or a generated class. Conflicting base classes, inaccessible expanded contracts, unresolved generic arguments and incompatible needs are also errors.
Kind accessibility and concrete class accessibility are separate. Explicit concrete visibility wins; otherwise the kind's default scope applies. Top-level visibility without an explicit applicable setting is internal. A public kind therefore does not automatically make all classes using it public.
Attributes contributed by a kind and directly declared on the class are both retained, including identical occurrences. Normal CLR attribute validation applies after expansion. Duplicate non-repeatable attributes are errors; valid repeatable attributes keep both occurrences. The compiler does not silently deduplicate attributes.
Data, records and copying
data provides the language's value-oriented record construction model; class supplies ordinary object identity. Their shared member notation does not erase that distinction. The classes and data specification defines construction, generated members and restrictions.
Core copy_of is shallow copying. Referenced objects remain shared unless application/library logic copies them independently. deep_copy_of is not a core operation; recursive copying requires a library policy for cycles, identity, resources and shared services.
Change ordinary writable members with use copy_of source ... end use. Copying runs once and the use body operates on the new instance. copy_of source with ... end with is not an override block; readonly/data construction-only properties cannot be assigned through this receiver scope. Mutating a shared nested object can still affect both instances, and failures do not roll back completed side effects.
Disposal belongs to resource and ownership rules, not to copying. A copy must not silently acquire ownership of borrowed dependencies.
Customer validation reference lowering
This reduced model focuses on Name and Email; the full walkthrough also supplies Id and IsActive. These are reference alternatives, not duplicate Customer declarations to add to that application. The source and C# below describe corresponding semantics. The C# is a reference illustration rather than compiler output. The full DirectSharp sample supplies declarations, file import and invocation context. A C# build must reference a compatible FluentValidation package; the namespace import alone does not restore it.
Ordinary class and constructor
DirectSharp:
public class Customer
public Name as String = ""
public Email as String = ""
public constructor()
end constructor
end class
Representative C#:
public class Customer
{
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public Customer() { }
}
Public stored members infer properties; explicit empty construction suppresses the implicit constructor but supplies the same public parameterless shape here. See CS-CLASS-001–CS-CLASS-003. Do not lower these as public fields, invent a required-member marker or turn the class into a record.
Inherited calls and expression selectors
DirectSharp, in its own file with the library import:
import Global.FluentValidation
public class CustomerValidator
inherits Global.FluentValidation.AbstractValidator <of Customer>
public constructor()
RuleFor(.Name).NotEmpty().Length(2, 100)
RuleFor(.Email).NotEmpty().EmailAddress()
end constructor
end class
Representative C# in a file containing its own using directive:
using FluentValidation;
public class CustomerValidator : global::FluentValidation.AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Name).NotEmpty().Length(2, 100);
RuleFor(customer => customer.Email).NotEmpty().EmailAddress();
}
}
RuleFor receives expression-tree selectors under its library signature; the C# lambda syntax relies on that same target typing. NotEmpty, Length and EmailAddress use imported library extension methods. Names are library symbols, not reserved DirectSharp words. A DirectSharp binder must resolve their actual overloads; this illustration is not evidence that a particular compiler/package combination already does so. See calls and delegate binding.
Class-kind binding and diagnostics
The optional kind sample replaces the direct validator declaration. It contributes the same AbstractValidator<Customer> base and checks assignability to IValidator<Customer>. The inherited base already implements that interface. The C# validator above can therefore represent both variants without adding a runtime Validator<T> class or repeating an interface in the C# header.
A valid kind-as-type use, inside an action with the sample declarations visible:
let validator as Validator <of Customer> = new CustomerValidator()
Corresponding C# local:
global::FluentValidation.IValidator<Customer> validator = new CustomerValidator();
This is an interface contract, not a concrete factory. Use new CustomerValidator() to create the object. A kind name used as a type does not instantiate the library base or introduce a service lookup.
Deliberately invalid type use: the following declaration-only kind omits exposes. Its application to a class is valid; its use as an action parameter type is not.
class kind InternalModel
default scope internal
end class kind
class CustomerModel is InternalModel
end class
class InvalidConsumer
action Inspect(model as InternalModel)
end action
end class
CS-KIND-004 requires a compile-time error at the parameter's type, with the kind declaration available as related evidence. The compiler must not silently use CustomerModel, Object or a generated InternalModel CLR type. The specification diagnostic table covers conflicting bases, ambiguous lookup, generic/accessibility failures and needs collisions as well.
| Kind / concrete declaration choice | Expected concrete CLR visibility |
|---|---|
| Public kind, internal default scope, explicit public concrete class (the sample) | Public |
| Public kind, internal default scope, concrete visibility omitted | Internal |
| Kind and concrete visibility/default scope all omitted at top level | Internal; kind symbol also internal |
| Explicit public concrete class using a kind with no default scope | Public, subject to valid accessible expanded base/interface contracts |
CS-KIND-005 separates the kind symbol's accessibility from the concrete class's scope. The sample uses declarations in the same compilation; a kind does not introduce an intermediate CLR class.
Calling the sample
The complete source's CustomerValidationExample.Run action has this representative C# behavior, in the same compilation as the Customer and CustomerValidator above:
public class CustomerValidationExample
{
public static void Run()
{
var customer = new Customer();
customer.Name = "Ana";
customer.Email = "ana@example.com";
var validator = new CustomerValidator();
var validation = validator.Validate(customer);
if (validation.IsValid)
{
global::System.Console.WriteLine("Customer is valid");
}
else
{
foreach (var failure in validation.Errors)
{
global::System.Console.WriteLine(failure.ErrorMessage);
}
}
}
}
There is one construction of each object and one Validate call. The ordinary loop observes the library's returned failures; no try_get swallows unexpected exceptions. The method is invoked by a host and is not a new DirectSharp entry-point convention. Matching runtime results, visibility and CLR member shapes must be checked separately from comparing source text.
Property-based indexers
An indexer is a parameterized property, declared as property [index as integer] as Customer, with get/end get and optional set/end set accessors, closing with end property. The setter's value is the incoming property value. Use collection[index] for a read or assignment; multiple parameters use collection[first, second]. Getter-only contracts are read-only.
Indexers overload by parameter signature and inherit under ordinary accessibility rules. Virtual, override and abstract indexers use standard property polymorphism: a base-typed reference invokes the derived override when the runtime object supplies it. Interfaces can declare indexer contracts, and their implementations retain the established explicit interface-intent rules. These declarations become ordinary .NET indexer properties usable from C#; no indexer keyword or action-based declaration is involved.
See the canonical syntax, polymorphism example and C# representation. Parentheses on an ordinary property read do not create an indexer.