【Unity优化】Unity中究竟能不能使用foreach?

本文探讨了Unity中使用foreach遍历数据结构时可能导致的GC Alloc问题,通过实验验证了foreach在不同场景下对内存的影响。作者发现泛型List的GetEnumerator不会产生GC Alloc,而Array和ArrayList的foreach以及GetEnumerator操作会。最后,作者提供了一些建议和优化方案,包括避免在Update中频繁使用foreach,并提出了自定义List实现以减少内存开销。

关于这个话题,网络上讨论的很多,我也收集了一些资料,都不是很齐全,所以自己亲自测试,这里把结果分享给大家。

foreach究竟怎么了?

研究过这个问题的人都应该知道,就是它会引起频繁的GC Alloc。也就是说,使用它之后,尤其在Update方法中频繁调用时,会快速产生小块垃圾内存,造成垃圾回收操作的提前到来,造成游戏间歇性的卡顿。
问题大家都知道,也都给出了建议,就是尽可能不要用。在start方法里倒无所谓,因为毕竟它只执行一次。Update方法一秒钟执行大概50-60次,这里就不要使用了。这个观点整体上是正确的,因为这样做毕竟避开了问题。
不过有一点点不是很方便的就是,foreach确实带来了很多便捷性的编码。尤其是结合了var之后,那么我们究竟还能不能使用它,能使用的话,应该注意哪些问题?带着这些问题,我做了以下的测试。

重现GC Alloc问题

首先,我写了一个简单的脚本来重现这个问题。
这个类中包括一个int数组,一个泛型参数为int的List。
代码如下:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ForeachTest : MonoBehaviour {

    int[] m_intArray;
    List<int> m_intList;
    ArrayList m_arryList;
    public void Start () 
    {
        m_intArray = new int[2];
        m_intList = new List<int>();
        m_arryList = new ArrayList();
        for (int i = 0; i < m_intArray.Length; i++)
        {
            m_intArray[i] = i;
            m_intList.Add(i);
            m_arryList.Add(i);
        }
    }

    void Update () 
    {
         testIntListForeach();
    }

    void testIntListForeach()
    {
        for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intList)
            {
            }
        }
    }
}

应用于IntList的foreach

首先我们看应用于泛型List的情况,如下图:

IntList foreach

这里确实是在产生GC Alloc,每帧产生39.1KB的新内存。我使用的Unity版本是64位的5.4.3f1,可能不同的版本产生的内存大小有些差别,但是产生新内存是不可避免的。

应用于IntList的GetEnumerator

接下来,我又做了另外一种尝试,就是用对等的方式写出同样的代码。将测试代码部分改成如下:

        for (int i = 0; i < 1000; i++)
        {
            var iNum = m_intList.GetEnumerator();
            while (iNum.MoveNext())
            {
            }
        }

原本以为,这个结果与上面的方式应该相同。不过结果出乎意料。

IntList GetEnumerator

它并没产生任何的新内存。于是,我准备使用IL反编译器来了解它的GCAlloc是如何产生的。
我们知道,List是动态数组,是可以随时增长、删减的,而int[]这种形式,在C#里面被编译成Array的子类去执行。为了有更多的对比,我将foreach和GetEmulator也写一份同样的代码,应用于Int数组和ArrayList,先查看运行的结果,然后一起查看他们的IL代码。

应用于IntArray的foreach

        for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intArray)
            {
            }
        }

IntArray Foreach

结果是没有产生GC Alloc。

应用于IntArray的GetEnumerator

        for (int i = 0; i < 1000; i++)
        {
            var iNum = m_intArray.GetEnumerator();
            while (iNum.MoveNext())
            {
            }
        }

IntArray GetEnumerator

结果是这里也在产生GC Alloc,每帧产生31.3KB的新内存。

应用于ArrayList的foreach

        for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intArray)
            {
            }
        
