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

Posted

技术标签:

【中文标题】为啥通过引用传递数组元素会显式导致 IL 中的赋值操作?【英文标题】:Why does passing elements of an array by reference explicitly cause assignment operations in IL?为什么通过引用传递数组元素会显式导致 IL 中的赋值操作? 【发布时间】:2021-12-23 08:22:03 【问题描述】:

我创建了以下 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) 中的赋值。

我问为什么会发生这种情况,因为一位开发人员要求我查看他们在涉及自定义代码的特定工作流程中遇到的错误。当源代码似乎只对集合执行 get 操作时,在对其进行迭代时正在修改集合。我确定错误是由显式使用 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 吗?他们不都是参考吗?任何可以帮助我理解这里起作用的机制的信息都将不胜感激。

【问题讨论】:

FWIW,枚举一个列表,然后将所述列表批发传递给循环内的被调用者(而不是每个枚举项)似乎有些做作。另外,你为什么要用ref 来装饰,除非目的是(可能)修改传递的对象? 为了清楚起见,C# 不允许dotnetfiddle.net/Jv1cF7,这是一个特定的 VB 问题,因为它允许 ByRef 转换,因此必须将它们编组到变量或从变量编组 请注意,当任何属性与ByRef 参数一起使用时,可能会发生通过copy-in/copy-out 传递。该属性在这里是隐含的,因为它是索引器(VB 中的Item 属性,oList(0) 扩展为oList.Item(0) [并且索引属性是 C# 不支持的另一个 VB 特定的东西,除了特定情况.NET 支持的索引器])。通常,It Just Works (tm),但当它失败时可能很难追踪。 另见:***.com/questions/52193306/… 【参考方案1】:

让我们看看VB.Net specification 看看发生了什么:

9.2.5.2 参考参数

参考参数以两种模式起作用,作为别名或通过copy-in copy-back

别名。当参数充当调用者提供的参数的别名时,使用引用参数。引用参数本身并不定义变量,而是引用相应参数的变量。引用参数的修改直接并立即影响相应的参数

Copy-in copy-back. 如果传递给引用参数的变量的类型与引用参数的类型不兼容,或者如果非变量(例如属性)是作为参数传递给引用参数,或者如果调用是后期绑定的,则分配一个临时变量并将其传递给引用参数。传入的值将在调用方法之前复制到此临时变量中,并在方法返回时复制回原始变量(如果有并且可写)。因此,引用参数可能不一定包含对传入变量的确切存储的引用,并且在方法退出之前,对引用参数的任何更改都可能不会反映在变量中。

所以,由于存储位置oList根据CLR规则与ref object不兼容(因为它可能导致插入非ArrayList对象),没有办法让编译器直接传递位置。

所以它使用了copy-in copy-back。

方法返回时会发生什么?

如果新对象不兼容,方法完成后会出现异常

当从F返回时(上一个示例),临时变量中的值被强制转换回变量的类型Derived,并赋值给d。由于传回的值无法转换为Derived,因此在运行时会引发异常。


为了清楚起见,C# 根本不允许这样做,您可以在 this fiddle 中看到。这是一个特定的 VB 问题,因为它允许 ByRef 进行 copy-in copy-back 转换。

【讨论】:

以上是关于为啥通过引用传递数组元素会显式导致 IL 中的赋值操作?的主要内容,如果未能解决你的问题,请参考以下文章

ref关键字的用法

为啥不能用赋值语句将一个字符串常量直接赋给一个字符数组

在 C# 中传递数组参数:为啥它是通过引用隐式传递的?

C++C++的拷贝控制

[PHP] foreach循环的引用赋值可能导致的问题

合并两个有序数组