4

我一直在研究 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”行),而通过动态变量的相同调用会引发异常。在这种情况下,我们可以认为通过动态变量调用是安全且可预测的吗?是否存在动态调用产生与其强类型对应物大不相同的结果的任何其他情况?

4

1 回答 1

6

dynamic只是object戴着一顶花哨的帽子,告诉编译器在运行时生成类型检查。这为我们提供了以下基本规则之一dynamic

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

您不能通过调用初始化object变量ref something;你必须将它分配给一个ref something变量。

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

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

更一般地说:这是现代语言设计有多困难的一个很好的例子。让像“引用返回”这样的新功能与过去十年中添加到语言中的每个现有功能一起工作可能非常非常困难。而且,当您添加一个新功能(例如“动态”)时,很难知道添加将来添加的所有功能会导致什么问题。

是否存在动态调用产生与其强类型对应物大不相同的结果的任何其他情况?

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

还有一个细节不合适。可能我错过了一些东西。refValXTuple.Item1Ref.Item1示例中的表达式如何正常工作?它也没有为ref变量分配任何东西。

出色的抓地力。让我解释。

正如您所注意到的,“引用返回”是 C# 7 的一项新功能,但ref自 C# 1.0 以来一直以三种方式出现。一个你意识到了,两个你可能不知道。

您意识到的方式是,您当然可以将参数传递给ref或形式参数;这会为作为参数传递的变量创建一个别名,因此形式和参数指的是同一个变量。outrefout

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

第二种方式是this值类型上的方法的调用是 aref。这就是您可以在可变值类型中改变呼叫接收者的方式!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它是对引用类型的完全普通对象的引用。

它返回什么?当我们完成后,引用被弹出,并且 aref ValXTuple<String>被推送。

好的,我们需要设置调用Item1什么?这是对值类型成员的调用,所以我们需要ref ValXTuple<String>堆栈上的 a 并且......我们有一个!哈利路亚,编译器不必在这里做任何额外的工作来履行其ref在调用之前将 a 放入堆栈的义务。

所以这就是为什么它有效。此时您需要 aref在堆栈上,并且您有一个

把它们放在一起;假设 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是一个value,而且,它必须可以存储在 an 中,object以便我们可以.Item1对该对象进行动态调用。但在运行时,结果证明它不是一个对象,而且,它甚至不是我们可以转换为object. 您可以装箱的值类型,但值类型的引用变量不是可装箱的东西。

于 2019-04-29T21:19:46.433 回答