我一直在研究 c# 7ref 返回功能,并在运行其中一个测试片段时遇到了意外情况。
以下代码:
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:RepoTestBed.LibFeaturesReturnRefByDynamic.cs:line 52
这有点出乎意料。我希望在打印输出中看到以下行而不是异常:
Print 4: True
当通过动态变量调用返回 ref 的属性时,将引发异常。我花了一些时间寻找答案(例如,此处的 C# 参考),但找不到任何可以证明这种行为合理的内容。我将不胜感激你在这方面的帮助。
很明显,通过强类型变量调用工作正常("Print 3"行),而通过动态变量的相同调用会引发异常。在这种情况下,我们是否可以认为通过动态变量进行的调用是安全和可预测的?是否存在动态调用产生与强类型对应项截然不同的结果的其他情况?
dynamic
只是object
,上面有一个花哨的帽子,告诉编译器在运行时生成类型检查。 这给了我们dynamic
的基本规则之一:
如果不能在某个位置使用object
,则也不能在该位置使用dynamic
。
不能使用ref something
调用初始化object
变量;必须将其分配给ref something
变量。
更具体地说:dynamic
专为以下方案而设计:您与动态对象模型进行互操作,并且您非常不关心性能,以至于您愿意在运行时再次启动编译器。 "Ref 返回"专为严格的类型安全方案而设计,在这些方案中,您非常关心性能,以至于愿意做一些危险的事情,例如将变量本身作为值传递。
它们是具有相反用例的方案;不要尝试将它们一起使用。
更一般地说:这是一个很好的例子,说明现代语言设计是多么困难。要使像"ref returns"这样的新功能与过去十年添加到该语言中的每个现有功能很好地协同工作可能非常非常困难。当您添加像"动态"这样的新功能时,很难知道当您添加将来要添加的所有功能时会导致什么问题。
是否存在动态调用产生与强类型对应项截然不同的结果的其他情况?
确定。例如,由于dynamic
是object
,并且由于没有"盒装可为空值类型"这样的东西,因此当您有T?
并将其转换为dynamic
时,您可能会遇到奇怪的情况。然后,您无法调用.Value
,因为它不再是T?
。要么是null
,要么是T
.
还有一个细节不合适。可能我错过了什么。示例中
refValXTuple.Item1Ref.Item1
表达式如何正常工作?它也不会为变量分配任何东西ref
。
优秀的捕获。让我解释一下。
如您所知,"ref returns"是 C# 7 的一项新功能,但自 C# 1.0 以来,ref
以三种方式存在。一个你意识到了,两个你可能不知道。
你意识到的方式是,当然你可以将ref
或out
参数传递给ref
或out
形式参数;这会为作为参数传递的变量创建一个别名,因此形式参数和参数引用同一个变量。
您可能没有意识到ref
在语言中的第一种方法实际上是 ref 返回的一个例子;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-variable-of-value-type 不是可装箱的东西。