简体   繁体   English

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

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

I have been looking into c# 7 ref return feature and came across unexpected scenario when running one of test snippets. 我一直在研究c#7 ref return功能,并在运行其中一个测试片段时遇到了意外情况。

The following code: 以下代码:

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);
            }
        }
    }
}   

gives the following printout: 给出以下打印输出:

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

which is somewhat unexpected. 这有些出乎意料。 I would expect to see the following line in the printout instead of exception: 我希望在打印输出中看到以下行而不是异常:

Print 4: True

Exception is thrown when property which returns ref is called through dynamic variable. 当通过动态变量调用返回ref的属性时抛出异常。 I have spent some time looking for the answer (eg here C# Reference ) but could not find anything which could justify such behavior. 我花了一些时间寻找答案(例如这里的C#参考 ),但找不到任何可以证明这种行为的理由。 I would appreciate your help on this. 我很感激你的帮助。

It is clear that call via strong typed variable works just fine ("Print 3" line) whereas the same call via dynamic variable throws an exception. 很明显,通过强类型变量调用工作得很好(“打印3”行),而通过动态变量调用同样会引发异常。 Can we consider calls via dynamic variable safe and predictable in this circumstances? 在这种情况下,我们可以考虑通过动态变量安全和可预测的呼叫吗? Are there any other scenario where dynamic calls produce far different results then their strong typed counterparts? 还有其他情况,动态调用产生的结果与强类型对应的结果大不相同吗?

dynamic is just object with a fancy hat on it that tells the compiler to generate type checks at run time. dynamic就是一个带有花哨帽子的object ,它告诉编译器在运行时生成类型检查。 This gives us one of the fundamental rules of dynamic : 这给了我们dynamic的基本规则之一:

If you cannot use object in a location, then you cannot use dynamic in that location either. 如果您无法在某个位置使用object ,则您也无法在该位置使用dynamic

You can't initialize an object variable with a ref something call; 您无法使用ref something调用初始化object变量; you have to assign it to a ref something variable. 你必须将它分配给ref something变量。

More specifically: dynamic is designed for scenarios where you're interoperating with dynamic object models, and you care so little about performance that you're willing to start the compiler again at runtime. 更具体地说: dynamic是为您与动态对象模型进行互操作的场景而设计的,而您对性能的关注很少,您愿意在运行时再次启动编译器。 "Ref returns" are designed for strictly typesafe scenarios where you care so much about performance that you're willing to do something dangerous like passing around variables themselves as values. “参考回报”是针对严格的类型安全场景而设计的,在这些场景中,您非常关注性能,而您愿意做一些危险的事情,比如将变量本身作为值传递。

They're scenarios that have opposite use cases; 它们是具有相反用例的场景; do not try to use them together. 不要试图一起使用它们。

More generally: this is a great example of how difficult modern language design is. 更一般地说:这是现代语言设计有多么困难的一个很好的例子。 It can be very, very difficult to make a new feature like "ref returns" work well with every existing feature added to the language in the previous decade . 将“ref return”这样的新功能与前十年中添加到该语言的每个现有功能配合使用可能非常非常困难。 And when you add a new feature like "dynamic" it is hard to know what problems that is going to cause when you add all the features you're going to add in the future. 而当你添加新的功能,如“动态”很难知道会出现什么问题导致当你添加你打算在未来加入的功能。

Are there any other scenario where dynamic calls produce far different results then their strong typed counterparts? 还有其他情况,动态调用产生的结果与强类型对应的结果大不相同吗?

Sure. 当然。 For example, since dynamic is object , and since there is no such thing as a "boxed nullable value type", you can run into odd situations when you have a T? 例如,由于dynamicobject ,并且由于没有“盒装可空值类型”这样的东西,当你有一个T?时,你可以遇到奇怪的情况T? and convert it to dynamic . 并将其转换为dynamic You cannot then call .Value on it because it is no longer a T? 你不能再调用.Value ,因为它不再是T? . It's either null or T . 它是nullT

there is still one detail which does not fit. 还有一个不适合的细节。 Probably I'm missing something. 可能我错过了一些东西。 How is that the expression refValXTuple.Item1Ref.Item1 from the sample works just fine? 如何从样本中的表达式refValXTuple.Item1Ref.Item1工作得很好? It does not assign anything to ref variable either. 它也没有为ref变量赋值。

Excellent catch. 很棒的捕获。 Let me explain. 让我解释。

As you note, "ref returns" is a new feature for C# 7, but ref has been around since C# 1.0 in three ways. 正如您所注意到的,“ref returns”是C#7的一个新功能,但是自从C#1.0以来, ref以三种方式存在。 One you realized, and two you might not have known about. 一个你意识到的,还有两个你可能不知道的。

The way you realized was that of course you can pass ref or out arguments to ref or out formal parameters; 你意识到的方式当然是你可以将refout参数传递给refout参数; this creates an alias to the variable passed as the parameter, so the formal and the argument refer to the same variable. 这会为作为参数传递的变量创建别名,因此形式和参数引用相同的变量。

The first way you perhaps might not realize that ref was in the language is actually an example of ref return; 你可能没有意识到ref在语言中的第一种方式实际上是ref return的一个例子; C# will sometimes generate operations on multidimensional arrays by calling helper methods that return a ref into the array. C#有时会通过调用将ref返回到数组的辅助方法在多维数组上生成操作。 But there is no "user visible" surface to this in the language. 但是在语言中没有“用户可见”的表面。

The second way is the this of a call to a method on a value type is a ref . 第二种方法是this对值类型的方法的呼叫的是一个ref That's how you can mutate the receiver of a call in a mutable value type! 这就是你如何以可变值类型改变调用的接收者! this is an alias for the variable which contains the call. this是包含调用的变量的别名。

So now let's look at your call site. 现在让我们来看看你的通话网站。 We'll simplify it: 我们将简化它:

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

OK, what's going to happen at the IL level here? 好的,IL级别会发生什么? At a high level we need: 在高层次上,我们需要:

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

What are we going to do to compute the left side of the equality? 我们打算如何计算平等的左侧?

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

What's the receiver? 什么是接收器? It's a reference. 这是一个参考。 Not a ref . 不是ref It's a reference to a perfectly ordinary object of reference type. 它是对完全普通的引用类型对象的引用。

What does it return? 它返回了什么? When we are done, the reference is popped , and a ref ValXTuple<String> is pushed. 完成后,将弹出引用,并按下ref ValXTuple<String>

OK, what do we need to set up the call to Item1 ? 好的,我们需要设置对Item1的调用吗? It's a call to a member of a value type, so we'll need a ref ValXTuple<String> on the stack and... we have one! 它是对值类型成员的调用,因此我们需要在堆栈上使用ref ValXTuple<String>并且......我们有一个! Hallelujah, the compiler doesn't have to do any additional work here to meet its obligation to put a ref on the stack before the call. Hallelujah,编译器不必在此处做任何额外的工作来履行在调用之前将ref放入堆栈的义务。

So that's why this works. 这就是为什么这样做的原因。 You need a ref on the stack at this point and you have one . 此时你需要在堆栈上有一个 ref ,你有一个

Put it all together; 把它们放在一起; suppose loc.0 contains a reference to our RefXTuple. 假设loc.0包含对我们的RefXTuple的引用。 The IL is: 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.

Now compare that to the dynamic case. 现在将其与动态案例进行比较。 When you say 当你说

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

That basically does the moral equivalent of: 这基本上与道德相当:

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);

