为什么对ref return属性的动态调用会引发异常?

时间:2019-04-29 20:22:16

标签: c# dynamic-keyword

我一直在研究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),但找不到任何可以证明这种行为合理的东西。非常感谢您的帮助。

很明显,通过强类型变量的调用工作得很好(“打印3”行),而通过动态变量进行的同一调用将引发异常。在这种情况下,我们能否考虑通过动态变量进行的调用安全且可预测?在其他情况下,动态调用所产生的结果与其强类型对应会产生很大不同吗?

1 个答案:

答案 0 :(得分:6)

dynamic只是object,上面有花哨的帽子,它告诉编译器在运行时生成类型检查。这为我们提供了dynamic的基本规则之一:

如果您不能在某个位置使用object,那么您也不能在该位置使用dynamic

您无法通过object调用来初始化ref something变量;您必须将其分配给ref something变量。

更具体地说:dynamic是为与动态对象模型进行互操作而又不关心性能的情况而设计的,因此您愿意在运行时再次启动编译器。 “引用返回”是为严格类型安全的方案而设计的,在这种情况下,您非常关心性能,因此您愿意做一些危险的事情,例如将变量本身作为值传递。

它们是具有相反用例的场景;不要尝试一起使用它们。

更笼统地说:这是现代语言设计有多么困难的一个很好的例子。要使“引用返回”之类的新功能与过去十年中添加到该语言的所有现有功能一起正常工作可能非常非常困难。而且,当您添加“动态”之类的新功能时,很难知道将来添加所有将要添加的功能时将导致哪些问题。

  

在其他情况下,动态调用所产生的结果与强类型对应结果会大不相同吗?

好的。例如,由于dynamicobject,并且由于没有“装箱的可为空的值类型”之类的东西,所以当您拥有T?并将其转换时,可能会遇到奇怪的情况到dynamic。然后,您无法再对其调用.Value,因为它不再是T?。是nullT

  

还有一个细节不合适。可能我缺少了一些东西。样本中的表达式refValXTuple.Item1Ref.Item1如何正常工作?它也不会为ref变量分配任何内容。

极好的捕获。让我解释一下。

您注意到,“ ref return”是C#7的新功能,但是ref从C#1.0开始就以三种方式出现。你意识到一个,而你可能不知道的两个。

您意识到的方式是,您当然可以将refout参数传递给refout形式参数;这样会为作为参数传递的变量创建一个别名,因此形式变量和参数都引用相同的变量。

您可能没有意识到ref是该语言的第一种方式实际上是ref return的示例; C#有时会通过调用将引用返回到数组的辅助方法来对多维数组生成操作。但是这种语言没有“用户可见”的表面。

第二种方法是对值类型的方法的调用thisref 。这样便可以将可变值类型的呼叫的接收者变为突变! 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是对引用类型完全普通的对象的引用。

它返回什么?完成后,引用被弹出 ,并且推送了ref ValXTuple<String>

好的,我们需要建立对Item1的呼叫吗?这是对值类型成员的调用,因此我们在堆栈上需要一个ref ValXTuple<String>,并且...我们有一个! Hallelujah,编译器无需在此处执行任何其他工作即可履行其在调用之前将ref放在堆栈中的义务。

所以这就是为什么这样。此时,您需要在堆栈上放置ref,并且您有一个{em>。

将所有内容放在一起;假设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可能(非常!)会对该变量进行突变,因此手头有一个变量是很好的。 / p>

与之相反,与

dynXTuple.Item1Ref.Item1

子表达式dynXTuple.Item1Ref是一个,此外,子表达式必须存储在object中,以便我们可以动态调用.Item1在那个对象上。但是在运行时,结果证明它不是对象,而且甚至不是我们可以转换为object的任何东西。可以装箱的值类型,但对值的引用变量不是可装箱的东西。