C# 不可变地传递集合

C# 不可变地传递集合,c#,C#,我正在对一堆代码进行一次大的重构,这些代码使用了一堆不断调整大小的多维数组。我创建了一个数据对象来替换2D数组,现在我正在传递这些数组的列表 我发现了一些让我有点担心的事情。假设我有一些代码如下所示: List<NCPoint> basePoints = new List<NCPoint>(); // ... snip populating basePoints with starting data List<NCPoint> newPoints = Tr

我正在对一堆代码进行一次大的重构,这些代码使用了一堆不断调整大小的多维数组。我创建了一个数据对象来替换2D数组,现在我正在传递这些数组的列表

我发现了一些让我有点担心的事情。假设我有一些代码如下所示:

List<NCPoint> basePoints = new List<NCPoint>();

// ... snip populating basePoints with starting data

List<NCPoint> newPoints = TransformPoints(basePoints, 1, 2, 3);

public List<NCPoint> TransformPoints(List<NCPoint> points, int foo, int bar, int baz){
    foreach(NCPoint p in points){
        points.X += foo
        points.Y += bar
        points.Z += baz
    }

    return points;
}
var p = points[i];
var newPoint = p.WithX(p.X + foo)
                .WithY(p.Y + bar)
                .WithZ(p.Z + baz);
列表基点=新列表();
// ... 用起始数据填充基点
列出新点=转换点(基点1、2、3);
公共列表点(列表点、int-foo、int-bar、int-baz){
foreach(以点为单位的NCP点){
点X+=foo
点Y+=巴
点Z+=baz
}
返回点;
}
这样做的目的是保留一个原始点列表(
基点)和一个更新点列表(
新点
)。但是C#通过引用传递列表,就像任何对象一样。这将更新
基点
,因此现在
基点
新点
将具有相同的数据


目前,在处理数据之前,我正在努力小心地制作传入的
列表的完整副本。这是确保函数内对象的更改不会在函数外产生副作用的唯一合理方法吗?是否有类似于使用
常量传递对象的内容?

您可能正在搜索

提供泛型只读集合的基类

例如:

public IEnumerable<..> GetReadonlyCollection(List<...> originalList) 
{
  return new ReadOnlyCollection<string>(originalList);
}
public IEnumerable GetReadonlyCollection(列表原始列表)
{
返回新的只读集合(原始列表);
}
请注意一个事实:这是为使只读(不可变)集合成为集合而不包含类型而提供的服务。我可以从该集合中获取一个对象并对其进行更改,如果该对象是引用类型,则这些更改也会在原始集合中进行反射

如果您想拥有只读对象,这将变得有点棘手(取决于对象的复杂程度)。其基本思想是(Servy也建议)在原始对象上使用只读公共成员制作包装器(因此对于该类型的使用者,它是不可变的)

希望这有帮助。

简言之:没有

C#本身没有
const
引用的概念。如果要使对象不可变,必须显式地对其进行编码,或者利用其他“技巧”

您可以通过多种方式使集合不可变(
readonlycollection
,返回迭代器,返回浅层副本),但这只保护序列,而不保护存储在其中的数据

因此,您真正需要做的是返回深度拷贝或投影,可能使用LINQ:

public IEnumerable<NCPoint> TransformPoints(List<NCPoint> points, int foo, int bar, int baz)
{
    // returning an iterator over the sequence so original list won't be changed
    // and creating new NCPoint using old NCPoint + modifications so old points
    // aren't altered.
    return points.Select(p => new NCPoint
        { 
           X = p.X + foo,
           Y = p.Y + bar,
           Z = p.Z + baz
        });
}
public IEnumerable转换点(列表点、int-foo、int-bar、int-baz)
{
//在序列上返回迭代器,以便不会更改原始列表
//并使用旧的NCPoint+修改创建新的NCPoint
//没有改变。
返回点。选择(p=>newncpoint
{ 
X=p.X+foo,
Y=p.Y+bar,
Z=p.Z+baz
});
}
此外,返回迭代器的美妙之处(与仅将
列表
返回为
IEnumerable等不同)在于它不能转换回原始集合类型