As you can see, it is nothing but calls to helpers and assignments to object variables. 正如您所看到的,它只是调用帮助程序和分配object变量。

If you want to see something horrifying, take a look at the real generated IL for your dynamic expression; 如果你想看到一些可怕的东西,请看一下你动态表达的真实生成的IL; it is a lot more complex than I've laid out here, but morally equivalent. 它比我在这里列出的要复杂得多,但在道德上相当。


I just thought of another way to express this concisely. 我只是想到了另一种简明扼要的表达方式。 Consider: 考虑:

refValXTuple.Item1Ref.Item1

The refValXTuple.Item1Ref of this expression is classified as a variable, not a value because it is a ref to a variable; 此表达式的refValXTuple.Item1Ref分类为变量,而不是值,因为它是对变量的ref ; it's an alias. 这是别名。 .Item1 requires that the receiver must be a variable -- because Item1 might (bizarrely!) mutate the variable, and so it's good that we have a variable in hand. .Item1要求接收器必须是一个变量 - 因为Item1可能(奇怪!)改变变量,所以我们手头有一个变量是好的。

By contrast, with 相比之下,与

dynXTuple.Item1Ref.Item1

the subexpression dynXTuple.Item1Ref is a value , and moreover, one that must be storable in an object so that we can do a dynamic invocation of .Item1 on that object. 子表达式dynXTuple.Item1Ref是一个 ,而且是一个必须可以存储在object ,以便我们可以在该对象上动态调用.Item1 But at runtime it turns out to not be an object, and moreover, is not even anything we can convert to object . 但是在运行时它结果不是一个对象,而且,甚至不是我们可以转换为object任何东西。 A value type you can box, but a ref-to-variable-of-value-type is not a boxable thing. 您可以使用的值类型,但是ref-to-value-value-type不是一个盒装的东西。

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

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