Consider the following code in a class library:
public class Service
{
public delegate string Formatter(string s1, string s2);
public void Print(Formatter f)
{
Console.WriteLine(f("a", "b"));
}
}
And here's a console application that uses it:
static void Main(string[] args)
{
s = new Service();
s.Print(Concat);
}
static string Concat(string s1, string s2)
{
return string.Format("{0}-{1}", s1, s2);
}
So far it prints "a-b", just as one would expect.
Now, I change the class library as follows:
public class Service
{
public delegate string Formatter(string s1);
public void Print(Formatter f)
{
Console.WriteLine(f("a"));
}
}
I.e. I removed one parameter from the delegate. I compile the class library only and overwrite the dll sitting next to the console app (console app is not recompiled). I'd expect that this is a breaking change in the library and if I execute the app, it finds the mismatch, resulting in some runtime exception.
In contrast, when I run the app there's no exception at all, and I get the stunning output "-a". When I debug, I can see that the Concat method (with 2 parameters) is called, the call stack level below shows Print calling to f("a") (one parameter), no error indication anywhere. Most interestingly, in Concat s1 is null, s2 is "a".
I also played around with different changes to the signature (adding parameters, changing parameter type) mostly with the same result. When I changed the type of s2 from string to int I got an exception, but not when the Concat method was called, but when it tried to call string.Format.
I tried it with .NET target framework 4.5.1 and 3.5, x86 and x64.
Can anyone answer whether this is the expected behaviour or a bug? It seems pretty dangerous to me.
Here's a simpler repro - basically, I'm using the "under the hood" constructor on the delegate type (the one that the IL uses) to pass a method target with the wrong signature, and... it works fine (by which I mean it doesn't throw an exception - it behaves just like your code):
using System;
static class P
{
static void Main()
{
// resolve the (object, IntPtr) ctor
var ctor = typeof(Func<string, string>).GetConstructors()[0];
// resolve the target method
var mHandle = typeof(P).GetMethod(nameof(Concat))
.MethodHandle.GetFunctionPointer();
object target = null; // because: static
// create delegate instance
var del = (Func<string, string>)ctor.Invoke(new object[] { target, mHandle });
var result = del("abc");
Console.WriteLine(result); // "-abc"
}
public static string Concat(string s1, string s2)
{
return string.Format("{0}-{1}", s1, s2);
}
}
This is not really an explanation. But it might be helpful if you want to ask someone more CLR-expert! I would have expected the delegate constructor to have complained loudly about the target being incorrect.
At a guess (pure speculation), it is a case of: if you're passing an IntPtr
(native int), then you're entirely on your own - the code does the fastest thing possible. It does seem like a nasty trap for the unwary, though!
As for why s2
has the value and s1
is empty: I guess that is because the stack builds down (not up), hence in a two parameter method, arg1
is the parameter immediately adjacent to the previous position on the stack. When we pass a single value instead of two, we only put one value underneath, so s2
has a value, and s1
is undefined (could be garbage from previous code).
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