belte

4 Namespaces, Classes, and Objects

4.1 Classes

Classes are structures that contain data and functionality in the form of fields and methods. Fields are similar to variables, and methods are similar to functions both in syntax and functionality.

4.1.1 Declaring and Using Classes

Classes are declared using the class keyword:

class MyClass {
  // Members
}

An object is an instance of a class, and can be created using the new keyword:

new MyClass();

The containing instance can be accessed within a method using the this keyword:

class MyClass {
  int a;

  int GetA() {
    return this.a;
  }
}

When the target type of the object creation is clear from context, the type can be omitted. Alternatively, implicit typing can be used. The follow are all equivalent:

MyClass a = new MyClass();
MyClass a = new ();
var a = new MyClass();

Similar to namespaces, a class can be file scoped so that all members following the declaration are members of the class:

class MyClass;

int a;

int GetA() {
  return this.a
}

Unlike normal classes, members will default to being public instead of private if no accessibility modifier is specified. Additionally, members inside of a static file-scoped class will also be static even if the static modifier is not specified:

static class Program;

void Main(string[]! args) {
  // ...
}

In the above example, Main is public and static and is a valid entry point.

4.1.2 Inheritance

The extends keyword is used to specify that a class inherits from another, meaning it adopts all of that classes fields and methods. If a base type is not specified, classes will inherit directly from Object.

var myB = new B();
myB.M();

class A {
  public void M() { }
}

class B extends A { }

Members can interact with inheritance through certain modifiers.

Classes can restrict or necessitate inheritance through the sealed and abstract modifiers.

4.1.3 Base Access

In the case of a base type containing a member of the same name as a derived class, the more derived member will take precedence. To access the base member, the base keyword can be used similar to this.

class A {
  public virtual int M() {
    return 10;
  }
}

class B extends A {
  public override int M() {
    return 3;
  }

  public void F() {
    var num = M(); // num = 3
  }
}
class A {
  public virtual int M() {
    return 10;
  }
}

class B extends A {
  public override int M() {
    return 3;
  }

  public void F() {
    var num = base.M(); // num = 10
  }
}

4.2 Members

Members of an instance can be accessed externally using a member accession:

myInstance.member;

Members can also be accessed internally (i.e. inside of a method) using the this keyword:

this.member;

If no local symbol names conflict, the this keyword can be omitted:

member;

4.2.1 Fields

Fields are similar to locals. They are declared as such:

class MyClass {
  int myField1 = 0;
  string myField2 = "Starting Value";
  // etc.
}

4.2.1.1 Definite Assignment

Non-nullable fields without an initializer require definite constructor assignment:

class MyClass {
  int myField;

  public constructor(int p) {
    myField = p;
  }
}

This analysis assumes the constructors exit normally, meaning this is also okay even though there are paths where the field is not assigned:

class MyClass {
  int myField;

  public constructor(int p) {
    if (p < 0)
      throw new ArgumentException();

    myField = p;
  }
}

If a constructor calls a method that initializes fields, that method can be marked with an initializes clause to forward it’s definite assignments:

class MyClass {
  int myField;

  public constructor(int p) {
    if (p < 0)
      throw new ArgumentException();

    Init(p);
  }

  private void Init(int p) initializes(p) {
    myField = p;
  }
}

4.2.2 Methods

Methods are similar to functions and generally allow the same features. They are declared as such:

class MyClass {
  public void MyMethod(int param1) {
    // ...
  }
}

When accessing a method, it must be called:

var myInstance = new MyClass();
myInstance.MyMethod();

4.2.2.1 Overloading

As long as the signatures are different, it is valid to declare multiple methods with the same name (overloads). When calling a method with that name, the “best” overload is chosen.

class A {
  public static void MyMethod() { }

  public static void MyMethod(int param) { }
}

A.MyMethod(3); // This calls the second overload because it expects an argument, while the first overload does not.

The const modifier on a parameter does not affect overload resolution, meaning two methods with otherwise the same signature will conflict:

class A {
  public static void M(const List<int> list) { }

  public static void M(List<int> list) { } // Error
}

4.2.2.2 State and Reverse Clauses

