Почему динамический вызов свойства ref return вызывает исключение? - PullRequest
4 голосов
/ 29 апреля 2019

Я искал функцию c # 7 ref return и столкнулся с неожиданным сценарием при запуске одного из тестовых фрагментов.

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

namespace StackOverflow
{
    using System;

    public interface IXTuple<T>
    {
        T Item1 { get; set; }
    }

    public class RefXTuple<T> : IXTuple<T>
    {
        T _item1;

        public ref T Item1Ref
        {
            get => ref _item1;
        }

        public T Item1
        {
            get => _item1;
            set => _item1 = value;
        }
    }

    public struct ValXTuple<T> : IXTuple<T>
    {
        T _item1;

        public T Item1
        {
            get => _item1;
            set => _item1 = value;
        }
    }

    public class UseXTuple
    {
        public void Experiment1()
        {
            try
            {
                RefXTuple<ValXTuple<String>> refValXTuple = new RefXTuple<ValXTuple<String>> {Item1 = new ValXTuple<String> {Item1 = "B-"}};
                dynamic dynXTuple = refValXTuple;

                refValXTuple.Item1Ref.Item1 += "!";
                Console.WriteLine($"Print 1: {refValXTuple.Item1.Item1 == "B-!"}");
                Console.WriteLine($"Print 2: {dynXTuple.Item1.Item1 == "B-!"}");

                refValXTuple.Item1Ref.Item1 += "!";
                Console.WriteLine($"Print 3: {refValXTuple.Item1Ref.Item1 == "B-!!"}");
                Console.WriteLine($"Print 4: {dynXTuple.Item1Ref.Item1 == "B-!!"}");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}   

выдает следующую распечатку:

Print 1: True
Print 2: True
Print 3: True
System.InvalidCastException: The result type 'StackOverflow.ValXTuple`1[System.String]&' of the dynamic binding produced by binder 'Microsoft.CSharp.RuntimeBinder.CSharpGetMemberBinder' is not compatible with the result type 'System.Object' expected by the call site.
   at System.Dynamic.DynamicMetaObjectBinder.Bind(Object[] args, ReadOnlyCollection`1 parameters, LabelTarget returnLabel)
   at System.Runtime.CompilerServices.CallSiteBinder.BindCore[T](CallSite`1 site, Object[] args)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at StackOverflow.UseXTuple.Experiment1() in C:\Repo\TestBed.Lib\Features\ReturnRefByDynamic.cs:line 52

, что несколько неожиданно.Я ожидаю увидеть следующую строку в распечатке вместо исключения:

Print 4: True

Исключение выдается, когда свойство, которое возвращает ref, вызывается через динамическую переменную.Я потратил некоторое время на поиск ответа (например, здесь C # Reference ), но не смог найти ничего, что могло бы оправдать такое поведение.Буду признателен за помощь в этом.

Ясно, что вызов через строго типизированную переменную работает очень хорошо (строка «Print 3»), тогда как тот же вызов через динамическую переменную вызывает исключение.Можем ли мы считать вызовы через динамическую переменную безопасными и предсказуемыми в этих условиях?Есть ли другой сценарий, когда динамические вызовы дают результаты, отличные от их строго типизированных аналогов?

1 Ответ

6 голосов
/ 30 апреля 2019

dynamic - это просто object с причудливой шляпой, которая говорит компилятору генерировать проверки типов во время выполнения.Это дает нам одно из фундаментальных правил dynamic:

Если вы не можете использовать object в локации, то вы также не можете использовать dynamic в этой локации.

Вы не можете инициализировать переменную object с помощью вызова ref something;Вы должны назначить его переменной ref something.

Более конкретно: dynamic предназначен для сценариев, в которых вы взаимодействуете с динамическими объектными моделями, и вам так мало нужна производительность, что вы готовы снова запустить компилятор во время выполнения.«Ref ref» предназначены для строго типобезопасных сценариев, когда вы так заботитесь о производительности, что готовы сделать что-то опасное, например, передать сами переменные в качестве значений.

Это сценарии с противоположными вариантами использования;не пытайтесь использовать их вместе.

В целом: это отличный пример того, насколько сложен современный дизайн языка.Может быть очень, очень трудно заставить работать новую функцию, такую ​​как «возврат возврата», с каждой существующей функцией, добавленной к языку в предыдущем десятилетии .И когда вы добавляете новую функцию, такую ​​как «динамическая», трудно понять, какие проблемы возникнут при добавлении всех функций, которые вы собираетесь добавить в будущем.

Существуют ли другие сценарии, в которых динамические вызовы дают результаты, значительно отличающиеся от их строго типизированных аналогов?

Конечно.Например, поскольку dynamic - это object, и поскольку не существует такого понятия, как «тип значения в штучной упаковке», вы можете столкнуться с нечетными ситуациями, когда у вас есть T? и преобразовать его в dynamic.Вы не можете тогда позвонить .Value на него, потому что он больше не T?.Это либо null, либо T.

, но есть еще одна деталь, которая не подходит.Возможно, я что-то упустил.Как это выражение refValXTuple.Item1Ref.Item1 из примера работает просто отлично?Она также ничего не присваивает переменной ref.

Отличный улов.Позвольте мне объяснить.

Как вы заметили, «ref return» - это новая функция для C # 7, но ref существует с C # 1.0 тремя способами.Один вы поняли, а два, о котором вы могли не знать.

Вы поняли, что вы, конечно, можете передавать ref или out аргументы в ref или out формальные параметры;это создает псевдоним для переменной, передаваемой в качестве параметра, поэтому формальный и аргумент ссылаются на одну и ту же переменную.

Первый способ, которым вы, возможно, можете не осознавать, что ref был в языке, на самом деле является примеромвозврата реф;C # иногда генерирует операции над многомерными массивами, вызывая вспомогательные методы, которые возвращают ссылку в массив.Но в языке нет «видимой для пользователя» поверхности.

Второй способ - this вызова метода для типа значения: ref,Вот как вы можете изменить вид получателя вызова в изменяемый тип значения!this является псевдонимом для переменной, содержащей вызов.

Итак, теперь давайте посмотрим на ваш сайт вызовов.Мы упростим это:

bool result = refValXTuple.Item1Ref.Item1 == "whatever";

Хорошо, что здесь произойдет на уровне IL?На высоком уровне нам нужно:

push the left side of the equality
push "whatever"
call string equality
store the result in the local

Что мы собираемся сделать, чтобы вычислить левую часть равенства?

put refValXTuple on the stack
call the getter of Item1Ref with the receiver that's on the stack

Что такое получатель?Это ссылка. Не ref. Это ссылка на совершенно обычный объект ссылочного типа.

Что он возвращает?Когда мы закончим, ссылка будет popped , и ref ValXTuple<String> будет нажата.

ОК, что нам нужно, чтобы настроить вызов на Item1?Это вызов члена типа значения, поэтому нам понадобится ref ValXTuple<String> в стеке и ... у нас он есть!Аллилуйя, компилятору здесь не нужно выполнять никакой дополнительной работы, чтобы выполнить свое обязательство поместить ref в стек перед вызовом.

Так вот почему это работает.В этот момент вам нужно ref в стеке , и у вас есть один .

Соберите все вместе;предположим, что loc.0 содержит ссылку на наш RefXTuple.IL:

// the evaluation stack is empty
ldloc.0
// a reference to the refxtuple is on the stack
callvirt instance !0& class StackOverflow.RefXTuple`1<valuetype StackOverflow.ValXTuple`1<string>>::get_Item1Ref()
// a ref valxtuple is on the stack
call instance !0 valuetype StackOverflow.ValXTuple`1<string>::get_Item1()
// a string is on the stack
ldstr "whatever"
// two strings are on the stack
call bool [mscorlib]System.String::op_Equality(string, string)
// a bool is on the stack
stloc.1
// the result is stored in the local and the stack is empty.

Теперь сравните это с динамическим регистром.Когда вы говорите

bool result = dynXTuple.Item1Ref.Item1 == "whatever"

Это в основном соответствует моральному эквиваленту:

object d0 = dynXTuple;
object d1 = dynamic_property_get(d0, "Item1Ref");
object d2 = dynamic_property_get(d1, "Item1");
object d3 = "whatever"
object d4 = dynamic_equality_check(d2, d3);
bool result = dynamic_conversion_to_bool(d4);

Как вы можете видеть, это не что иное, как призывы к помощникам и присвоения object переменных.

Если вы хотите увидеть что-то ужасное, взгляните на сгенерированный real IL для вашего динамического выражения;это намного сложнее, чем я изложил здесь, но морально эквивалентно.


Я просто подумал о другом способе выразить это кратко.Обратите внимание:

refValXTuple.Item1Ref.Item1

refValXTuple.Item1Ref этого выражения классифицируется как переменная, а не значение , потому что это ref для переменной;это псевдоним..Item1 требует, чтобы получатель был переменной - потому что Item1 может (причудливо!) Мутировать переменную, и поэтому хорошо, что у нас есть переменная в руке.

Напротив, с

dynXTuple.Item1Ref.Item1

подвыражение dynXTuple.Item1Ref является значением , и, более того, оно должно быть сохранено в object, чтобы мы могли динамически вызывать.Item1 на этом объекте.Но во время выполнения он оказывается не объектом, и, более того, это даже не то, что мы можем преобразовать в object.Тип значения, который вы можете пометить, но ref-to-variable-of-value-type не является коробкой.

...