Delegates & events
E# has two callable tiers, chosen by intent. Function pointers are specified in Functions; this page covers delegates and events.
| Tier | Form | CLR | Cost |
|---|---|---|---|
| function pointer | &f, type &(int, int -> int) | ldftn + calli | zero-alloc, single-target |
| delegate | Func/Action/EventHandler, delegate func, lambdas | MulticastDelegate subclass | heap, GC, multicast |
Method-group conversion
Section titled “Method-group conversion”A bare function name where a delegate type is expected converts to that delegate, bound directly to the real method (no synthesized forwarder, so reflection sees the actual target):
func dbl(x: int) -> int = x * 2let f: Func<int, int> = dbl // ldnull; ldftn dbl; newobj Func`2::.ctorIt fires only when the target delegate type is known — a typed parameter, a typed let, a return type,
or an event. An un-annotated let f = dbl is an error (ambiguous: delegate or function pointer?); write the
delegate type, or &dbl for a pointer. It works for any delegate type, including bridging to a nominally
distinct BCL delegate (let p: Predicate<int> = isEven). Lambdas convert the same way, with parameter
types inferred from the target’s Invoke signature.
Nominal delegate types — delegate func
Section titled “Nominal delegate types — delegate func”DelegateFuncDecl = "delegate" "func" TypeName "(" [ ParamList ] ")" [ ReturnType ] .delegate func Name(...) mints a nominal delegate type — a sealed MulticastDelegate subclass,
distinct from any structurally-identical Func<>. The emitted type is exactly what C# emits for
delegate R Name(...), so it crosses the assembly boundary both ways. A Func<int,int> is not a
BinOp even though both wrap (int) -> int — the same nominal philosophy as E#‘s interfaces. delegate
is contextual (only before func at member scope).
Events
Section titled “Events”EventDecl = "event" identifier ":" Type . // Type shall be a delegateAn event is a member — a controlled subscription point over a delegate. Events are declared field-style on
ref data or interface only (they imply identity); an event on a value data is
ES2140. The type after : shall be a delegate, else
ES2141. event is contextual (only before name : in a type body).
Raise. raise Name(args) fires the event; it lowers to a thread-safe capture-then-invoke and is
null-safe (a no-op with no subscribers). raise naming an event not declared on the enclosing ref data
is ES2142. raise is contextual (the event name between raise and (
distinguishes it from a call).
Subscribe / unsubscribe with += / -=, on E#-declared and external (C#) events alike. The emitted
CLR shape is exactly what C# emits for a field-style event — backing field, add_/remove_ accessors
(lock-free Delegate.Combine/Remove + Interlocked.CompareExchange), and an EventDefinition — so a C#
consumer subscribes with no glue, and E# subscribes to C# events the same way.