A few days ago I asked why delegates are reference types, based on my misguided notion that all you need for a delegate are two references: one to an object, and one to a function. What I completely overlooked (not because I wasn't aware, simply because I forgot) is that in .NET, delegates are at least partially in place to support events as a built-in implementation of the Observer pattern, which means that every delegate supports multiple subscribers by way of an invocation list.
This got me thinking, delegates really play two different roles in the .NET world. One is that of a humble function pointer, such as:
Action<string> writeLine = Console.WriteLine;
The other is that of an observable:
textBox.TextChanged += HandleTextChanged;
The existence of an invocation list seems to be exclusively for the second role, as in cases like the simple writeLine
example above you generally don't even think about subscribers.
So really, it seems to me there could be two different "kinds" of delegates: the "function pointer" kind, and the "observable" kind. The former, it seems to me, could be a value type.
Now, I'm not arguing that this should be the case, if it's even possible. I am sure there would be a lot of downsides to making this distinction between regular and multicast delegates, such as the likely high frequency of boxing if delegates were value types, the possible need to introduce a new keyword (multicast
?), the inevitable developer confusion, etc. 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.
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?
I apologize for all of the informal terms I've used that may have confused some of the more educated developers out there.
In the very early days of the CLR there used to be a distinction between System.Delegate (function pointer like) and System.MulticastDelegate (event like). That was scrapped before .NET 1.0 shipped, there is no way to create an instance of a delegate type that derives from Delegate. It just wasn't necessary. MulticastDelegate was optimized to only create its invocation list when there's more than one subscriber. System.Delegate was preserved for some reason, probably too much work to remove it.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With