Summary#
A class that describes how to work with a value should not have to know which value. Declare it with a type parameter and one declaration serves every type:
class Column<T> {
public string Label;
public Func<T, string> Value;
}Without generics that shape has to be copied once per entity — a ReportColumn, a PersonColumn — each identical
except for one type. With them, Column<Report> and Column<Person> are two types from one declaration.
Signature#
class Name<T> { … } // one type parameter
class Name<TIn, TOut> { … } // several, comma-separated
Name<Report> // a CONSTRUCTED type: the argument supplied at the use site
new Name<Report> { … } // …and at constructionA type parameter is in scope over the whole class body — including in its methods' signatures. A method may also declare parameters of its own, which the call binds rather than the receiver:
class Util { public T Echo<T>(T v) { … } } // the method's own parameter
T Echo<T>(T v) { … } // a top-level function, the same wayentity cannot be generic.
Description#
The type argument goes where the class is USED#
The declaration names the parameter; the use site supplies the argument. Between them, the compiler knows what T
is at every point:
entity Report { [MaxLength(80)] string Title; decimal Total; }
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
render {
foreach (var r in rows) {
Row { foreach (var c in columns) { Text(c.Value(r)); } }
}
}
}
[Page("/")] [AllowAnonymous]
component Home() {
var reports = Report.ToList(); // the read is a MEMBER — a render body holds values, it does not fetch
render {
Grid(rows: reports, columns: [
new Column<Report> { Label = "Title", Value = r => r.Title },
new Column<Report> { Label = "Band", Value = r => r.Total > 1000 ? "large" : "small" }
]);
}
}Grid mentions no entity at all. Column<T> mentions no entity. The only place Report appears is the call site —
which is exactly the point.
Reading a member substitutes the argument#
Value is declared Func<T, string>. Read off a Column<Report> it is a Func<Report, string>, so the lambda's
parameter binds to a real row and its body is checked against it:
new Column<Report> { Value = r => r.Title } // ✓ r is a Report
new Column<Report> { Value = r => r.Nmae } // ✗ 'Report' has no property 'Nmae'That substitution is the whole feature. Passing the wrong one is caught too — a Column<Person> where a
Column<Report> is expected is a different type, not merely a differently-labelled Column.
Where a type parameter may appear#
T is first-class on its own, as an array, and inside a function type:
class Holder<T> {
public T Row; // the type parameter itself
public T[] Rows; // an array of it
public Func<T, string> Read; // a selector over it
}What is not built yet is T inside a mutable collection or another generic type — List<T>, HashSet<T>,
Dictionary<string, T>. Those carry a collection kind and a value slot that the compiler derives from the
declaration alone, so they need their own storage rather than being let through. The refusal names the three
spellings that do work.
An unset member typed T reads default(T), decided by the type argument: a Holder<decimal> reads 0m, a
Holder<Report> reads null — because null is default for a type with no meaningful zero.
Methods can have their OWN type parameters#
A method may declare type parameters that the class does not, and they are bound by the call rather than by the receiver — inferred from the argument types:
class Util {
public T Echo<T>(T v) { return v; }
public string Describe<T>(T v) { return "described"; }
}
string UseIt() {
var u = new Util();
var n = u.Echo(2m); // T is decimal here
var s = u.Echo("hello"); // …and string here, from the same declaration
return s;
}This is the difference worth holding on to: a generic class fixes its argument once, where it is used
(Column<Report>), and every member read off it substitutes that one answer. A generic method decides per call.
Write the arguments explicitly when inference has nothing to read them from — a type parameter that appears in no parameter cannot be inferred, and the compiler says so rather than guessing:
string Explicitly() {
var u = new Util();
return u.Echo<string>("hello");
}A top-level function may be generic the same way, and so may a method on a class that is itself generic — there the two sets are bound by different things, the class's by the receiver and the method's by the call. A method may not reuse one of its class's parameter names: the two would be different types wearing one name, and nothing in the source would show the reader which is which.
Classes only#
An entity cannot be generic. An entity is a table, and a table has no columns until T is known:
entity Row<T> { string Label; } // ✗ — write `class Row<T>` for an in-memory shapeExamples#
A comparator and a formatter are the same shape as a column, which is why this generalises past grids:
class Sorter<T> {
public string Label;
public Func<T, string> Key;
}
class Formatter<T> {
public Func<T, string> Render;
}Several type parameters are written the way C# writes them:
class Mapping<TIn, TOut> {
public Func<TIn, string> Read;
public Func<TOut, string> Write;
}Errors#
| What you wrote | What you get |
|---|---|
entity Row<T> { … } | 'Row' is an entity, and an entity cannot be generic — it is a table, and a table has no columns until its type argument is known. |
public List<T> Rows; on a class Holder<T> | 'Holder.Rows' is typed List<T>, and a type parameter is not supported inside a mutable collection or a generic type yet — it works on its own (T Rows;), as an ARRAY (T[] Rows;), or inside a function type (Func<T, string> Rows;). |
Column<Report, Report> | 'Column' takes 1 type argument (T), but 2 were written. |
u.Echo<decimal, string>(2m) on T Echo<T>(T v) | 'Echo' declares 1 type parameter ('T'), but 2 were given. |
calling string Make<T>(), which mentions T in no parameter | 'Make' cannot infer its type parameter 'T' from these arguments … Write the type argument explicitly: Make<T>(…). |
public string Show<T>(T v) inside a class Box<T> | 'Show' declares a type parameter 'T', and so does the type that declares it … Rename one of them. |
c.Add<decimal>(1m) where Add declares no type parameters | 'Add' declares no type parameters, so the type argument in Add<…>(…) binds to nothing … Drop the type arguments. |
Column with no argument | 'Column' is generic — it needs a type argument for 'T'. |
a Column<Person> where a Column<Report> is expected | expects Column<T>[] for 'columns', but got Column<Person>[]. |
See also#
- Func<T, R> —
Func<T, R>as a value: what a selector IS, and why its body reads only its parameters - Classes — the class itself: a value shape, never a table
- class properties — members that run a body on access
- component — generic components, which take their type argument by inference from the call site