何时使用包含引用类型的值类型数组而不是引用类型数组?

6
假设我有以下内容:
public class MyElement
{
}

[Serializable]
[StructLayout(LayoutKind.Sequential)]
struct ArrayElement
{
    internal MyElement Element;
}

public class MyClass
{
    internal MyElement ComputeElement(int index)
    {
        // This method does a lengthy computation.
        // The actual return value is not so simple.
        return new MyElement();
    }

    internal MyElement GetChild(ref MyElement element, int index)
    {
        if (element != null)
        {
            return element;
        }

        var elem = ComputeElement(index);
        if (Interlocked.CompareExchange(ref element, elem, null) != null)
        {
            elem = element;
        }

        return elem;
    }
}

public class MyClassA : MyClass
{
    readonly MyElement[] children = new MyElement[10];

    public MyElement GetChild(int index)
    {
        return GetChild(ref children[index], index);
    }
}

public class MyClassB : MyClass
{
    readonly ArrayElement[] children = new ArrayElement[10];

    public MyElement GetChild(int index)
    {
        return GetChild(ref children[index].Element, index);
    }
}

在什么情况下使用 MyClassBMyClassA 更有优势?

如果数组元素包含一个 MyElement 数组,则可以更好地格式化您的 xml 序列化。 - Sayse
你在哪里找到那段代码的? - CodesInChaos
2个回答

12

为了澄清usr的答案,他的回答是正确的但有点简略:

C#支持一项特性——我认为是"C#中最糟糕的特性"——称为数组类型协变。也就是说,如果你有一个海龟数组,你可以将它赋给一个类型为“动物数组”的变量:

class Animal {}
class Turtle : Animal {}
...
Animal[] animals = new Turtle[10];

这是"协变性(covariance)",因为数组的赋值兼容规则 和其元素的赋值兼容规则方向相同:

Turtle --> Animal
Turtle[] --> Animal[]

这个功能不是类型安全的,因为...

animals[0] = new Giraffe();

我们刚刚将一个长颈鹿放入了一个实际上是乌龟数组的数组中。编译器无法确定这里是否违反了类型安全性——长颈鹿是一种动物——因此检查必须由运行时执行。

为防止此类情况在运行时发生,运行时会在每次将长颈鹿放入动物数组中以检查它是否真正是乌龟数组。但事实上几乎从不是如此。但这个检查需要时间,因此该功能有效地减缓了每次成功的数组访问

不安全的数组协变仅适用于其元素类型为引用类型的数组。它不适用于值类型。(这是一个小谎言;CLR允许您将int[]强制转换为object,然后将object强制转换为uint[]。但通常,协变不适用于值类型。)

因此,您可以通过使数组实际上成为值类型数组来节省检查费用,其中值类型只是对引用的包装器。数组的大小不受影响,但对其的访问将稍微更快。

除非您有实证证据表明这样做实际上可以解决实际性能问题,否则不应使用这些疯狂的技巧。需要优化的情况相当少,但在一些地方,此类技巧可能会产生影响。

我注意到您也可以通过封装乌龟类型并然后使用一个乌龟数组来避免检查成本。运行时将推断出数组类型不能真正更加派生,因为那样的话其元素类型将派生自一个已封装的类型,而封装后的类型是不可能派生的。


3

ArrayElement 是一个包装器,它允许JIT生成更好的代码。.NET数组具有运行时类型检查,因为它们在所有方面都不是静态类型安全的。

var array = new Stream[10];
((object[])array)[0] = "somestring"; //runtime exception

使用包装器后,不再需要类型检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接