Arrays, strings and collections
Arrays
Section titled “Arrays”Arrays are managed dynamic data (see memory model): reference semantics, ARC, a fixed length after creation.
var a = new int[10];var b = new int[] { 1, 2, 3 }; // an initializer listvar jagged = new int[3][]; // an array of arrays
a[0] = 42;Console.WriteLine(a.Length.ToString());
var c = a; // c references the same array as avar copy = a.Clone(); // an independent copy
Array.Copy(source, sourceIndex, destination, destinationIndex, count);Indexing is bounds-checked; an out-of-range index panics.
Collection expressions
Section titled “Collection expressions”[a, b, c] lists the elements; it becomes the type it is used as:
int[] a = [1, 2, 3]; // an array of exactly this lengthSlice<int> s = [4, 5]; // a new array, as a viewReadOnlySlice<int> r = [6, 7]; // the same, read-only (in a constant: static data, no array)List<string> names = ["ann", "bob"]; // Create(), then Add per elementHashSet<int> seen = [1, 2, 2]; // any struct with 'static Create()' and 'Add(T)'int[] all = [..a, ..s, 6]; // ..x spreads an array, a slice or a collection with ToArray()int[] none = [];var inferred = [1.5, 2.0]; // no type to become: an array of the first element's type (double[])Process([1, 2, 3]); // as an argument, the parameter's type decidesOptional<int[]> Find() { return [1, 2]; } // Optional<T> / Error<T>: the T is built, then wrappedAn array or slice is built with one allocation of exactly the right length (spreads included). The elements convert
to the element type like in an assignment. new int[n] (a zeroed array of a given length) and
new int[] { 1, 2, 3 } still work.
Fixed-size arrays: Fixed<T, N>
Section titled “Fixed-size arrays: Fixed<T, N>”int[] is always a heap array. For small buffers of a known size there is Fixed<T, N>: N elements stored
inline - in the variable (on the stack) or inside the struct that has the field. No allocation, no reference
count:
Fixed<int, 16> buffer; // 16 zerosbuffer[3] = 7;buffer[^1] = 9; // ^n counts from the endFixed<float, 16> matrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
struct Vertex{ Fixed<float, 3> Pos; // 12 bytes inside the struct}
const int Slots = 4;Fixed<string, Slots> names = ["a", "b", "c", "d"]; // the size can be an integer constant- A value, like a struct: assigning, passing and returning copy all elements, so nothing can refer to a Fixed
after it is gone. Pass big ones as
ref/const refto avoid the copy. - Never mixed up with heap arrays: there is no implicit conversion in either direction.
f.ToArray()copies into a newT[];Fixed<int, 4> f = [..heapArray];copies in (the length is checked when the program runs). - Checked: a collection expression must have exactly N elements; a constant index outside
0..N-1is a compile error, any other index is checked when the program runs. Lengthis a constant,foreachworks, and so doFixedof structs,FixedofFixed,Fixedin lists,Optional<Fixed<T, N>>and generic parameters (T First<T>(Fixed<T, 2> pair)).- A
Fixedof plain values can be athreadparameter (it is copied like any value). - C arrays in structs (
float m[16],int grid[2][3]) are imported asFixed<float32, 16>andFixed<Fixed<int32, 3>, 2>(C interop).
Strings
Section titled “Strings”Strings are UTF-8, immutable, and managed by ARC. Modifying one produces a new string:
string name = "Ann";string greeting = "Hello, " + name + "!";char first = greeting[0];int len = greeting.Length;string part = greeting.Substring(7, 3);
if (greeting == "Hello, Ann!") Console.WriteLine("match");Building text piece by piece is cheap: text += ... (and text = text + a + b) on a local variable appends in place
when nothing else refers to the string - no copy, a loop of appends takes linear time. A string that is shared (a
copy in another variable, a slice of it, a parameter that the caller still holds) is never changed: it is copied
first, exactly as immutability requires. The same holds for the pieces of a + b + c.
string log = "";foreach (var item in items) log += item.Name + ": " + item.Count.ToString() + "\n"; // linear, like a StringBuilderInterpolated strings put values into text; {x} is the same as + x +, so anything that can be added to a
string works (numbers, bool, char, strings, and structs with a string ToString() method). {{ and }} are
braces:
int count = 3;string text = $"{name} has {count} item(s), {count * 2} in total {{approx.}}";const string Title = $"{AppName} {Version}"; // works in constants tooNumbers become the shortest text that reads back as the same value: 0.1, 1.0 / 3.0 is 0.3333333333333333.
Number formats and alignment
Section titled “Number formats and alignment”ToString("F2") and {x:F2} in an interpolated string format a number; {x,8} fills the text with spaces on the
left up to 8 characters, {x,-8} on the right (for any value), and both combine as {x,8:F2}:
double price = 1234.5;Console.WriteLine($"{"Total",-8}|{price,12:N2}|"); // "Total | 1,234.50|"string hex = 255.ToString("X4"); // "00FF"| Format | For | Result |
|---|---|---|
D5 |
integers | at least 5 digits: 42 → 00042 |
X, x4 |
integers | hexadecimal, upper/lower case, at least 4 digits: 255 → FF, 00ff; negative values in two’s complement of the type’s size |
B8 |
integers | binary, at least 8 digits: 5 → 00000101 |
F2 |
all numbers | fixed point with 2 decimals (default 2): 3.14159 → 3.14 |
N2 |
all numbers | like F, with , between thousands: 1234567 → 1,234,567.00 |
E3, e3 |
all numbers | scientific, 3 decimals (default 6): 1234.5 → 1.235E+003 |
P1 |
all numbers | percent (times 100), 1 decimal (default 2): 0.256 → 25.6 % |
G |
all numbers | the same as ToString() |
- The letter can be followed by up to two digits (0 to 99). The text is the same on every platform:
.for the decimal point and,between thousands, no locale. - Floating point values are formatted from their exact binary value and rounded half away from zero, like .NET:
0.125.ToString("F2")is0.13,2.5.ToString("F0")is3. A result that is zero has no minus sign ((-0.001).ToString("F2")is0.00). - A format written as a string literal is checked by the compiler; a format that is only known at run time panics if it is invalid.
- In a hole, the
:of a conditionala ? b : cis part of the expression, the next:starts the format. - The alignment counts bytes (like
PadLeft), so text with non-ASCII characters lines up only in bytes. - A hole with a format or alignment is not a constant expression (it calls the standard library).
More string operations are extension-style methods from the standard library (Contains, Trim, Split, Join,
PadLeft, ParseInt, …) — see the standard library for the full list.
Slices
Section titled “Slices”a[i..j] is a view of the elements i to j - 1 of an array, a string or another slice — nothing is copied:
int[] a = new int[] { 1, 2, 3, 4, 5 };Slice<int> mid = a[1..4]; // 2, 3, 4var head = a[..2]; // 1, 2var tail = a[3..]; // 4, 5var last = a[^2..]; // ^n counts from the end: 4, 5int end = a[^1]; // also for a single element: 5
string s = "hello world";StringSlice word = s[6..]; // "world"bool same = word == "world"; // slices and strings compare by their bytes- An array gives a
Slice<T>, a string aStringSlice(byte offsets, likes[i]). Slices haveLength,[i](also[^i]),foreach, slicing again, and==for string slices; they go into text like strings ("x" + word,word + other,$"{word}"). AStringSlicehas the methods of strings (Trim,Split,IndexOf, …) and can be a key of aDictionaryor sorted in aListlike a string. - A slice is a small value (the array or string it belongs to, the first element and the length). It holds a
reference to that block, so it can be stored, returned and put into lists like any value; it also keeps the
whole block alive (
bigText[0..10]keeps all ofbigText). Copy out withword.ToString()ormid.ToArray()— copying is always explicit; a slice never turns into astringor array by itself. - A
Slice<T>writes through:mid[0] = 20changesa[1](arrays are shared anyway). AStringSliceis read-only. ReadOnlySlice<T>is the same view without write access:view[0] = 1is a compile error. Arrays,Slice<T>and collection expressions convert to it for free (never the other way), soint Sum(ReadOnlySlice<int> values)accepts all of them and promises not to change them. Slicing aReadOnlySlice<T>gives another one;ToArray()copies it into a normal array. Constant slices have this type.- These views work inside an
Optional<T>orError<T>as well: anOptional<int[]>converts toOptional<ReadOnlySlice<int>>, anError<string>toError<StringSlice>(no copy either way). - A whole string or array converts to a slice for free, so a function that takes
StringSliceorSlice<T>accepts both:int Sum(Slice<int> values)can be called withaora[1..]. - Text is also a
ReadOnlySlice<char>: astringorStringSliceconverts to it for free, so code written for slices of characters takes text too (astringargument prefers aStringSliceparameter).text.AsBytes()is the same view asReadOnlySlice<uint8>, for functions that take bytes:stream.Write(line.AsBytes()). - Ranges are checked:
0 <= start <= end <= Length, otherwise the program panics like with an index out of range. - A
ReadOnlySlice<T>can be passed to athreadfunction: the thread gets its own copy of the elements (threading.md).Slice<T>andStringSlicecannot (the block’s reference count is not atomic, and aSlice<T>could be written); password.ToString()for aStringSlice. Inunsafecode,.Ptr()gives a pointer to the first element (no terminating NUL);word.CStr()gives the text followed by a 0 byte for C (a copy only if the slice does not reach the end of its string).
List<T>
Section titled “List<T>”A growable array:
using System;
var names = List<string>.Create();names.Add("Ann");names.Add("Bob");names.Insert(0, "Zoe");
foreach (var name in names) Console.WriteLine(name);
Console.WriteLine(names.Count().ToString());names.Sort(); // needs T : IComparable<T>
names[0] = "Amy"; // the indexer: names.Set(0, "Amy")string first = names[0];var longNames = names.Where(n => n.Length > 3);Indexers: x[k] calls x.Get(k) and x[k] = v calls x.Set(k, v) (also x[k] += v), for List<T>,
Dictionary<K, V> and any struct of your own with such methods.
Dictionary<TKey, TValue>
Section titled “Dictionary<TKey, TValue>”A hash table; keys need IEquatable/IHashable (numbers, bool, char, enums and string already have them —
see interfaces and generics for adding them to your own structs):
var ages = Dictionary<string, int>.Create();ages["Ann"] = 30; // ages.Set("Ann", 30)ages["Ann"] += 1;int annsAge = ages["Ann"]; // panics if the key is missing
if (ages.TryGet("Ann") is int age) Console.WriteLine(age.ToString());
foreach (var entry in ages.Entries()) Console.WriteLine(entry.Key + ": " + entry.Value.ToString());
ListandDictionaryare small structs pointing at shared storage (there are no classes), so a copy sees the same elements as the original. The storage is made by.Create()(or[]); the zero value —new List<T>(),new(), a field without a value — is an empty container that can be read but not changed:AddandSetpanic (IsCreated()tells). The same holds for the containers below.
HashSet<T>, Stack<T>, Queue<T> and StringBuilder
Section titled “HashSet<T>, Stack<T>, Queue<T> and StringBuilder”var seen = HashSet<int>.Create();if (seen.Add(id)) Console.WriteLine("new");
var undo = Stack<string>.Create(); // last in, first outundo.Push("move");string last = undo.Pop(); // panics when empty; TryPop() gives an Optional<string>
var jobs = Queue<Job>.Create(); // first in, first outjobs.Enqueue(job);while (jobs.TryDequeue() is Job next) Run(next);
var sb = StringBuilder.Create();sb.Append("x = ");sb.Append(42.ToString());sb.AppendLine();string text = sb.ToString();