<think> 我们正在讨论Unity中的foreach循环。根据引用[1]:“foreach本质是迭代器,迭代器本身就是不能进行修改的”,并且它指出foreach不能修改数组(但这里的问题是关于顺序的)。 引用[5]中提到了for循环正序和倒序的性能比较,但没有直接提到foreach的倒序。 首先,我们需要明确:在C#(Unity使用的语言)中,foreach循环的迭代顺序是由集合的迭代器(IEnumerator)决定的。对于数组和List等集合,foreach默认是正序(从索引0开始到最后一个)迭代的。foreach循环本身不提供直接倒序的机制,因为它的设计就是按迭代器的顺序遍历。 但是,我们可以通过一些方法实现倒序遍历,比如: 1. 反转集合(如果是数组或List,可以使用Reverse方法)然后再用foreach遍历。 2. 使用for循环倒序遍历(这是最常见的方法)。 注意:反转集合可能会产生额外的开销(特别是大集合),所以如果对性能敏感,使用for循环倒序更优。 另外,引用[5]中比较了for正序、倒序和foreach的性能,在数据量很大时,倒序for循环可能比正序for循环稍快(因为比较条件j>-1比j<arr.length更快?但实际差异很小),而foreach在性能上可能略低于for循环(因为foreach涉及迭代器模式)。 因此,针对用户的问题: 问题1:Unityforeach是否只能正序运行? 答:对于标准集合(如数组、List),foreach默认是正序的,并且foreach本身不提供倒序的语法。所以,如果你不采取额外措施,它只能正序运行。 问题2:如何实现倒序运行? 方法1:使用for循环倒序(推荐,因为不会产生额外的反转开销)。 例如:for (int i = array.Length-1; i>=0; i--) 方法2:先反转集合,再用foreach正序遍历(注意:反转操作会改变原集合或创建新集合,有性能开销)。 例如:Array.Reverse(array); 然后 foreach (var item in array) { ... } 或者使用LINQ的Reverse()(会生成一个新的反转序列):foreach (var item in array.Reverse()) 注意:反转原集合会改变其顺序,如果后续还需要原顺序,则需谨慎。使用LINQ的Reverse()不会改变原集合,但它会创建一个新的序列,可能带来内存和性能开销。 另外,根据引用[1]:foreach不能用于修改集合(如删除或添加元素),否则会抛出异常。这点在倒序时同样适用。 下面给出示例代码: 方法一:使用for循环倒序(适用于数组、List等) ```csharp int[] array = new int[] {1, 2, 3, 4, 5}; for (int i = array.Length - 1; i >= 0; i--) { Debug.Log(array[i]); } ``` 方法二:使用反转后再用foreach(以数组为例) ```csharp int[] array = new int[] {1, 2, 3, 4, 5}; Array.Reverse(array); // 反转原数组 foreach (int item in array) { Debug.Log(item); // 此时输出5,4,3,2,1 } // 注意:原数组已经被反转 ``` 方法三:使用LINQ(需要引入System.Linq)不会改变原数组 ```csharp using System.Linq; int[] array = new int[] {1, 2, 3, 4, 5}; foreach (int item in array.Reverse()) { Debug.Log(item); // 输出5,4,3,2,1 } // 原数组保持不变 ``` 总结: 1. foreach本身只能正序,但我们可以通过反转集合来实现倒序遍历。 2. 倒序遍历推荐使用for循环,因为效率更高且不会修改原集合。 3. 如果使用反转方法,注意反转操作对原集合的影响。 最后,根据引用[5]的性能提示,在数据量很大时,优先选择for循环(无论是正序还是倒序)来保证性能。 </think> 在Unity中,`foreach`循环**默认只能正序遍历**集合数据(如数组、List等),因为它遵循底层迭代器的顺序。以下是实现倒序运行的三种解决方案: ### 方案一:使用`for`循环倒序(推荐) ```csharp // 适用于数组、List等支持索引访问的集合 int[] numbers = {1, 2, 3, 4}; // 倒序遍历:从最后一个元素开始递减索引 for (int i = numbers.Length - 1; i >= 0; i--) { Debug.Log(numbers[i]); // 输出:4,3,2,1 } ``` **优势**: - 无额外内存开销(无需反转集合) - 性能高效,尤其适合大型数据集[^5] ### 方案二:反转集合后正序遍历 ```csharp using System.Linq; List<int> numbers = new List<int>{1, 2, 3, 4}; // 反转集合并遍历(产生临时副本) foreach (var num in numbers.AsEnumerable().Reverse()) { Debug.Log(num); // 输出:4,3,2,1 } ``` **注意**: - 需要引入`System.Linq`命名空间 - 反转操作会产生临时副本,增加内存开销 - 不适用于需要同步修改原集合的场景 ### 方案三:自定义反转迭代器 ```csharp // 实现自定义反转迭代器 public static IEnumerable<T> ReverseIterator<T>(IList<T> list) { for (int i = list.Count - 1; i >= 0; i--) { yield return list[i]; // 倒序生成元素 } } // 使用自定义迭代器 foreach (var num in ReverseIterator(numbers)) { Debug.Log(num); // 输出:4,3,2,1 } ``` **适用场景**: - 需要复用倒序遍历逻辑 - 避免频繁反转大型集合 --- ### 关键原理说明 1. **`foreach`的限制**: `foreach`依赖底层迭代器顺序,而.NET标准集合的迭代器默认均为正序[^1]。 2. **性能对比**: - `for`倒序:时间复杂度$O(n)$,空间复杂度$O(1)$ - `Reverse()`反转:时间复杂度$O(n)$,空间复杂度$O(n)$(需临时存储) 大数据量时优选`for`循环[^5]。 3. **修改集合的注意点**: `foreach`遍历中禁止修改集合结构(如增删元素),否则抛出`InvalidOperationException`。需改用`for`循环[^1]。
评论 9
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值