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.
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();
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;
Fields are similar to variables or constants. They are declared as such:
class MyClass {
int myField1;
string myField2 = "Starting Value";
// etc.
}
Fields have the same flexibility as traditional variables and constants, meaning they can be any type including another class:
class A { }
class B {
A myField = new A();
}
Methods are similar to functions. They are declared as such:
class MyClass {
public void MyMethod(int param1) {
// Body
}
}
When accessing a method, it must be called:
var myInstance = new MyClass();
myInstance.MyMethod();
Operators are similar to methods. They are declared as such:
class MyClass {
public static MyClass operator+(MyClass left, MyClass right) {
// Body
}
}
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.
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.
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.
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.
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.
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 { }
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);
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 })
});
Templates can be constrained at compile-time to ensure intended functionality. These constraints are defined within a
single where block in the class header.
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
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
}
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 (0, 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 {
None,
Field1,
Field2,
}
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;
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.