# Sequence fields on a class

> A class can hold many values in one field — `string[]`, `int[]`, `List<Tag>`, `HashSet<string>`, `Dictionary<string, int>`. The element may be a scalar, an enum, or another class; the whole LINQ surface reads it, a `foreach` over a map binds each entry's `Key` and `Value`, and an array is fixed-size exactly as in C#. An entity cannot hold one: a collection member of an entity is a relation.

<!-- id: class-collections · area: class · stability: stable · html: https://osysharp.com/reference/class/collections/ -->

## Summary        {#summary}
A `class` field can hold **many values**. Four spellings, all of them C#'s:

```osy title="four ways to hold many values" test app=class-collections
class Tag { public string Name; }

class Document {
  public string Title;
  public string[] Keywords;             // fixed-size — its length is part of it
  public int[] Scores;                  // any scalar, not just string
  public List<Tag> Tags;                // growable
  public HashSet<string> Seen;          // distinct, unordered
  public Dictionary<string, int> Counts;
}
```

## Signature      {#signature}
```osy syntax
public T[] Field;                      // array   — fixed-size; indexable, .Count, foreach, LINQ. No .Add
public List<T> Field;                  // list    — everything an array does, plus .Add / .Remove / .RemoveAll
public HashSet<T> Field;               // set     — distinct membership; .Add / .Contains / .Remove
public Dictionary<K, V> Field;         // map     — d[k], .ContainsKey, .TryGetValue, .GetValueOrDefault, .Keys, .Values
```

`T` may be a **scalar** (`string`, `int`, `long`, `decimal`, `double`, `bool`, `Guid`, `DateTime`, …), an **enum**
you declared, **another class**, or **another collection** — `Dictionary<string, List<int>>` and `List<List<int>>`
are both fine, at any depth. `byte[]` is the exception and is not a sequence at all — it is the binary scalar type.

⚠ The one thing that cannot nest is a **Dictionary KEY**: a key has to be comparable, and a collection is not. The
compiler says so rather than storing it and failing later.

## Description    {#description}

