[英]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?
例如,由于
dynamic
是object
,并且由于没有“盒装可空值类型”这样的东西,当你有一个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
. 它是
null
或T
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 toref
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; 你意识到的方式当然是你可以将
ref
或out
参数传递给ref
或out
参数; 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.