Подтвердить что ты не робот

Изменение сигнатуры делегата в библиотеке, чтобы опустить аргумент, не нарушает использование приложений

Рассмотрим следующий код в библиотеке классов:

public class Service
{
    public delegate string Formatter(string s1, string s2);

    public void Print(Formatter f)
    {
        Console.WriteLine(f("a", "b"));
    }
}

И вот консольное приложение, которое его использует:

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);
}

Пока он печатает "a-b", как и следовало ожидать.

Теперь я меняю библиотеку классов следующим образом:

public class Service
{
    public delegate string Formatter(string s1);

    public void Print(Formatter f)
    {
        Console.WriteLine(f("a"));
    }
}

т.е. Я удалил один параметр из делегата. Я компилирую только библиотеку классов и перезаписываю dll, сидящую рядом с консольным приложением (консольное приложение не перекомпилировано). Я бы ожидал, что это нарушение в библиотеке, и если я запустил приложение, он обнаружит несоответствие, в результате чего возникнет какое-то исключение во время выполнения.

В отличие от этого, когда я запускаю приложение, здесь нет никакого исключения, и я получаю потрясающий вывод "-a". Когда я отлаживаю, я вижу, что вызывается метод Concat (с двумя параметрами), уровень стека вызовов ниже показывает вызов печати в f ( "a" ) (один параметр), без каких-либо ошибок. Самое интересное, что в Concat s1 имеет значение null, s2 является "a".

Я также играл с различными изменениями в сигнатуре (добавление параметров, изменение типа параметра) в основном с тем же результатом. Когда я изменил тип s2 из строки в int, я получил исключение, но не тогда, когда был вызван метод Concat, но когда он попытался вызвать string.Format.

Я попробовал это с .NET target framework 4.5.1 и 3.5, x86 и x64.

Может ли кто-нибудь ответить, является ли это ожидаемым поведением или ошибкой? Мне кажется довольно опасным.

4b9b3361

Ответ 1

Здесь более простой вариант - в основном, я использую конструктор "под капотом" в типе делегирования (тот, который использует IL), чтобы передать целевой объект с неправильной подписью и... он отлично работает (под которым я подразумеваю, что он не генерирует исключение - он ведет себя точно так же, как ваш код):

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);
    }
}

На самом деле это не объяснение. Но было бы полезно, если вы хотите спросить кого-то еще CLR-эксперта! Я бы ожидал, что конструктор делегата громко пожаловался, что цель неверна.

При догадках (чистая спекуляция) это случай: если вы передаете IntPtr (native int), то вы полностью по своему усмотрению - код делает самую быструю вещь. Это похоже на неприятную ловушку для неосторожного, хотя!

Что касается того, почему s2 имеет значение, а s1 пусто: я думаю, это связано с тем, что стек сбрасывается (не вверх), поэтому в двухпараметрическом методе arg1 - это параметр немедленно смежный в предыдущую позицию в стеке. Когда мы передаем одно значение вместо двух, мы помещаем только одно значение, поэтому s2 имеет значение, а s1 - undefined (может быть мусор из предыдущего кода).