繁体   English   中英

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

[英]Why dynamic call on ref return property throws exception?

我一直在研究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#参考 ),但找不到任何可以证明这种行为的理由。 我很感激你的帮助。

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

dynamic就是一个带有花哨帽子的object ,它告诉编译器在运行时生成类型检查。 这给了我们dynamic的基本规则之一:

如果您无法在某个位置使用object ,则您也无法在该位置使用dynamic

您无法使用ref something调用初始化object变量; 你必须将它分配给ref something变量。

更具体地说: dynamic是为您与动态对象模型进行互操作的场景而设计的,而您对性能的关注很少,您愿意在运行时再次启动编译器。 “参考回报”是针对严格的类型安全场景而设计的,在这些场景中,您非常关注性能,而您愿意做一些危险的事情,比如将变量本身作为值传递。

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

更一般地说:这是现代语言设计有多么困难的一个很好的例子。 将“ref return”这样的新功能与前十年中添加到该语言的每个现有功能配合使用可能非常非常困难。 而当你添加新的功能,如“动态”很难知道会出现什么问题导致当你添加你打算在未来加入的功能。

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

当然。 例如,由于dynamicobject ,并且由于没有“盒装可空值类型”这样的东西,当你有一个T?时,你可以遇到奇怪的情况T? 并将其转换为dynamic 你不能再调用.Value ,因为它不再是T? 它是nullT

还有一个不适合的细节。 可能我错过了一些东西。 如何从样本中的表达式refValXTuple.Item1Ref.Item1工作得很好? 它也没有为ref变量赋值。

很棒的捕获。 让我解释。

正如您所注意到的,“ref returns”是C#7的一个新功能,但是自从C#1.0以来, ref以三种方式存在。 一个你意识到的,还有两个你可能不知道的。

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

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

第二种方法是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 它是对完全普通的引用类型对象的引用。

它返回了什么? 完成后,将弹出引用,并按下ref ValXTuple<String>

好的,我们需要设置对Item1的调用吗? 它是对值类型成员的调用,因此我们需要在堆栈上使用ref ValXTuple<String>并且......我们有一个! Hallelujah,编译器不必在此处做任何额外的工作来履行在调用之前将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变量。

如果你想看到一些可怕的东西,请看一下你动态表达的真实生成的IL; 它比我在这里列出的要复杂得多,但在道德上相当。


我只是想到了另一种简明扼要的表达方式。 考虑:

refValXTuple.Item1Ref.Item1

此表达式的refValXTuple.Item1Ref分类为变量,而不是值,因为它是对变量的ref ; 这是别名。 .Item1要求接收器必须是一个变量 - 因为Item1可能(奇怪!)改变变量,所以我们手头有一个变量是好的。

相比之下,与

dynXTuple.Item1Ref.Item1

子表达式dynXTuple.Item1Ref是一个 ,而且是一个必须可以存储在object ,以便我们可以在该对象上动态调用.Item1 但是在运行时它结果不是一个对象,而且,甚至不是我们可以转换为object任何东西。 您可以使用的值类型,但是ref-to-value-value-type不是一个盒装的东西。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM