在处理只读List 成员时,我应该如何使用属性
当我想在我的课外制作一个只读的值类型时,我这样做:
public class myClassInt { private int m_i; public int i { get { return m_i; } } public myClassInt(int i) { m_i = i; } }
我可以做些什么来使List
类型只读(因此它们不能在我的类之外添加/删除元素)? 现在我只是宣布公开:
public class myClassList { public List li; public myClassList() { li = new List(); li.Add(1); li.Add(2); li.Add(3); } }
您可以将其公开为AsReadOnly 。 也就是说,返回一个只读的IList
包装器。 例如 …
public ReadOnlyCollection List { get { return _lst.AsReadOnly(); } }
只返回IEnumerable
是不够的。 例如 …
void Main() { var el = new ExposeList(); var lst = el.ListEnumerator; var oops = (IList)lst; oops.Add( 4 ); // mutates list var rol = el.ReadOnly; var oops2 = (IList )rol; oops2.Add( 5 ); // raises exception } class ExposeList { private List _lst = new List () { 1, 2, 3 }; public IEnumerable ListEnumerator { get { return _lst; } } public ReadOnlyCollection ReadOnly { get { return _lst.AsReadOnly(); } } }
史蒂夫的回答也有一个聪明的方法来避免演员。
试图隐藏信息到这种程度是有限的。 该属性的类型应该告诉用户他们可以使用它做什么。 如果用户决定滥用您的API,他们会找到一种方法。 阻止它们进行铸造并不会阻止它们:
public static class Circumventions { public static IList AsWritable (this IEnumerable source) { return source.GetType() .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Select(f => f.GetValue(source)) .OfType>() .First(); } }
通过这种方法,我们可以绕过迄今为止在这个问题上给出的三个答案:
List a = new List {1, 2, 3, 4, 5}; IList b = a.AsReadOnly(); // block modification... IList c = b.AsWritable(); // ... but unblock it again c.Add(6); Debug.Assert(a.Count == 6); // we've modified the original IEnumerable d = a.Select(x => x); // okay, try this... IList e = d.AsWritable(); // no, can still get round it e.Add(7); Debug.Assert(a.Count == 7); // modified original again
也:
public static class AlexeyR { public static IEnumerable AsReallyReadOnly (this IEnumerable source) { foreach (T t in source) yield return t; } } IEnumerable f = a.AsReallyReadOnly(); // really? IList g = f.AsWritable(); // apparently not! g.Add(8); Debug.Assert(a.Count == 8); // modified original again
重申……这种“军备竞赛”只要你喜欢就可以继续下去!
停止此操作的唯一方法是完全断开与源列表的链接,这意味着您必须制作原始列表的完整副本。 这是BCL返回数组时的作用。 这样做的缺点是,每当他们希望只读取一些数据时,你就会对99.9%的用户施加潜在的巨大成本,因为你担心00.1%的用户会有这种麻烦。
或者你可以拒绝支持绕过静态类型系统的API的使用。
如果您希望属性返回具有随机访问权限的只读列表,请返回实现的内容:
public interface IReadOnlyList : IEnumerable { int Count { get; } T this[int index] { get; } }
如果(更常见)它只需要按顺序枚举,只需返回IEnumerable
:
public class MyClassList { private List li = new List { 1, 2, 3 }; public IEnumerable MyList { get { return li; } } }
更新自从我写了这个答案后,C#4.0问世了,所以上面的IReadOnlyList
接口可以利用协方差:
public interface IReadOnlyList
现在.NET 4.5已经到了,它已……猜猜……
IReadOnlyList接口
因此,如果您想创建一个具有包含只读列表的属性的自我记录API,答案就在框架中。
JP关于返回IEnumerable
的答案是正确的(您可以向下转换为列表),但这是一种阻止向下转换的技术。
class ExposeList { private List _lst = new List () { 1, 2, 3 }; public IEnumerable ListEnumerator { get { return _lst.Select(x => x); } // Identity transformation. } public ReadOnlyCollection ReadOnly { get { return _lst.AsReadOnly(); } } }
枚举期间的标识转换有效地创建了一个编译器生成的迭代器 – 一种与_lst
无关的新类型。
Eric Lippert在他的博客上发表了一系列关于Immutability In C#的文章。
该系列的第一篇文章可以在这里找到。
您也可以找到有用的Jon Skeet对类似问题的回答 。
public List li;
不要声明公共字段,它通常被认为是不好的做法…而是将其包装在属性中。
您可以将集合公开为ReadOnlyCollection:
private List li; public ReadOnlyCollection List { get { return li.AsReadOnly(); } }
public class MyClassList { private List _lst = new List () { 1, 2, 3 }; public IEnumerable ListEnumerator { get { return _lst.AsReadOnly(); } } }
检查一下
MyClassList myClassList = new MyClassList(); var lst= (IList)myClassList.ListEnumerator ; lst.Add(4); //At this point ypu will get exception Collection is read-only.
public static IEnumerable AsReallyReadOnly (this IEnumerable source) { foreach (T t in source) yield return t; }
如果我加入Earwicker的例子
... IEnumerable f = a.AsReallyReadOnly(); IList g = f.AsWritable(); // finally can't get around it g.Add(8); Debug.Assert(a.Count == 78);
我得到InvalidOperationException: Sequence contains no matching element
。