A method with a reverse clause will automatically run that code when exiting a with expression or statement

For example:

var stack = new Stack<int>();

with (stack.Push(3)) {
  // ...
}

public class Stack<type T> {
  public void Push(T item) {
    // ...
  } reverse {
    Pop();
  }

  public T Pop() {
    // ...
  }
}

In the above example, the method Stack<T>.Push(T) defines a reverse clause that will run at the end of the above with statement.

The reverse clause can optionally accept a single parameter that automatically passes the return value of the target method to communicate data between the two.

For example:

class A {
  public int Func(int val) {
    return val;
  } reverse (p) {
    // ...
  }
}

var a = new A();

with (a.Func(10)) {
  // ...
}

In the above example, the reverse clause will be executed with p = 10. The reverse parameter can also be explicitly typed to any type that the return type of the target method can implicitly cast to:

class A {
  public int Func(int val) {
    return val;
  } reverse (decimal p) {
    // ...
  }
}

A state clause can be added to a method where when the data needed to reverse an operation does not naturally derive from the operation’s return value. State clauses can also be used to avoid polluting a method with a return value only used in reversal when the method would otherwise have a void return.

For example, a method that appends an element to a list and is reversible:

public class List<type T> {
  public void Append(T value) {
    if (_res > _length) {
      _array[_length] = value;
      _length++;
      return;
    }

    ExpandStorage();
    _array[_length] = value;
    _length++;
  } state (int) {
    return _length - 1;
  } reverse (index) {
    RemoveAt(index);
  }
}

When calling List<T>.Append() normally, it has a void return. But when used in a with statement or expression, the return value of the state clause will be stored and passed to the reverse clause when reversing:

List<int> myList = { 1, 2, 3 };

with (myList.Append(4)) {
  // myList = { 1, 2, 3, 4 }
  // ...
}

// myList = { 1, 2, 3}

The state clause can use any values of the target method (parameters and locals) as long as all code paths have assigned a value to those values.

4.2.3 Operators

4.2.3.1 Operator Overloading

Operators are similar to methods. They are declared as such:

class MyClass {
  public static MyClass operator+(MyClass left, MyClass right) {
    // ...
  }
}

Operator overloading is used to allow custom classes to use syntactical operators. The overloadable operators are:

Operators Notes
+x, -x, !x, ~x, ++, --, x[]  
x + y, x - y, x * y, x / y, x % y, x & y, x \| y, x ^ y, x << y, x >> y, x >>> y  
x == y, x != y, x < y, x > y, x <= y, x >= y Must be overloaded in the following pairs: == and !=, < and >, <= and >=

Note that operators must be marked public and static.

4.2.3.2 Casts

Explicit and implicit casts from or to the class type can be declared as such:

Implicit cast from A to int:

class MyClass {
  public static implicit operator int(A a) {
    // ...
  }
}

Explicit cast from int to A:

class MyClass {
  public static explicit operator A(int a) {
    // ...
  }
}

These casts are automatically applied when casting like normal:

A a = (A)3;
int a = new A();

4.2.3.3 User-Defined Literals

Similar to user-defined casts, custom literal suffixes can be defined:

struct Time {
  int milliseconds;

  constructor(int milliseconds) {
    this.milliseconds = milliseconds;
  }

  static Time literal ms(int milliseconds) {
    return new Time(milliseconds);
  }

  static Time literal s(int seconds) {
    return new Time(seconds * 1000);
  }
}

A custom literal conversion is a public static method that returns the containing type, uses the literal keyword, and whose name is the desired literal suffix. Literal methods must have exactly 1 parameter. The parameter type must be a type represented by a literal: this includes all numeric types, char, and string. uint8* and char* are also valid because of c-string literals. Suffixes are case-sensitive.

With the above struct definition, the following can be done:

Time myTime = 3s; // myTime.milliseconds = 3000
Time myOtherTime = 30ms; // myOtherTime.milliseconds = 30

Another example:

Sleep(500ms);

void Sleep(Time time) { /* ... */ }

Prefixed literals (such as interpolated strings and hexadecimal numerics) also support suffixes:

var myNum = 10;
A myA = f"num is {myNum}"a; // myA.str = "num is 10A"

