我试图了解这个想法,将公共方法的只读对象列表发布的最佳方式是什么??
从Eric Lippert's Blog开始,数组有点糟糕,因为有人可以轻松添加新条目。因此,每次调用该方法时都必须传递一个新数组。
他建议,要传递IEnumerable<T>
,因为这是每个定义只读(没有添加,删除方法),我练习了很长一段时间。
但是在我们的新项目中,人们甚至开始创建这些IEnumerables
的数组,因为他们不知道背后的数据源,所以他们得到了:Handling warning for possible multiple enumeration of IEnumerable
我对技术方法感兴趣,如何解决这个难题。到目前为止,我提出的唯一解决方案是使用IReadOnlyCollection
,但这比IEnumerable
更明确。
发布此类列表的最佳做法是什么,这些列表不应更改,但应声明为内存列表?
答案 0 :(得分:14)
通常 - 并且一段时间后 - 使用immutable collections解决了这个问题。
例如,您的公共属性应为IImmutableList<T>
,IImmutableHashSet<T>
类型等。
任何IEnumerable<T>
都可以转换为不可变集合:
someEnumerable.ToImmutableList();
someEnumerable.ToImmutableHashSet();
这样,您可以使用可变集合处理私有属性,并仅提供不可变集合的公共表面。
例如:
public class A
{
private List<string> StringListInternal { get; set; } = new List<string>();
public IImmutableList<string> StringList => StringListInternal.ToImmutableList();
}
还有一种使用接口的替代方法:
public interface IReadOnlyA
{
IImmutableList<string> StringList { get; }
}
public class A : IReadOnlyA
{
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList => StringList.ToImmutableList();
}
检查IReadOnlyA
has been explicitly-implemented,因此可变和不可变StringList
属性可以作为同一类的一部分共存。
当您想要公开不可变A
时,您会将A
个对象追溯到IReadOnlyA
,而上层将无法改变整个StringList
在上面的示例中:
public IReadOnlyA DoStuff()
{
return new A();
}
IReadOnlyA a = DoStuff();
// OK! IReadOnly.StringList is IImmutableList<string>
IImmutableList<string> stringList = a.StringList;
每次访问不可变列表时,应该避免将源列表转换为不可变列表。
如果项目类型覆盖Object.Equals
和GetHashCode
,并且可选地实现IEquatable<T>
,则公共不可变列表属性访问权限可能如下所示:
public class A : IReadOnlyA
{
private IImmutableList<string> _immutableStringList;
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList
{
get
{
// An intersection will verify that the entire immutable list
// contains the exact same elements and count of mutable list
if(_immutableStringList.Intersect(StringList).Count == StringList.Count)
return _immutableStringList;
else
{
// the intersection demonstrated that mutable and
// immutable list have different counts, thus, a new
// immutable list must be created again
_immutableStringList = StringList.ToImmutableList();
return _immutableStringList;
}
}
}
}
答案 1 :(得分:6)
我认为不可改变的方式
int[] source = new int[10000000];//uses 40MB of memory
var imm1 = source.ToImmutableArray();//uses another 40MB
var imm2 = source.ToImmutableArray();//uses another 40MB
列表的行为方式相同。如果我想每次都进行完整复制,我不必关心用户对该数组的处理方式。使其不可变不会保护集合中对象的内容,它们可以自由更改。 @HansPassant建议似乎是最好的
public class A
{
protected List<int> list = new List<int>(Enumerable.Range(1, 10000000));
public IReadOnlyList<int> GetList
{
get { return list; }
}
}
答案 2 :(得分:5)
对于您不打算修改的集合,IEnumerable<T>
仍然可能是最安全的选项,而且它允许任何集合类型,而不仅仅是数组。发出警告的原因是IEnumerable
表示使用延迟执行的查询的可能性,这意味着可能会多次执行可能很昂贵的操作。
请注意,没有一个接口可以区分内存中的集合与可能的延迟执行包装器。 That question has been asked before
。如果代码需要执行多次迭代(可能是合法的),那么在迭代之前将集合水化为List<T>
。
答案 3 :(得分:5)
IEnumerable
是一个只读界面,隐藏了用户的实现。有人可以争辩说,用户可以投射IEnumerable
来列出并添加新项目,但这意味着两件事:
IEnumerable
描述了行为,而List
是该行为的实现。当您使用IEnumerable
时,您可以让编译器有机会将工作推迟到以后,可能会在此过程中进行优化。如果使用ToList()
,则强制编译器立即准备结果。
我使用IEnumerable
每当使用LINQ表达式时,因为仅通过指定行为,我给LINQ提供了推迟评估并可能优化程序的机会。
为了防止对List<T>
对象进行任何修改,只能通过不公开修改集合的方法的ReadOnlyCollection<T>
包装器公开它。但是,如果对基础List<T>
对象进行了更改,则只读集合会反映MSDN中所述的更改。
答案 4 :(得分:0)
我编程的时间比我记忆中的时间长,而且从来没有这样的要求来保护列表不被修改。我并不是说这不是一个可能的要求,我只是说很少需要这样的要求。如果您的应用中有一个列表,那么很可能您必须修复您的设计。如果您需要帮助,请告诉我们您如何使用该列表。
您在问题和评论中提供的示例并不是何时需要不可变或只读列表的好例子。让我们一一讨论。
您提到将其作为API发布。根据定义,您从API返回的任何内容都不再属于您,您不应该关心它的使用方式。事实上,一旦它离开你的API,它现在就在API客户端的域中,他们可以用它做任何他们想做的事情。除了一旦它在域中你无法保护它的事实,你不应该决定他们将如何使用它。更重要的是,您不应该在API中接受任何输入,即使它与您之前返回的列表相同,并且您认为您受到保护。所有输入都必须进行适当的验证。
也许,你并不是指API,而是更像你在项目中共享的DLL库。无论是DLL还是项目中的类,都适用相同的原则。当您返回某些内容时,由用户决定如何使用它。而且你不应该在没有验证的情况下接受同样的事情(列表或其他)。同样,您不应该公开您班级的内部成员,无论是列表还是单个值。毕竟,这是使用属性而不是将成员标记为公共的全部理念。为单值成员创建属性时,将返回该值的副本。同样,您应该为列表创建一个属性并返回一个副本,而不是列表本身。
如果您确实需要一个可全局访问的列表,并且您只想加载一次,请将其公开给其他类,保护它不被修改,而且它太大而无法复制它。您可以查看一些设计,例如将其包装在Singleton中和/或按照Antonín Lejsek's answer将其设置为只读。事实上,由于C#中简化的Singleton实现,他的答案可以通过将构造函数标记为私有来轻松转换为Singleton。