在处理只读List <T>成员时,我应该如何使用属性

Geo*_*rge 10 c# collections readonly

当我想在我的课外制作一个只读的值类型时,我这样做:

public class myClassInt
{
    private int m_i;
    public int i {
        get { return m_i; }
    }

    public myClassInt(int i)
    {
        m_i = i;
    }
}
Run Code Online (Sandbox Code Playgroud)

我可以做些什么来List<T>在我的课堂之外创建一个readonly类型(所以他们不能在其中添加/删除元素)?现在我只是宣布公开:

public class myClassList
{
    public List<int> li;
    public  myClassList()
    {
        li = new List<int>();
        li.Add(1);
        li.Add(2);
        li.Add(3);
    }
}
Run Code Online (Sandbox Code Playgroud)

JP *_*oto 18

您可以将其公开为AsReadOnly.也就是说,返回一个只读IList<T>包装器.例如 ...

public ReadOnlyCollection<int> List
{
    get { return _lst.AsReadOnly(); }
}
Run Code Online (Sandbox Code Playgroud)

刚回来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(); }
  }
}
Run Code Online (Sandbox Code Playgroud)

史蒂夫的回答也有一个聪明的方法来避免演员.


Dan*_*ker 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这一种方法,我们可以绕过迄今为止在这个问题上给出的三个答案:

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
Run Code Online (Sandbox Code Playgroud)

也:

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
Run Code Online (Sandbox Code Playgroud)

重申......这种"军备竞赛"只要你喜欢就可以继续下去!

阻止这种情况的唯一方法是完全断开与源列表的链接,这意味着您必须制作原始列表的完整副本.这是BCL返回数组时的作用.这样做的缺点是,每当他们希望只读取一些数据时,你就会对99.9%的用户施加潜在的巨大成本,因为你担心00.1%的用户会有这种麻烦.

或者你可以拒绝支持绕过静态类型系统的API的使用.

如果您希望属性返回具有随机访问权限的只读列表,请返回实现的内容:

public interface IReadOnlyList<T> : IEnumerable<T>
{
    int Count { get; }
    T this[int index] { get; }
}
Run Code Online (Sandbox Code Playgroud)

如果(更常见)它只需要按顺序枚举,只需返回IEnumerable:

public class MyClassList
{
    private List<int> li = new List<int> { 1, 2, 3 };

    public IEnumerable<int> MyList
    {
        get { return li; }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新自从我写了这个答案后,C#4.0问世了,所以上面的IReadOnlyList界面可以利用协方差:

public interface IReadOnlyList<out T>
Run Code Online (Sandbox Code Playgroud)

现在.NET 4.5已经到了,它已......猜猜......

IReadOnlyList接口

因此,如果您想要创建一个包含只读列表的属性的自我文档API,那么答案就在框架中.

  • 我有点不同意你的逻辑.您可以使用反射调用私有成员,并使用"#define private public"在C++中工作,但您不会认为您不应该再次使用私有内容.接口的要点不是破坏恶意开发人员,而是向开发人员表明接口的正确用法是什么.AsReadOnly做得很好. (6认同)
  • 为什么使用`First(v =>(v as IList <T>)!= null)`?难道你不能使用`First(v => v is IList <T>)`?无论如何,我明白你的观点,但不得不同意JP.此外,保护自己不受简单演员的影响,我认为有时可能是一件好事.但是当人们开始在你的代码上使用反射时,所有的赌注都会消失,所以不需要打扰:p (2认同)

Ste*_*idi 5

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(); }
  }
}
Run Code Online (Sandbox Code Playgroud)

枚举期间的标识转换有效地创建了一个编译器生成的迭代器 - 一种与_lst任何方式无关的新类型.