简体   繁体   中英

Changing Delegate signature in library to omit an argument does not break applications using it

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 " ab ", 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"));
    }
}

Ie 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).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM