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

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

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's 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?

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

37
задан Matt Warren 18 June 2010 в 17:11
поделиться

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)

Здесь мы видим, что компилятор C # всегда генерирует вызовы для интерфейса или класса, в котором определен метод. IActualInterface имеет слот метода для Store, поэтому он используется для actualTest.Store . IExtendedInterface этого не делает, поэтому для вызова используется IActualInterface . TestInterface определяет новый метод Store, используя модификатор IL 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)

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

Давайте теперь посмотрим, что мы получим, если сами напишем IL, используя нужный нам тип, вместо того, чтобы позволить компилятору C # выбирать его за нас. Я изменил 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. Однако он не может пройти peverify и вылетает во время выполнения со следующей ошибкой:

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

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

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

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

86
ответ дан 27 November 2019 в 04:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: