.NET中是否可以使用不可变数组?

时间:2008-10-16 21:49:16

标签: c# .net arrays immutability

是否有可能以某种方式将System.Array标记为不可变。当置于public-get / private-set之后,它们无法添加,因为它需要重新分配和重新分配,但消费者仍然可以设置他们希望的任何下标:

public class Immy
{
    public string[] { get; private set; }
}

我认为readonly关键字可以解决问题,但没有这样的运气。

10 个答案:

答案 0 :(得分:28)

Framework Design Guidelines建议返回数组的副本。这样,消费者就无法从阵列中更改项目。

// bad code
// could still do Path.InvalidPathChars[0] = 'A';
public sealed class Path {
   public static readonly char[] InvalidPathChars = 
      { '\"', '<', '>', '|' };
}

这些更好:

public static ReadOnlyCollection<char> GetInvalidPathChars(){
   return Array.AsReadOnly(InvalidPathChars);
}

public static char[] GetInvalidPathChars(){
   return (char[])InvalidPathChars.Clone();
}

这些例子直接来自本书。

答案 1 :(得分:26)

ReadOnlyCollection<T>可能就是你要找的东西。它没有Add()方法。

答案 2 :(得分:13)

请参阅基类库中的 Immutable Collections Now Available (目前处于预览状态)。

答案 3 :(得分:9)

您可以使用Array.AsReadOnly方法返回。

答案 4 :(得分:2)

我认为最佳做法是使用IList&lt; T&gt;而不是公共API中的数组出于这个原因。 readonly 会阻止在构造函数之外设置成员变量,但正如您所发现的那样,不会阻止人们在数组中分配元素。

有关详细信息,请参阅Arrays Considered Somewhat Harmful

编辑:数组不能只读,但可以通过@shahkalpesh指出的Array.AsReadOnly()转换为只读的IList实现。

答案 5 :(得分:1)

除了最简单和最传统的用例之外,.NET倾向于远离数组。对于其他一切,有各种可枚举/集合实现。

当您想将一组数据标记为不可变时,您将超越传统数组提供的功能。 .NET提供了相同的功能,但技术上并不是数组的形式。要从数组中获取不可变集合,请使用Array.AsReadOnly<T>

var mutable = new[]
{
    'a', 'A',
    'b', 'B',
    'c', 'C',
};

var immutable = Array.AsReadOnly(mutable);

immutable将是ReadOnlyCollection<char>个实例。作为更一般的用例,您可以从任何通用ReadOnlyCollection<T>实现创建IList<T>

var immutable = new ReadOnlyCollection<char>(new List<char>(mutable));

请注意,它必须是通用实现;普通的旧IList将不起作用,这意味着您不能在传统的数组上使用此方法,而传统的数组只实现IList。这揭示了使用Array.AsReadOnly<T>作为快速获取通常无法通过传统阵列访问的通用实现的方法的可能性。

ReadOnlyCollection<T>将允许您访问您期望从不可变数组中获得的所有功能:

// Note that .NET favors Count over Length; all but traditional arrays use Count:
for (var i = 0; i < immutable.Count; i++)
{
    // this[] { get } is present, as ReadOnlyCollection<T> implements IList<T>:
    var element = immutable[i]; // Works

    // this[] { set } has to be present, as it is required by IList<T>, but it
    // will throw a NotSupportedException:
    immutable[i] = element; // Exception!
}

// ReadOnlyCollection<T> implements IEnumerable<T>, of course:
foreach (var character in immutable)
{
}

// LINQ works fine; idem
var lowercase =
    from c in immutable
    where c >= 'a' && c <= 'z'
    select c;

// You can always evaluate IEnumerable<T> implementations to arrays with LINQ:
var mutableCopy = immutable.ToArray();
// mutableCopy is: new[] { 'a', 'A', 'b', 'B', 'c', 'C' }
var lowercaseArray = lowercase.ToArray();
// lowercaseArray is: new[] { 'a', 'b', 'c' }

答案 6 :(得分:0)

唯一要添加的是Arrays 暗示可变性。当您从函数返回一个数组时,您建议客户端程序员可以/应该更改内容。

答案 7 :(得分:0)

继Matt的回答之后,IList是一个完整的数组抽象接口,所以它允许添加,删除等。我不确定为什么Lippert似乎建议它作为IEnumerable的替代品,需要不变性。 (编辑:因为IList实现可以为这些变异方法抛出异常,如果你喜欢这种东西的话)。

也许要记住的另一件事是列表中的项目也可能具有可变状态。如果你真的不希望调用者修改这种状态,你有一些选择:

确保列表中的项是不可变的(如示例所示:string是不可变的)。

返回所有内容的深层克隆,因此在这种情况下,无论如何都可以使用数组。

返回一个可以只读访问项目的界面:

interface IImmutable
{
    public string ValuableCustomerData { get; }
}

class Mutable, IImmutable
{
    public string ValuableCustomerData { get; set; }
}

public class Immy
{
    private List<Mutable> _mutableList = new List<Mutable>();

    public IEnumerable<IImmutable> ImmutableItems
    {
        get { return _mutableList.Cast<IMutable>(); }
    }
}

请注意,可从IImmutable接口访问的每个值本身必须是不可变的(例如字符串),或者是您即时创建的副本。

答案 8 :(得分:0)

您可以希望做的最好的事情是扩展现有的集合以构建您自己的集合。最大的问题是它必须以不同于每个现有集合类型的方式工作,因为每次调用都必须返回一个新集合。

答案 9 :(得分:0)

您可能需要查看my answer类似的问题,以获取有关在对象上公开集合的更多提示。