当我想在我的班级之外将值类型设为只读时,我这样做:
public class myClassInt
{
private int m_i;
public int i {
get { return m_i; }
}
public myClassInt(int i)
{
m_i = i;
}
}
我可以做什么才能使List<T>
类型只读(因此他们不能在我的课程之外添加/删除元素)?现在我只是宣布公开:
public class myClassList
{
public List<int> li;
public myClassList()
{
li = new List<int>();
li.Add(1);
li.Add(2);
li.Add(3);
}
}
答案 0 :(得分:17)
您可以公开AsReadOnly。也就是说,返回一个只读的IList<T>
包装器。例如......
public ReadOnlyCollection<int> List
{
get { return _lst.AsReadOnly(); }
}
仅返回IEnumerable<T>
是不够的。例如......
void Main()
{
var el = new ExposeList();
var lst = el.ListEnumerator;
var oops = (IList<int>)lst;
oops.Add( 4 ); // mutates list
var rol = el.ReadOnly;
var oops2 = (IList<int>)rol;
oops2.Add( 5 ); // raises exception
}
class ExposeList
{
private List<int> _lst = new List<int>() { 1, 2, 3 };
public IEnumerable<int> ListEnumerator
{
get { return _lst; }
}
public ReadOnlyCollection<int> ReadOnly
{
get { return _lst.AsReadOnly(); }
}
}
Steve's answer也有一种聪明的方法来避免演员。
答案 1 :(得分:10)
尝试隐藏信息的程度有限。该属性的类型应告诉用户他们可以使用它做什么。如果用户决定滥用您的API,他们会找到一种方法。阻止他们进行施法并不能阻止他们:
public static class Circumventions
{
public static IList<T> AsWritable<T>(this IEnumerable<T> source)
{
return source.GetType()
.GetFields(BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance)
.Select(f => f.GetValue(source))
.OfType<IList<T>>()
.First();
}
}
通过这种方法,我们可以绕过迄今为止在这个问题上给出的三个答案:
List<int> a = new List<int> {1, 2, 3, 4, 5};
IList<int> b = a.AsReadOnly(); // block modification...
IList<int> c = b.AsWritable(); // ... but unblock it again
c.Add(6);
Debug.Assert(a.Count == 6); // we've modified the original
IEnumerable<int> d = a.Select(x => x); // okay, try this...
IList<int> 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<T> AsReallyReadOnly<T>(this IEnumerable<T> source)
{
foreach (T t in source) yield return t;
}
}
IEnumerable<int> f = a.AsReallyReadOnly(); // really?
IList<int> 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<T> : IEnumerable<T>
{
int Count { get; }
T this[int index] { get; }
}
如果(更常见)它只需要按顺序枚举,只需返回IEnumerable
:
public class MyClassList
{
private List<int> li = new List<int> { 1, 2, 3 };
public IEnumerable<int> MyList
{
get { return li; }
}
}
更新由于我写了这个答案,C#4.0问世了,所以上面的IReadOnlyList
界面可以利用协方差:
public interface IReadOnlyList<out T>
现在.NET 4.5已经到了,它已经猜到了......
因此,如果您想创建一个具有包含只读列表的属性的自我记录API,答案就在框架中。
答案 2 :(得分:5)
IEnumerable<int>
的JP's answer是正确的(您可以向下转换为列表),但这是一种阻止向下转换的技术。
class ExposeList
{
private List<int> _lst = new List<int>() { 1, 2, 3 };
public IEnumerable<int> ListEnumerator
{
get { return _lst.Select(x => x); } // Identity transformation.
}
public ReadOnlyCollection<int> ReadOnly
{
get { return _lst.AsReadOnly(); }
}
}
枚举期间的标识转换有效地创建了一个编译器生成的迭代器 - 一种与_lst
无关的新类型。
答案 3 :(得分:1)
Eric Lippert在他的博客上发表了一系列关于C#Immutability的文章。
系列文章的第一篇文章可以找到here。
您可能还会发现有用的Jon Skeet's answer to a similar question。
答案 4 :(得分:0)
public List<int> li;
不要声明公共字段,它通常被认为是不好的做法......而是将其包装在属性中。
您可以将您的集合公开为ReadOnlyCollection:
private List<int> li;
public ReadOnlyCollection<int> List
{
get { return li.AsReadOnly(); }
}
答案 5 :(得分:0)
public class MyClassList
{
private List<int> _lst = new List<int>() { 1, 2, 3 };
public IEnumerable<int> ListEnumerator
{
get { return _lst.AsReadOnly(); }
}
}
检查
MyClassList myClassList = new MyClassList();
var lst= (IList<int>)myClassList.ListEnumerator ;
lst.Add(4); //At this point ypu will get exception Collection is read-only.
答案 6 :(得分:0)
public static IEnumerable<T> AsReallyReadOnly<T>(this IEnumerable<T> source)
{
foreach (T t in source) yield return t;
}
如果我添加到Earwicker的例子
...
IEnumerable<int> f = a.AsReallyReadOnly();
IList<int> g = f.AsWritable(); // finally can't get around it
g.Add(8);
Debug.Assert(a.Count == 78);
我得到InvalidOperationException: Sequence contains no matching element
。