class A {
  public string str = "";

  public static A literal a(string str) {
    return new A()..str = (str + "A");
  }
}

4.3 Modifiers

Fields can be marked const, final, and constexpr which shares the same meaning as when those modifiers are applied to locals, which can be read about here. Fields can also be references which can be read about here.

4.3.1 Accessibility Modifiers

Public indicates that the member can be seen everywhere, including outside the class. Protected indicates that the member can only be seen within the class and child classes. Private indicates that the member can only be seen within the class, not even in child classes.

class MyClass {
  private int a;
  protected int b;
  public int c;
}

A member can only have one accessibility modifier, but they do not require the modifier. By default, all struct members are public and all class members are private.

All types of members can be marked with all three accessibility modifiers except operators, which must always be public.

4.3.2 Overriding Modifiers

By default, members cannot be overridden. To allow a member to be overridden, it can be marked virtual. Virtual members still require a definition. To override a virtual member, the override can be marked override. To instead hide a member without overriding, a member can be marked new. A member cannot be marked as both override and new or virtual.

An overriding member can be marked sealed to prevent classes deriving from it to override the member again.

Similar to virtual, a member can be marked abstract. Abstract members must be overridden in all non-abstract child implementations, and as such abstract members do not have a definition when declared.

Currently, these modifiers only apply to methods.

4.3.3 Static and ConstExpr

Class members are instance members by default, meaning they require an instance to access. With the static and constexpr keywords methods and fields respectively can be accessed without an instance.

class MyClass {
  public constexpr int a = 3;

  public static void B() { }
}

var myA = MyClass.a;
MyClass.B();

Classes themselves can also be marked as static, meaning that all contained members must also be static or constant expressions.

Static fields can be accessed without an instance and refer to a global singleton of the containing class. A private static constructor can be defined for a class that will run the first time a static field is accessed.

4.3.4 Const

Methods marked as const cannot modify instance data or call instance methods not marked const. A const local of a class type can only read fields and call const methods.

4.3.5 Sealed and Abstract

Classes can be marked as sealed to indicate that they cannot be derived.

sealed class A { }

Classes can be marked as abstract to indicate that they must be derived.

abstract class A { }

4.4 Constructors and Finalizers

When creating an object, values can be passed to modify the creation process. By default, no values are passed:

class MyClass { }

new MyClass();

But with a constructor data can be allowed to be passed when creating the object. Constructors are declared similar to methods, but do not have a return type and they use the constructor keyword in place of an identifier.

class MyClass {
  int myField;

  public constructor(int a) {
    this.myField = a;
  }
}

new MyClass(4);

Finalizers run when the garbage collector cleans up the memory used by an object. One can be defined per class to run some code right before the garbage collector does this:

class MyClass {
  finalizer() {
    // Cleanup ...
  }
}

4.5 Templates

