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

Странное поведение при использовании динамических типов в качестве параметров метода

У меня есть следующие интерфейсы, которые являются частью существующего проекта. Я хочу, чтобы можно было вызвать функцию Store (..) с динамическими объектами. Но я не хочу менять иерархию интерфейса (если это вообще возможно).

public interface IActualInterface
{
    void Store(object entity);    
}
public interface IExtendedInterface : IActualInterface
{
    //Interface items not important
}        
public class Test : IExtendedInterface 
{
    public void Store(object entity)
    {
        Console.WriteLine("Storing: " + entity.ToString());
    }       
}

и следующий код:

IExtendedInterface extendedInterfaceTest = new Test();
IActualInterface actualInterfaceTest = new Test();
Test directTest = new Test();

dynamic employee = new ExpandoObject();
employee.Name = "John Smith";
employee.Age = 33;
employee.Phones = new ExpandoObject();
employee.Phones.Home = "0111 123123";
employee.Phones.Office = "027 321123";
employee.Tags = new List<dynamic>() { 123.4D, 99.54D };

try
{
    extendedInterfaceTest .Store(employee);
}
catch (RuntimeBinderException rbEx)
{
    Console.WriteLine(rbEx.Message);
}

//Casting as (object) works okay as it not resolved at runtime
extendedInterfaceTest.Store((object)employee);

//this works because IActualInterface implements 'Store'
actualInterfaceTest.Store(employee);
//this also works okay (directTest : IProxyTest)
directTest.Store(employee);

Когда я вызываю extendedInterfaceTest.Store(employee), он вызывает исключение связующего времени во время выполнения. Почему тип интерфейса имеет значение, когда он имеет один и тот же базовый тип? Я могу называть его IActualInterface и Type, но не IExtendedInterface?

Я понимаю, что при вызове функции с динамическим параметром разрешение происходит во время выполнения, но почему разные поведения?

4b9b3361

Ответ 1

Что нужно помнить, так это то, что динамическое разрешение в основном выполняет тот же процесс, что и статическое разрешение, но во время выполнения. Все, что не может быть разрешено с помощью CLR, не будет разрешено DLR.

Возьмите эту небольшую программу, вдохновленную вашим, и которая вообще не использует динамику:

namespace ConsoleApplication38 {

    public interface IActualInterface {
        void Store(object entity);
    }
    public interface IExtendedInterface : IActualInterface {
    }
    public class TestInterface : IExtendedInterface {
        public void Store(object entity) {
        }
    }

    public abstract class ActualClass {
        public abstract void Store(object entity);
    }
    public abstract class ExtendedClass : ActualClass { 
    }
    public class TestClass : ExtendedClass {
        public override void Store(object entity) {
        }
    }

    class Program {

        static void TestInterfaces() {
            IActualInterface actualTest = new TestInterface();
            IExtendedInterface extendedTest = new TestInterface();
            TestInterface directTest = new TestInterface();

            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void TestClasses() {
            ActualClass actualTest = new TestClass();
            ExtendedClass extendedTest = new TestClass();
            TestClass directTest = new TestClass();

            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void Main(string[] args) {
            TestInterfaces();
            TestClasses();
        }
    }
}

Все компилируется отлично. Но что создал компилятор? Давайте посмотрим, используя ILdasm.

Для интерфейсов:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

Мы видим здесь, что компилятор С# всегда генерирует вызовы для интерфейса или класса, где определяется метод. IActualInterface имеет слот метода для Store, поэтому он используется для actualTest.Store. IExtendedInterface не используется, поэтому для вызова используется IActualInterface. TestInterface определяет новый метод Store, используя модификатор newslot IL, эффективно назначая новый слот в таблице vtable для этого метода, поэтому он непосредственно используется, так как directTest имеет тип TestInterface.

Для классов:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

Для трех разных типов один и тот же вызов генерируется, потому что слот метода определен в ActualClass.

Теперь посмотрим, что получим, если мы сами напишем IL, используя нужный тип, а не позволяем компилятору С# выбрать его для нас. Я изменил IL, чтобы выглядеть так:

Для интерфейсов:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IExtendedInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

Для классов:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ExtendedClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestClass::Store(object)

Программа компилируется с ILasm. Тем не менее, он не может пройти проверку и сбой во время выполнения со следующей ошибкой:

Необработанное исключение: System.MissingMethodException: метод не найден: "Пустота ConsoleApplication38.IExtendedInterface.Store(System.Object). в ConsoleApplication38.Program.TestInterfaces() в ConsoleApplication38.Program.Main(String [] арг)

Если вы удалите этот недействительный вызов, вызовы производных классов отлично работают без каких-либо ошибок. CLR может разрешить базовый метод из вызова производного типа. Однако интерфейсы не имеют истинного представления во время выполнения, и среда CLR не может разрешить вызов метода из расширенного интерфейса.

В теории, компилятор С# мог бы выдать вызов непосредственно правильному классу, указанному во время выполнения. Это позволит избежать проблем с вызовами средних классов, как показано на блоге Эрика Липперта. Однако, как показано, это невозможно для интерфейсов.

Вернемся к DLR. Он решает метод точно так же, как и CLR. Мы видели, что IExtendedInterface.Store не может быть разрешен CLR. DLR тоже не может! Это полностью скрывается из-за того, что компилятор С# будет выдавать правильный вызов, поэтому всегда будьте осторожны при использовании dynamic, если вы не знаете, как это работает в среде CLR.