Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

Array literals

[a, b, c] · [ new GridColumn<User> { Name = "Email", Value = u => u.Email }, … ]

An array literal [a, b, c] is a value: a list you can return, assign, pass as an argument, or supply as a UI prop. Its element type is inferred from the items. Object literals inside it (new T { … }) are plain data, so a render prop like columns: [ new GridColumn<User> { Name = "Email", Value = u => u.Email } ] is fully expressible.

preview6 examples compiled by CIfunctiontypesauthoringcollections

Summary#

An array literal [a, b, c] is a value expression — a list you can return, assign to a local, pass as an argument, or hand to a UI control as a prop. The element type is inferred from the items (a homogeneous list is the common case). Because object literals (new T { … }) are plain data, a list of them is a first-class value too — so a UI prop like columns: [ new GridColumn<User> { Name = "Email", Value = u => u.Email } ] is expressed directly, no workaround.

Signature#

[1, 2, 3]                                     // int[]
["Email", "DisplayName"]                       // string[]
[ new GridColumn<User> { Name = "Email", Value = u => u.Email } ]   // GridColumn<User>[]
[]                                             // an empty list

Description#

  • A value, anywhere a value fits — return it, assign it (var xs = [1, 2, 3];), pass it as an argument, or use it as a render/state value in a component.
  • Element type is inferred from the items; a list of new GridColumn<User> { … } is a GridColumn<User>[], matching a prop declared GridColumn<T>[].
  • Object literals inside are pure data. In a render position a new T { … } builds a plain data object (it is not a stored row — that is what a new T { … } inside an action body does). This is why a control's columns prop takes [ new GridColumn<T> { … } ] cleanly.
  • Distinct from set-membership. Testing membership is written [a, b].Contains(x) (or x in [a, b]), which the compiler lowers to a set test — separate from using [a, b] as a value.

It takes the type it is written into#

As in C# 12, a literal is target-typed: written where a List<T>, a HashSet<T> or a byte[] is expected, it is one — List<string> tags = []; needs no .ToList(). An empty [] written where a Dictionary<K, V> is expected is an empty dictionary, and where a HashSet<T> is expected an empty set (a value added to it twice is held once). The declaration decides, wherever it is written — a local, a class's field, a component's field — and the value is the same collection on the server and in the browser. That holds for a return, an argument, a field initializer, each branch of a ?:, and the right-hand side of ??, which takes the LEFT operand's collection type (or, failing that, the type the whole expression is written into). So the null fallback C# code writes everywhere is just that:

class Draft { public List<string>? Tags; }

List<string> TagsOf(Draft d) => d.Tags ?? [];                // the empty list, as a List<string>
List<string> TagsOrDefault(Draft d) { return d.Tags ?? ["untagged"]; }

int WithOneMore(Draft d) {
  var tags = d.Tags ?? [];        // a List<string> (the left side's type), so it can grow
  tags.Add("new");
  return tags.Count;
}
[Test]
void The_fallback_takes_the_left_sides_type() {
  Assert.Equal(0, TagsOf(new Draft()).Count);
  Assert.Equal(["untagged"], TagsOrDefault(new Draft()));
  Assert.Equal(["a"], TagsOrDefault(new Draft { Tags = ["a"] }));
  Assert.Equal(1, WithOneMore(new Draft()));
}

⚠ Only a literal is retyped. An existing T[] on the right of ?? is not turned into a List<T>: an array is a read-only sequence (usually a query result), and handing it back as a list would promise an .Add it cannot keep. Write .ToList() when that is what you mean.

Where can new() leave out the type?#

C# 9's target-typed new() takes the type of the place it is written — a declared local, a class's or a component's field (public Dictionary<string, string> Values = new();), a return, an argument, an initializer member: List<int> xs = new();, Point p = new();. With braces it is that type's initializer: new() { 1, 2 } for a list or set, new() { ["k"] = 1 } for a dictionary, new() { X = 1 } for a class. Where the position gives no type — a lambda's body, an interpolation hole, the right of ?? — C# casts, and so does Osy#: (List<int>)[1, 2], (int[])[], (Point)new().

int Counts() {
  List<int> xs = new(); xs.Add(1);
  HashSet<int> ids = new() { 1, 2, 2 };                // a set: 2 elements
  Dictionary<string, int> stock = new() { ["a"] = 3 };
  return xs.Count + ids.Count + stock["a"] + ((List<int>)[7, 8]).Count;   // 1 + 2 + 3 + 2
}

Can a page send one to the server?#

Yes. A List<T>, a HashSet<T> and a Dictionary<K, V> are passed to a server function from a component's action as any value is — as the argument itself, or as a member of a class that is — and come back the same way. A dictionary or set made with StringComparer.OrdinalIgnoreCase still compares that way on the other side.

public class Reply {
  public string Comment = "";
  public Dictionary<string, string> Values = [];
}

public string Send(Reply reply) { return reply.Comment + ": " + reply.Values.Count; }

component Form() {
  Dictionary<string, string> typed = [];
  string said = "";
  action Submit() { said = Send(new Reply { Comment = "done", Values = typed }); }
  render { Pressable(onClick: Submit) { Text("Send " + said); } }
}

Is there a Queue<T> or a Stack<T>?#

Not yet. The collections are List<T>, HashSet<T>, Dictionary<K, V> and arrays (T[]); new Queue<int>(), new Stack<int>() and new LinkedList<int>() are refused by name, each with its stand-in. A list does all three jobs:

/// First in, first out: 1 then 2.
int ServeNext() {
  var queue = new List<int>();
  queue.Add(1); queue.Add(2);                       // enqueue
  var next = queue[0]; queue.RemoveAt(0);           // dequeue
  return next * 10 + queue[0];                      // 12 — and queue[0] peeks
}

/// Last in, first out: 4 then 3.
int UndoLast() {
  var stack = new List<int>();
  stack.Add(3); stack.Add(4);                                        // push
  var top = stack[stack.Count - 1]; stack.RemoveAt(stack.Count - 1); // pop
  return top * 10 + stack[stack.Count - 1];                          // 43
}

Examples#

int[] Small() { return [1, 2, 3]; }
string[] Names() { var xs = ["Ada", "Grace"]; return xs; }

Supplying a control's typed columns from an array of object literals (a UI prop):

entity User {
  [Required, MaxLength(200)] string Email;
  [Required, MaxLength(120)] string DisplayName;
  security { allow read when IsAuthenticated; }
}

[Route("/users")] [Render(CSR)]
component UsersPage() {
  var users = User.ToList();
  render {
    DataGrid(
      label: "Users",
      rows: users,
      columns: [
        new GridColumn<User> { Name = "Email", Label = "Email", Value = u => u.Email },
        new GridColumn<User> { Name = "DisplayName", Label = "Name", Value = u => u.DisplayName }
      ]
    );
  }
}

See also#

Related

List indexer

Positional get/set on a List<T> by integer index, exactly like C#'s List<T>.this[int]. The index must be an integer…

List OrderBy (in-memory)

Sort a local List<T> in memory by a key selector, returning a NEW sorted List<T> (the source is untouched). Ascending…

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…