In my opinion, in the vast majority of cases, .NET developers find the 'unified' support for unicast / multicast and closed / open-instance well worth the minor overheads involved. It's unfortunate if you fall into the minority case, but there are ways around it if you don't mind breaking idioms and reinventing a lot of wheels. More to come on that later.
Actually, the intent of the question isn't all that clear, but I will try to tackle the individual points.
What I'm really curious to know is simply if it would be possible,
from a CLR perspective, to have a value type that could act as a
function pointer.
Of course. In fact, delegates are built using native int sized value-type function-pointers (IntPtr
in the managed world). All of the type-safe bells and whistles are built on top of that.
The IL for your Action<string> writeLine = Console.WriteLine;
example looks something like this:
// Push null-reference onto stack.
// (Console.WriteLine is a static method)
ldnull
// Push unmanaged pointer to desired function onto stack.
ldftn void [mscorlib]System.Console::WriteLine(string)
// Create delegate and push reference to it onto stack.
instance void [mscorlib]System.Action`1<string>::.ctor(object, native int)
// Pop delegate-reference from top of the stack and store in local.
stloc.0
where the Action<T>
constructor is very conveniently declared as:
// First arg is 'this' for closed-instance delegates.
// Second arg is pointer to function.
public Action(object @object, IntPtr method);
I guess another way of asking this would be: is System.Delegate, with
its invocation list and all, basically a fundamental CLR type; or is
it a wrapper around a simpler "function reference" type that simply
isn't exposed by any CLR languages?
Well, it's both a fundamental CLR type (in the sense that the execution-engine knows about it and treats it specially) and a "wrapper" around a "function reference" - System.Delegate
stores a pointer to the function as a field (and an object reference for closed-instance delegates). Multicast-support, through its subclass System.MulticastDelegate
, is obviously a lot more complicated because of the need to store the multicast invocation-list. Unification is achieved through the fact that all delegate-types in the wild must inherit from System.MulticastDelegate
.
And I would argue that the "function reference" is exposed to CLR languages - it's possible to get a MethodInfo
representing the method through the delegate's Target
property, and from there the associated method-handle and function-pointer.
But you probably know all of this already. It seems to me like your real question is:
How would I build a lightweight, type-safe, delegate-like value-type
that stores nothing but a pointer to a managed function in .NET?
Given everything I've mentioned so far, it's really quite easy:
// Cool but mostly useless lightweight (in space, not time)
// type-safe delegate-like value-type. Doesn't support closed-instance scenarios
// in the interests of space, but trivial to put in if desired.
public struct LeanDelegate<TDelegate>
{
// The only storage required.
private readonly IntPtr _functionPointer;
public LeanDelegate(TDelegate source)
{
if (source == null)
throw new ArgumentNullException("source");
var del = source as Delegate;
if (del == null)
throw new ArgumentException("Argument is not a delegate", "source");
if (del.Target != null)
throw new ArgumentException("Delegate is a closed-instance delegate.", "source");
if (del.GetInvocationList().Length > 1)
throw new ArgumentException("Delegate is a multicast delegate.", "source");
// Retrieve and store pointer to the delegate's target function.
_functionPointer = del.Method.MethodHandle.GetFunctionPointer();
}
// Creates delegate-instance on demand.
public TDelegate Delegate
{
get
{
if (_functionPointer == IntPtr.Zero)
throw new InvalidOperationException("Uninitialized LeanDelegate instance.");
// Use the aforementioned compiler-generated constructor
// to generate the delegate instance.
return (TDelegate)Activator.CreateInstance
(typeof(TDelegate), null, _functionPointer);
}
}
}
And then you can do:
var del = new LeanDelegate<Action<string>>(Console.WriteLine);
del.Delegate("Hello world");
What have you gained?
The ability to store a pointer to an arbitrary static-method (or instance-method if the delegate is an open-instance one) and execute it in a type-safe manner, all in machine-pointer size space (excluding temporary allocations).
What have you lost?
Closed-instance capability. Multicast. Direct compatibility with many other APIs. Speed (almost certainly), although workarounds are conceivable. The love of developers who will use your API.