更新:或者用.NET 2.0的说法:

public IEnumerable<NCPoint> TransformPoints(List<NCPoint> points, int foo, int bar, int baz)
{
    // returning an iterator over the sequence so original list won't be changed
    // and creating new NCPoint using old NCPoint + modifications so old points
    // aren't altered.
    NCPoint[] result = new NCPoint[points.Count];

    for (int i=0; i<points.Count; ++i)
    { 
        // if you have a "copy constructor", can use it here.
        result[i] = new NCPoint();
        result[i].X = points[i].X + foo;
        result[i].Y = points[i].Y + bar;
        result[i].Z = points[i].Z + baz;
    }

    return result;
}
public IEnumerable转换点(列表点、int-foo、int-bar、int-baz)
{
//在序列上返回迭代器,以便不会更改原始列表
//并使用旧的NCPoint+修改创建新的NCPoint
//没有改变。
NCPoint[]结果=新的NCPoint[points.Count];
对于(inti=0;i如果您使用的是C#4.5,我建议您看看不可变集合,它可以通过Nuget软件包下载

PM> Install-Package Microsoft.Bcl.Immutable
这篇博文很好地解释了如何使用它们:

要保护封闭对象,必须将所有属性设置为私有。 我喜欢使用“
”和“…”
模式来帮助构建对象

    public class NCPoint
    {
        public int X { get; private set; }
        public int Y { get; private set; }
        public int Z { get; private set; }

        public NCPoint(int x, int y, int z)
        {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public NCPoint WithX (int x)
        {
            return x == X ? this : new NCPoint(x, Y, Z);
        }

        public NCPoint WithY(int y)
        {
            return y == Y ? this : new NCPoint(X, y, Z);
        }

        public NCPoint WithZ(int z)
        {
            return z == Z ? this : new NCPoint(X, Y, z);
        }
    }
您可以这样使用它:

List<NCPoint> basePoints = new List<NCPoint>();

// ... snip populating basePoints with starting data

List<NCPoint> newPoints = TransformPoints(basePoints, 1, 2, 3);

public List<NCPoint> TransformPoints(List<NCPoint> points, int foo, int bar, int baz){
    foreach(NCPoint p in points){
        points.X += foo
        points.Y += bar
        points.Z += baz
    }

    return points;
}
var p = points[i];
var newPoint = p.WithX(p.X + foo)
                .WithY(p.Y + bar)
                .WithZ(p.Z + baz);
我知道这个解决方案不是每个人都喜欢的,因为它需要大量的编码。但我觉得它相当优雅

最后,调用不可变列表的Replace方法:


immutablePoints.Replace(p,newPoint)他想修改对象,但让它们返回副本,ReadOnlyCollection不允许他这样做,它只保护列表本身。这里的基本原则可以应用于在任何可变变量上生成不可变的包装器,即使用包装器类或带有所提供函数有限子集的接口选择性(即读而不写).+1以澄清在封闭类型上不提供不变性。我正在用它做一个测试,当我仍然能够编辑内容的属性时,我感到有点困惑。看起来除了制作一个可以安全变异的列表的显式副本之外,我不能完全做我想做的事情。这仍然是一个有用的答案,如果不是最终答案的话直接适用。@KChaloux:如果不进行克隆也是一种解决方案,您可以创建一个包装器对象,当然。在某个时候,如果您希望有一个新的、不同的集合,但仍然保留旧集合,则需要创建列表项的副本。您可能可以让其他人进行该副本,但在某个地方会实现宁。我有点担心这是真的。很高兴至少得到了确认。不幸的是,我被困在.NET2.0中,所以我没有喜欢的LINQ=(.Foreach循环应该这样做。
NCPoint
对象有一个深度副本构造函数。@KChaloux:Ah,在任何情况下,同样的原则都适用,返回一个新的
列表
,然后使用深度副本,然后修改副本。