### Reading one   {#reading}
Every sequence field answers the same questions, and they are the [LINQ verbs](https://osysharp.com/reference/query/in-memory-linq/) you already
use — filter, project, sort, aggregate — evaluated in memory over the values the object holds:

```osy title="the whole LINQ surface, over a field" test app=class-collections
int LongKeywords(Document d) {
  return d.Keywords.Where(k => k.Length > 5).Count;
}

string FirstAlphabetically(Document d) {
  return d.Keywords.OrderBy(k => k).First(k => k != "");
}

bool EveryScorePasses(Document d) {
  return d.Scores.All(s => s >= 0);
}

// `Max` has no zero to fall back on, so over NO scores it answers absent — which is why this returns `int?`
// where `Count` above returns `int`. Writing `int` here is a compile error, not a silent 0.
int? Best(Document d) {
  return d.Scores.Max(s => s);
}
```

Indexing and `foreach` read it directly, with no ceremony:

```osy title="index it, walk it" test app=class-collections
int Total(Document d) {
  var sum = 0;
  foreach (var s in d.Scores) { sum = sum + s; }
  return sum + d.Scores[0];
}
```

### A collection member starts EMPTY   {#starts-empty}
A sequence member of a class is an empty collection from the moment the object is made — you never have to check
it before adding to it:

```osy syntax
class Valley {
  List<Prop> trees = new List<Prop>();

  public void Build() {
    trees.Add(new Prop { X = 1 });     // works on a brand-new Valley
  }
}
```

The initializer above is the natural way to write it and is what most people will. It is not load-bearing: a
`List<Prop> trees;` with no initializer is empty too. Every other reading in the language already treats an absent
collection as empty — iterating one yields nothing, `.Count` answers 0, a query over it is empty — so `.Add` agrees
with them rather than being the one operation that does not.

### Filling one   {#filling}
A collection literal fills any of them at construction:

```osy title="build one" test app=class-collections
Document New() {
  return new Document {
    Title = "notes",
    Keywords = ["osy", "reference"],
    Scores = [10, 30, 20]
  };
}
```

### Reading a map         {#dictionary}
`d[k]` reads one entry and **throws when the key is absent**, exactly as in C#. The two total forms are the ones to
reach for when a miss is ordinary:

```osy title="reading a key that may not be there" test app=class-collections
// `.TryGetValue` — C#'s own spelling, and the value is in scope in the branch.
int SeenOrZero(Document d, string word) {
  if (d.Counts.TryGetValue(word, out var n)) { return n; }
  return 0;
}

// `.GetValueOrDefault` — the same question as an expression, so it composes with `??`.
int Seen(Document d, string word) {
  return d.Counts.GetValueOrDefault(word, 0);
}
```

⚠ With **no fallback**, `.GetValueOrDefault(k)` answers **null** on a miss rather than the type's zero. That is a
deliberate difference from C#, and it is the useful one: a `0` is indistinguishable from a *stored* `0`, so a miss
would be invisible. A null makes you decide.

### Iterating a map       {#iterating}
**A `foreach` over a map binds each entry as a pair, with `Key` and `Value`** — C#'s own shape:

```osy title="every entry, both halves" test app=class-collections
string Summarise(Document d) {
  var s = "";
  foreach (var kv in d.Counts) { s = s + kv.Key + "=" + kv.Value + ";"; }
  return s;
}
```

The same pair is what the **LINQ verbs** filter and project over, so a map is a source like any other sequence:

```osy title="LINQ straight over a map" test app=class-collections
int TotalRepeated(Document d) {
  return d.Counts.Where(kv => kv.Value > 1).Sum(kv => kv.Value);
}

// `First` answers the PAIR, so its `Key` is one hop away.
string Rarest(Document d) {
  return d.Counts.OrderBy(kv => kv.Value).First(kv => kv.Value > 0).Key;
}
```

`.ToList()` and `.ToArray()` both hand you the pairs as a plain sequence.

`.Keys` and `.Values` are still there when you only want one half:

```osy title="one half at a time" test app=class-collections
int TotalSeen(Document d) {
  return d.Counts.Values.Sum();
}

int HowManyKeys(Document d) {
  return d.Counts.Keys.Count;
}
```

Both are a **snapshot**, not C#'s live view: a `.Values` you already took does not follow a later `.Add`. Take it
again when you want the current contents.

### Changing a map        {#map-changing}
`.Add(k, v)` **throws if the key is taken** — that is exactly what separates it from `d[k] = v`, which overwrites.
When you mean "add only if absent", `.TryAdd` says so and answers whether it went in:

```osy title="the three ways to write into a map" test app=class-collections
void Record(Document d, string word) {
  d.Counts[word] = 1;                    // upsert — replaces whatever was there
  d.Counts.TryAdd(word, 1);              // adds only if absent; answers false if not
  d.Counts.Remove(word);                 // answers whether it was there
  d.Counts.Clear();                      // empties it
}

bool AnyoneAt(Document d, int n) {
  return d.Counts.ContainsValue(n);      // the mirror of .ContainsKey
}
```

### Changing one          {#changing}
A `List<T>` grows and shrinks with `.Add(x)` and `.Remove(x)`. **`.RemoveAll(x => …)` deletes every match in one go
and answers how many went** — it changes the list you called it on, so anything else holding that same list sees the
change too:

```osy title="remove every match, in place" test app=class-collections
int DropShortTags(Document d) {
  return d.Tags.RemoveAll(t => t.Name.Length < 3);
}
```

That in-place mutation is the difference from `d.Tags.Where(…)`, which answers a **new** sequence and leaves the
original alone. Reach for `Where` when you want a filtered view, and `RemoveAll` when the list itself should change.

The rest of C#'s vocabulary is there too — `.AddRange(other)` appends every item of another collection,
`.Insert(i, x)` places one at a position, and `.Clear()` empties it (on a `List`, a `HashSet` or a `Dictionary`):

```osy title="append, place, empty" test app=class-collections
void Reset(Document d, List<Tag> extra) {
  d.Tags.AddRange(extra);          // append all of them
  d.Tags.Insert(0, new Tag { Name = "first" });
  d.Tags.Clear();
}
```

To ask **where** something sits rather than change anything, `.IndexOf(item)`, `.LastIndexOf(item)` and
[`.FindIndex(x => …)`](https://osysharp.com/reference/query/in-memory-linq/) answer its position, or `-1` when it is not there. `IndexOf` finds the
first occurrence and `LastIndexOf` the last, which is the only thing they disagree about. Over a list of
entity rows all three compare by [row identity](https://osysharp.com/reference/entity/equality/), so `.IndexOf(row)` finds the row without your
comparing `.Id`.

Because it changes things, a change belongs in an `action` (or an `on-change` body), never in a `render` slot — a
render expression is re-evaluated whenever anything it reads changes, in an order nobody controls, so the compiler
refuses `.Add` / `.Remove` / `.RemoveAll` there and names where they go instead. The reads (`.Count`, `.Contains`,
`.Keys`, `.Values`, indexing, every LINQ verb) are render-slot material and always were.

### A collection inside a collection   {#nesting}
The grouping shape — a map of lists — is an ordinary field:

```osy title="a map of lists" test app=class-collections-nested
class Index {
  public Dictionary<string, List<int>> ByTag;
  public List<List<int>> Rows;
}

int TotalTagged(Index ix) {
  var n = 0;
  foreach (var kv in ix.ByTag) { n = n + kv.Value.Count; }
  return n;
}

// The inner list is a REAL list — reached through the pair and appended to in place.
void Tag(Index ix, string tag, int id) {
  if (!ix.ByTag.ContainsKey(tag)) { ix.ByTag[tag] = new List<int>(); }
  ix.ByTag[tag].Add(id);
}
```

### An enum element       {#enums}
An element may be an **enum you declared**, in any of the four spellings — including both slots of a map, which is how
a per-status tally is written:

```osy title="collections of an enum" test app=class-collections-enum
enum Status { Open, Closed }

class Board {
  public List<Status> Lanes;
  public Status[] Order;
  public Dictionary<Status, int> Tally;      // an enum KEY
  public Dictionary<string, Status> ByName;  // an enum VALUE
}

int OpenCount(Board b) {
  var n = 0;
  foreach (var s in b.Lanes) { if (s == Status.Open) { n = n + 1; } }
  return n;
}

int OpenTally(Board b) {
  return b.Tally.Where(kv => kv.Key == Status.Open).Sum(kv => kv.Value);
}
```

### Array or list?     {#array-or-list}
Use a **`List<T>`** when the contents change — it is what you will want most of the time. Use a **`T[]`** when the set
of values is settled once and then only read.

The difference is C#'s and the compiler holds you to it: an array's length is part of the type, so it has no `.Add`:

```osy syntax
d.Keywords.Add("x");   // ✗ `Add` needs a growable collection, and `string[]` is fixed-size
d.Tags.Add(tag);       // ✓ a List grows
```

Nothing else differs. Both are indexable, both count, both `foreach`, both answer every LINQ verb — so choosing an
array never costs you a way to read it.

### Can an entity hold a list?   {#entities}
This is a `class` surface, and deliberately. On an [entity](https://osysharp.com/reference/entity/declaration/) a collection member means something
else entirely: `OrderLine[] Lines` is a **relation** — a set of child ROWS, stored in their own table and reached
through a foreign key. A column cannot hold a list, so `string[] Tags;` on an entity is refused, and it names the two
real answers:

- **Model each value as a row.** A `DocumentTag` entity holding one `string`, reached as
  `[ForeignKey(Document)] DocumentTag[] Tags;` — which is queryable, indexable, and the answer whenever you will ever
  want to search or count by it.
- **Or keep the list on a `class`**, when it is a value the row simply carries and nobody queries across.

See [relations](https://osysharp.com/reference/entity/relations/) for the relation form.

## See also       {#see-also}
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the verbs that read these fields, and the same verbs over a local sequence
- [class properties](https://osysharp.com/reference/class/properties/) — a member that runs a body on access
- [relations](https://osysharp.com/reference/entity/relations/) — what a collection member means on an entity, and why it is a different thing