Classes can be declared as templates and take in template arguments that change how the class operates at runtime. Template arguments can either be compile-time constants or types (similar to C++ templates and C# generics).

To demonstrate this concept, a simplified definition for a List type is shown:

class List<type t> {
  t[] array;
  int length;

  public constructor(t[] array) {
    this.array = array;
    length = Length(array);
  }

  public static ref t operator[](List<t> list, int index) {
    return ref list.array[index];
  }
}

This List template can be used to create List objects of different types:

var myList = new List<int>(
  { 1, 2, 3 }
);

var myList = new List<string>(
  { "Hello", "world!" }
);

var myList = new List<bool[]>(
  {
    { true, false },
    { false },
    { true, true, false }
  }
);

These types are not limited to primitives:

var myList = new List<List<int>>({
  new List<int>({ 1, 2, 3 }),
  new List<int>({ 3 }),
  new List<int>({ 3, 3, 2, 45 })
});

4.5.1 Constraint Clauses

Templates can be constrained at compile-time to ensure intended functionality. These constraints are defined within a single where block in the class header.

4.5.1.1 Expression Constraints

These expressions are enforced at compile-time, and as such they must be computable at compile time. To be computable at compile time, the set of allowed expressions is limited:

Expression Additional Restrictions
Unary  
Binary  
Ternary  
Cast Only compiler-computable casts; only casts between primitives
Index Only constant indexes on constant initializer lists
Member access Only when accessing members that are compile-time constants, meaning the accessed expression does not need to be a compile-time constant
Extend Only on type template parameters
Initializer list Only when every item is a compile-time constant
Literal  
Variable Only template parameters

Given the class definition:

class Int<int min, int max> where { min <= max; } {
  // ...
}

Then we can see the following examples:

Int<0, 10>
Int<5, 5>
Int<10, 0> // Compile error

4.5.1.2 Special Constraints

The following constraints only apply to type template parameters:

A T extends Y constraint ensures template parameter T is or derives from Y.

A T is primitive constraint ensures template parameter T is a primitive type.

A T is notnull constraint constrains the template parameter T to being a non-nullable type. Non-nullable annotations are disallowed on type template parameters, so this constraint is required for the template class to know the template parameter is a non-nullable type.

A T has default constraint ensures template parameter T has a default value (i.e. can use the default literal on it).

A T has constructor constraint ensures template parameter T has a parameterless constructor.

4.6 Enums

Enums are value types that contain a list of integral constants. Enum field values implicitly start at 0 and count up, but explicit values can be specified. The underlying integral type defaults to int but can be specified:

enum MyEnum extends uint8 {
  Field1,
  Field2,
  // ...
}

Where Field1 equals 0 and Field2 equals 1. Explicitly declaring field values can be done as such:

enum MyEnum {
  Field1 = 300,
  Field2 = 400,
  // ...
}

Creating an instance of an enum type is done by initializing to a field of the enum:

MyEnum myLocal = MyEnum.Field1;

enum MyEnum {
  Field1,
  // ...
}

Instances of enum types can interact with their underlying integral type implicitly:

int myLocal = MyEnum.Field1 + 10;
// myLocal = 20

enum MyEnum {
  Field1 = 10
}

4.6.1 Flags

The flags keyword can be used to signal to other developers that the enum is meant to be used with multiple fields at the same time. For example:

var myLocal = MyEnum.Field1 | MyEnum.Field2;

enum flags MyEnum {
  None,
  Field1,
  Field2,
}

Beyond documentation, the flags keyword also changes the default value behavior of enum fields. Instead of incrementally counting up from 0, enum fields will count up in powers of 2 starting at 1 (1, 2, 4, 8, etc.) so that when the fields are combined their bits do not conflict. You can still give fields explicit values like normal.

Additionally, flags enums string cast will display each field component of the value. For example:

var myLocal = MyEnum.Field1 | MyEnum.Field2;
var myString = (string)myLocal; // myString = "Field1, Field2"

enum flags MyEnum {
  Field1,
  Field2,
}

If a default flag at 0 is created the fields will still count up by powers of 2:

enum flags MyEnum {
  None = 0,
  Field1, // 1
  Field2, // 2
  Field3, // 4
}

4.6.2 Implicit Enum Fields

In target typed expressions, an implicit enum field expression can be used which omits the enum type name. The following are equivalent:

var myLocal = MyEnum.Field1;
MyEnum myLocal = .Field1;

Any target typed expression context supports this shorthand, which also includes method arguments:

Func(.Field1);

void Func(MyEnum param) { /* ... */ }

4.6.3 Experimental Underlying Types

When using the Evaluator, enums can additional represent the string and char primitives:

enum MyEnum extends string {
  Field1 = "some string",
}

This feature is experimental and may be removed.

4.6.4 Bit Testing

The traditional way to test for the presence of a enum field is to use a bit test:

var f = F.B;

if (f & F.B != 0) { /* ... */ }

enum flags F {
  None,
  A,
  B,
  C
}

To simplify this, enum fields can be qualified with an instance receiver in which case the bit test will be performed implicitly:

var f = F.B;

if (f.B) { /* ... */ }

4.6.5 Methods

Enums don’t contain methods in metadata, but methods can be written inside of an enum for convenience.

A static method is treated the same as an ordinary static method where it is qualified with the enum name. A non-static method is called off of a receiver that is of the enclosing enum type. Inside of a non-static enum method, this refers to an instance of the enum (that is, the value type).

For example:

var f = F.B;
return f.IsAOrB(); // true

enum flags F {
  None,
  A,
  B,
  C,

  public bool IsAOrB() {
    return this == .A || this == .B;
  }
}

Note that by using the bit testing shorthand the method IsAOrB could also be written:

public bool IsAOrB() {
  return this.A || this.B;
}

Methods inside of an enum can inter-splice the enum fields, but note that any preceding fields must include a trailing comma for the method to be parsed correctly:

enum E {
  public static void M1() { }

  A,
  B,

  public static void M2() { }

  C,

  public static void M3() { }
}

4.7 Namespaces

Namespaces can optionally be defined in a source file to organize types. Namespace names allow periods.

namespace MyNamespace {
  class A {
    // ...
  }

  // ...
}

Instead of using enclosing curly braces, namespaces can be scoped to the entire source file. Only one namespace can be used per file if they are file scoped:

namespace MyNamespace;

class A {
  // ...
}

4.8 Using Directives

Using directives can be used to access namespace or class members without needing to type the qualifier when outside of the container. Using namespace directives follow the format using <namespace name>;.

namespace MyNamespace {
  public class A { }
}
using MyNamespace;

var a = new A();

Using class directives follow the format using static <class name>;.

public class A {
  public class B { }
}
using static A;

var b = new B();

Using directives can be tied to the source file or to a namespace:

namespace A {
  using /* ... */;
}

4.8.1 Aliasing

An alias can be defined to allow referencing a type or namespace with another name, typically for brevity or clarity:

using D = A.B.C.D;

namespace A {
  namespace B {
    namespace C {
      public class D { }
    }
  }
}

var a = new D();

4.8.3 Global Using Directive

A global using directive can be used to apply a using directive to an entire project instead of only in the source file where the directive is placed.

4.8.2 Global Disambiguation

A global:: qualifier can be used to disambiguate cases where it is not clear what member is being referred to due to the usage of using directives:

using N;

class A { }

namespace N {
  public class A { }
}

var a = new A(); // ambiguous
var a = new global::A(); // clear

4.9 Structs

Structs are similar to classes. Unlike classes, structs are value types (passed by value). Structs are a collection of ordered fields:

struct A {
  int a;
  bool b;
}

Structs always have a parameterless constructor that sets every member to it’s default value. From there, fields can be set. Unlike class fields, struct fields are assigned a default value instead of requiring definite assignment. As such, struct fields must be of a type that has a default value (e.g. non-nullable reference types are not allowed).

var myStruct = new A();
myStruct.a = 3;
myStruct.b = true;

struct A {
  int a;
  bool b;
}

A cascade expression can be used to simplify this process:

var myStruct = new A()
  ..a = 3
  ..b = true;

struct A {
  int a;
  bool b;
}

Because struct fields cannot have explicit initializers, structs can only contain fields of types with a default value.

Accessibility modifiers can still be used in structs to make members private:

struct A {
  private int a;
}

Similar to classes, structs can contain members such as constructors and methods or other nested types:

struct A {
  int a;

  public constructor() {
    a = 3;
  }
}

Note that any parameterless struct constructors must be made public, but this is the default accessibility modifier so the following is also correct:

struct A {
  int a;

  constructor() {
    a = 3;
  }
}

While normal fields cannot contain initializers, static or constexpr fields still can:

struct A {
  static const int f1 = 3; // OK
  constexpr int f2 = 10; // OK
  int f3 = 3; // Invalid
}

4.9.1 Unions

A union struct is a struct where all of the fields overlap in memory. Because of this, assigning to any field in the union may effect the other fields:

var myUnion = new A();
myUnion.a = 5;
Console.PrintLine(myUnion.b); // 5

union A {
  int32 a;
  int16 b;
}

An anonymous union can be used inside of a struct to align certain fields together:

var myStruct = new A()
  ..a = 3
  ..c = 10;

Console.PrintLine(myStruct.b); // 10

struct A {
  int32 a;

  union {
    int32 b;
    int16 c;
  }
}

In this example, the fields b and c are overlapping with each other but not with a.