ToArray() 可以抛出异常吗?

本文关键字:抛出异常 ToArray | 更新日期: 2023-09-27 18:00:12

虽然这个问题的答案非常好,但它意味着你应该把对List.ToArray((的调用括在一个锁中以实现并发。 这篇博文还暗示它可能会灾难性地失败(但很少(。 在枚举列表或其他集合时,我通常使用 ToArray 而不是锁定,以避免"集合已修改,枚举可能无法完成"异常。 这个答案和博客文章对这一假设提出了质疑。

List.ToArray(( 的文档没有列出任何异常,所以我一直假设它将始终完成(尽管可能使用过时的数据(,虽然从数据一致性的角度来看它不是线程安全的,但从代码执行的角度来看它是线程安全的 - 换句话说,它不会引发异常,调用它不会破坏底层集合的内部数据结构。

如果这个假设不正确,那么虽然它从未引起问题,但它可能是高可用性应用程序中的定时炸弹。 最终的答案是什么?

ToArray() 可以抛出异常吗?

由于一个简单的原因,您将找不到有关ToArray方法可能异常的文档。这是一种具有许多"重载"的扩展方法。它们都具有相同的方法签名,但对于不同的集合类型,实现是不同的,例如 List<T>HashSet<T> .

但是,我们可以对大多数代码做出一个安全的假设,即 .NET 框架 BCL 出于性能原因不执行任何锁定。我还非常具体地检查了List<T> ToList的实现。

public T[] ToArray()
{
    T[] array = new T[this._size];
    Array.Copy(this._items, 0, array, 0, this._size);
    return array;
}

正如您可能想象的那样,这是非常简单的代码,最终以mscorlib执行。对于此特定实现,您还可以在 Array.Copy 方法的 MSDN 页中看到可能发生的异常。它归结为一个异常,如果在刚刚分配目标数组后列表的排名立即更改,则会抛出该异常。

请记住,List<T>是一个微不足道的例子,您可以想象在需要更复杂的代码才能存储在数组中的结构上出现异常的可能性会增加。Queue<T>的实现是更有可能失败的候选对象:

public T[] ToArray()
{
    T[] array = new T[this._size];
    if (this._size == 0)
    {
        return array;
    }
    if (this._head < this._tail)
    {
        Array.Copy(this._array, this._head, array, 0, this._size);
    }
    else
    {
        Array.Copy(this._array, this._head, array, 0, this._array.Length - this._head);
        Array.Copy(this._array, 0, array, this._array.Length - this._head, this._tail);
    }
    return array;
}

当文档或原则没有明确保证线程安全时,您不能假设它。如果您确实假设这样做,则可能会将一类无法调试的错误投入生产,并且可能会花费您大量的生产力/可用性/金钱。你愿意冒这个险吗?

你永远无法测试某些东西是线程安全的。你永远无法确定。您无法确定未来版本的行为方式是否相同。

以正确的方式进行操作并锁定。

顺便说一句,这些评论是针对List.ToArray这是ToArray更安全的版本之一。我理解为什么人们会错误地认为它可以与写入列表同时使用。当然IEnumerable.ToArray不可能是线程安全的,因为这是底层序列的属性。

ToArray 不是线程安全的,这段代码证明了这一点!

考虑这个相当荒谬的代码:

        List<int> l = new List<int>();
        for (int i = 1; i < 100; i++)
        {
            l.Add(i);
            l.Add(i * 2);
            l.Add(i * i);
        }
        Thread th = new Thread(new ThreadStart(() =>
        {
            int t=0;
            while (true)
            {
                //Thread.Sleep(200);
                switch (t)
                {
                    case 0:
                        l.Add(t);
                        t = 1;
                        break;
                    case 1:
                        l.RemoveAt(t);
                        t = 0;
                        break;
                }
            }
        }));
        th.Start();
        try
        {
            while (true)
            {
                Array ai = l.ToArray();
                //foreach (object o in ai)
                //{
                //    String str = o.ToString();
                //}
            }
        }
        catch (System.Exception ex)
        {
            String str = ex.ToString();                 
        }
    }

由于l.Add(t)行,此代码将在很短的时间内失败。因为 ToArray 不是线程安全的,它会将数组分配给 l 的当前大小,然后我们将添加一个元素到 l(在另一个线程中(,然后它会尝试将当前大小的 l 复制到 ai 并失败,因为 l 有太多元素。 ToArray抛出ArgumentException.

您似乎混淆了两件事:

  • List 不支持在枚举时进行修改。枚举列表时,枚举器会在每次迭代后检查列表是否已被修改。呼叫列表。枚举列表之前的 ToArray 可以解决此问题,因为您枚举的是列表的快照,而不是列表本身。

  • List 不是线程安全的集合。以上所有内容都假定从同一线程访问。从两个线程访问列表始终需要锁。列表。ToArray 不是线程安全的,在这里没有帮助。

首先,

您必须明确调用站点必须位于线程安全区域中。代码中的大多数区域都不是线程安全区域,并且将假定在任何给定时间执行单个线程(对于大多数应用程序代码(。对于(一个非常粗略的估计(99%的应用程序代码,这个问题没有真正的意义。

其次,您必须明确枚举函数到底是什么,因为这会因您正在运行的枚举类型而异 - 您是在谈论枚举的正常 linq 扩展吗?

第三,您提供的指向 ToArray 代码的链接及其周围的 lock 语句充其量是无稽之谈:如果不显示调用站点也锁定在同一集合上,它不能保证线程安全。

等等。