14

当我想在我的班级之外将值类型设为只读时,我会这样做:

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);
    }
}
4

7 回答 7

21

您可以将其公开为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(); }
  }
}

史蒂夫的回答也有一个巧妙的方法来避免演员。

于 2009-08-04T22:55:35.580 回答
11

试图将信息隐藏到这种程度的价值有限。属性的类型应该告诉用户他们可以用它做什么。如果用户决定要滥用您的 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; }
    }
}

UPDATE由于我写了这个答案,C# 4.0 出来了,所以上面的IReadOnlyList接口可以利用协方差:

public interface IReadOnlyList<out T>

现在 .NET 4.5 已经到来,它已经......猜猜是什么......

IReadOnlyList 接口

因此,如果您想创建一个具有包含只读列表的属性的自记录 API,答案就在框架中。

于 2009-08-05T10:15:18.847 回答
6

JP关于返回的答案IEnumerable<int>是正确的(您可以向下转换为列表),但这是一种防止向下转换的技术。

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枚举期间的恒等转换有效地创建了编译器生成的迭代器——一种与任何方式无关的新类型。

于 2009-08-04T23:22:45.477 回答
1

Eric Lippert 在他的博客上有一系列关于 C# 中的不变性的文章。

该系列的第一篇文章可以在这里找到。

您可能还会发现Jon Skeet 对类似问题的回答很有用。

于 2009-08-04T22:55:49.743 回答
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.
于 2009-08-05T09:50:16.293 回答
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

于 2009-08-05T11:03:27.237 回答
0
public List<int> li;

不要声明公共字段,这通常被认为是不好的做法……而是将其包装在属性中。

您可以将您的收藏公开为 ReadOnlyCollection :

private List<int> li;
public ReadOnlyCollection<int> List
{
    get { return li.AsReadOnly(); }
}
于 2009-08-04T23:00:49.390 回答