Summary#
A class field can hold many values. Four spellings, all of them C#'s:
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#
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, .Add(k, v), .Remove(k), .Keys, .ValuesT may be a scalar (string, int, long, decimal, double, bool, Guid, DateTime, …) or another
class. byte[] is the exception and is not a sequence at all — it is the binary scalar type.
Description#
Reading one#
Every sequence field answers the same questions, and they are the LINQ verbs you already use — filter, project, sort, aggregate — evaluated in memory over the values the object holds:
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:
int Total(Document d) {
var sum = 0;
foreach (var s in d.Scores) { sum = sum + s; }
return sum + d.Scores[0];
}Filling one#
A collection literal fills any of them at construction:
Document New() {
return new Document {
Title = "notes",
Keywords = ["osy", "reference"],
Scores = [10, 30, 20]
};
}A map's contents#
A Dictionary<K, V> answers d[k], .ContainsKey(k), .Add(k, v), .Remove(k) and .Count. .Keys and
.Values hand you its contents as an ordinary sequence — which is what lets every verb above reach a map at all:
int TotalSeen(Document d) {
return d.Counts.Values.Sum();
}
int HowManyKeys(Document d) {
return d.Counts.Keys.Count;
}
int Busiest(Document d) {
return d.Counts.Values.Where(c => c > 1).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 one#
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:
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.
To ask where something sits rather than change anything, .IndexOf(item), .LastIndexOf(item) and
.FindIndex(x => …) 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, 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.
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:
d.Keywords.Add("x"); // ✗ `Add` needs a growable collection, and `string[]` is fixed-size
d.Tags.Add(tag); // ✓ a List growsNothing 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?#
This is a class surface, and deliberately. On an entity 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
DocumentTagentity holding onestring, 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 for the relation form.
See also#
- LINQ over a local list — the verbs that read these fields, and the same verbs over a local sequence
- class properties — a member that runs a body on access
- relations — what a collection member means on an entity, and why it is a different thing