是否有可能以某种方式将 a 标记System.Array
为不可变。当放在 public-get/private-set 后面时,它们不能被添加,因为它需要重新分配和重新分配,但消费者仍然可以设置他们希望的任何下标:
public class Immy
{
public string[] { get; private set; }
}
我认为readonly
关键字可能会起作用,但没有这样的运气。
是否有可能以某种方式将 a 标记System.Array
为不可变。当放在 public-get/private-set 后面时,它们不能被添加,因为它需要重新分配和重新分配,但消费者仍然可以设置他们希望的任何下标:
public class Immy
{
public string[] { get; private set; }
}
我认为readonly
关键字可能会起作用,但没有这样的运气。
框架设计指南建议返回 Array的副本。这样,消费者就无法更改数组中的项目。
// 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();
}
这些例子直接来自书中。
ReadOnlyCollection<T>
可能是您正在寻找的。它没有Add()
方法。
请参阅基类库中现在可用的不可变集合(当前处于预览状态)。
您可以使用 Array.AsReadOnly 方法返回。
出于这个确切原因,我相信最佳实践是在公共 API 中使用 IList<T> 而不是数组。readonly将阻止在构造函数之外设置成员变量,但正如您所发现的,不会阻止人们在数组中分配元素。
有关更多信息,请参阅被认为有些有害的数组。
编辑:数组不能是只读的,但可以通过 Array.AsReadOnly() 将它们转换为只读的 IList 实现,正如@shahkalpesh 指出的那样。
除了最简单和最传统的用例之外,.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));
请注意,它必须是通用实现;plain oldIList
不起作用,这意味着您不能在传统数组上使用此方法,该数组仅实现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' }
唯一要补充的是数组意味着可变性。当你从一个函数返回一个数组时,你是在向客户端程序员建议他们可以/应该改变一些事情。
除了马特的回答,IList 是一个完整的数组抽象接口,因此它允许添加、删除等。我不确定为什么 Lippert 似乎建议它作为需要不变性的 IEnumerable 的替代方案。(编辑:因为 IList 实现可以为那些变异方法抛出异常,如果你喜欢那种东西)。
也许另一件事要记住,列表中的项目也可能具有可变状态。如果你真的不希望调用者修改这种状态,你有一些选择:
确保列表中的项目是不可变的(如您的示例中:字符串是不可变的)。
返回所有内容的深层克隆,因此在这种情况下,您无论如何都可以使用数组。
返回一个提供对项目的只读访问权限的接口:
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 接口访问的每个值本身必须是不可变的(例如字符串),或者是您即时创建的副本。
您希望做的最好的事情就是扩展现有的集合来构建您自己的集合。最大的问题是它的工作方式必须与每个现有集合类型不同,因为每次调用都必须返回一个新集合。