为什么通过引用传递数组元素会显式导致 IL 中的赋值操作?

Why does passing elements of an array by reference explicitly cause assignment operations in IL?

我创建了以下 SSCCE:

Module Module1

Sub Main()
    Dim oList As ArrayList = New ArrayList()
    oList.Add(New Object())
    For Each o As Object In oList
        subA(oList)
    Next

End Sub

Private Sub subA(ByRef oList As ArrayList)
    subB(oList(0))
End Sub

Private Sub subB(ByRef oObj As Object)
    oObj.ToString()
End Sub

End Module

此代码编译为以下 IL:

[StandardModule]
internal sealed class Module1
{
[STAThread]
public static void Main()
{
    ArrayList oList = new ArrayList();
    oList.Add(RuntimeHelpers.GetObjectValue(new object()));
    IEnumerator enumerator = default(IEnumerator);
    try
    {
        enumerator = oList.GetEnumerator();
        while (enumerator.MoveNext())
        {
            object o = RuntimeHelpers.GetObjectValue(enumerator.Current);
            subA(ref oList);
        }
    }
    finally
    {
        if (enumerator is IDisposable)
        {
            (enumerator as IDisposable).Dispose();
        }
    }
}

private static void subA(ref ArrayList oList)
{
    ArrayList obj = oList;
    object oObj = RuntimeHelpers.GetObjectValue(obj[0]);
    subB(ref oObj);
    obj[0] = RuntimeHelpers.GetObjectValue(oObj);
}

private static void subB(ref object oObj)
{
    oObj.ToString();
}
}

记下发生在 subA(ArrayList) 中的赋值。

我问为什么会这样,因为一位开发人员要求我查看他们在涉及自定义代码的特定工作流程中遇到的错误。当源代码似乎只对集合执行获取操作时,在迭代集合时正在修改集合。我确定错误是由显式使用 byref 引入的,实际上,如果我从方法签名中删除 byref 关键字,生成的 IL 如下所示:

[StandardModule]
internal sealed class Module1
{
    [STAThread]
    public static void Main()
    {
        ArrayList oList = new ArrayList();
        oList.Add(RuntimeHelpers.GetObjectValue(new object()));
        IEnumerator enumerator = default(IEnumerator);
        try
        {
            enumerator = oList.GetEnumerator();
            while (enumerator.MoveNext())
            {
                object o = RuntimeHelpers.GetObjectValue(enumerator.Current);
                subA(ref oList);
            }
        }
        finally
        {
            if (enumerator is IDisposable)
            {
                (enumerator as IDisposable).Dispose();
            }
        }
    }

    private static void subA(ref ArrayList oList)
    {
        subB(RuntimeHelpers.GetObjectValue(oList[0]));
    }

    private static void subB(object oObj)
    {
        oObj.ToString();
    }
}

请注意,现在没有分配。我并不完全理解这种行为,但对于开发人员来说,这似乎是一个痛苦的陷阱,在我的情况下显然是这样。有人可以详细说明 IL 以这种方式生成的原因吗?考虑到我专门传递引用类型,原始源代码的这两个变体不应该编译成相同的 IL 吗?他们不都是ref吗?任何能帮助我理解这里发挥作用的机制的信息都将不胜感激。

我们来看看VB.Net specification,看看是怎么回事:

9.2.5.2 Reference Parameters

Reference parameters act in two modes, either as aliases or through copy-in copy-back.

Aliases. A reference parameter is used when the parameter acts as an alias for a caller-provided argument. A reference parameter does not itself define a variable, but rather refers to the variable of the corresponding argument. Modifications of a reference parameter directly and immediately impact the corresponding argument

Copy-in copy-back. If the type of the variable being passed to a reference parameter is not compatible with the reference parameter's type, or if a non-variable (e.g. a property) is passed as an argument to a reference parameter, or if the invocation is late-bound, then a temporary variable is allocated and passed to the reference parameter. The value being passed in will be copied into this temporary variable before the method is invoked and will be copied back to the original variable (if there is one and if it's writable) when the method returns. Thus, a reference parameter may not necessarily contain a reference to the exact storage of the variable being passed in, and any changes to the reference parameter may not be reflected in the variable until the method exits.

所以,因为 存储位置 oList 根据 CLR 规则与 ref object 不兼容(因为它可能会导致非 ArrayList object to be inserted), 编译器无法直接传递位置。

所以它使用了拷贝入拷贝回。

方法returns时会发生什么?

如果新对象不兼容,你会得到一个异常方法完成后

When returning from F (previous example), the value in the temporary variable is cast back to the type of the variable, Derived, and assigned to d. Since the value being passed back cannot be cast to Derived, an exception is thrown at run time.


为清楚起见,C# 根本不允许这样做,如您在 this fiddle 中所见。这是一个特定的 VB 问题,因为它允许 ByRef 进行拷贝入拷贝回转换。