从 List<T> 中获取 T 的某个属性的最佳方法

8

我有一个 List<Users> - Users 有一个 Username 属性。

我想知道的是 - 是否有更好的方法来获取所有用户名的 List<string>,而不仅仅是通过循环并构建我的新列表?


1
你说的“更好的方式”是什么意思?你可以使用LINQ,但这也会在底层循环。 - Grzenio
5个回答

23

使用 LINQ:

List<string> usernames = users.Select(u => u.UserName).ToList();

与SLaks的答案相比,除非“users”只有4个或更少的项目,否则这种方法是不必要的浪费。 - Jon Hanna

12

像这样:

List<string> userNames = users.ConvertAll(u => u.UserName);

请注意,userNames 列表不会反映对 users 或其 UserName 的后续更改。


5
如果你确实需要一个列表,那么使用LINQ方法可能是最好的选择(尽管创建一个具有适当容量的新列表并添加元素可能会稍微提高速度,但不太可能有显著的改善)。
编辑:如果您要这样做,请使用ConvertAll而不是Select后跟ToList,尤其是如果您的列表可能很大。 ConvertAll预先分配正确大小,随着源列表的大小而影响性能的重要性也越来越大。
如果您想要一个类似于已完成此操作的只读IList,那么您可以从转换列表类中获得更好的性能。
public class ConvertingList<TSrc, TDest> : IList<TDest>
{
  private readonly IList<TSrc> _inner;
  private readonly Func<TSrc, TDest> _conv;
  public ConvertingList(IList<TSrc> inner, Func<TSrc, TDest> conv)
  {
      _inner = inner;
      _conv = conv;
  }
  public TDest this[int index]
  {
      get
      {
          return ReferenceEquals(null, _inner[index]) ? default(TDest) : _conv(_inner[index]);
      }
      set
      {
        throw new NotSupportedException("Readonly collection");
      }
  }
  public int Count
  {
      get
      {
        return _inner.Count;
      }
  }
  public bool IsReadOnly
  {
      get
      {
        return true;
      }
  }
  public int IndexOf(TDest item)
  {
      if(ReferenceEquals(item, null))
      {
        for(int i = 0; i != Count; ++i)
          if(ReferenceEquals(this[i], null))
            return i;
      }
      else
      {
        for(int i = 0; i != Count; ++i)
          if(item.Equals(this[i]))
            return i;
      }
      return -1;
  }
  public void Insert(int index, TDest item)
  {
      throw new NotSupportedException("Readonly collection");
  }
  public void RemoveAt(int index)
  {
      throw new NotSupportedException("Readonly collection");
  }
  public void Add(TDest item)
  {
      throw new NotSupportedException("Readonly collection");
  }
  public void Clear()
  {
      throw new NotSupportedException("Readonly collection");
  }
  public bool Contains(TDest item)
  {
      return IndexOf(item) != -1;
  }
  public void CopyTo(TDest[] array, int arrayIndex)
  {
      if(array == null)
        throw new ArgumentNullException();
        if(arrayIndex < 0)
            throw new ArgumentOutOfRangeException();
        if(array.Rank != 1 || array.Length < arrayIndex + Count)
            throw new ArgumentException();
        foreach(TDest item in this)
          array[arrayIndex++] = item;
  }
  public bool Remove(TDest item)
  {
      throw new NotSupportedException("Readonly collection");
  }
  public IEnumerator<TDest> GetEnumerator()
  {
      foreach(TSrc srcItem in _inner)
        yield return ReferenceEquals(null,srcItem) ? default(TDest) : _conv(srcItem)
  }
  IEnumerator IEnumerable.GetEnumerator()
  {
      return GetEnumerator();
  }
}

有了这个,那么:

IList<string> userNames = new ConvertingList<User, string>(users, u => u.Username);

将在常数时间内创建一个新对象,该对象表现为名称的只读列表。

(对空用户进行保护,在这里返回空字符串,当然也可以提供其他行为)。


只读列表仅延迟了转换函数的执行。因此,如果您打算使用该列表(您最终可能会使用),则节省的时间将在以后需要。更糟糕的是:如果您希望两次循环列表,则转换函数将一遍又一遍地执行,从而导致列表变慢。说不要使用Select是微观优化,人们不会注意到那额外的10毫秒。此外,Select由Microsoft支持,因此新框架版本中的任何优化也将直接使您的代码受益。 - Arcturus
@Arcturus,只读列表比ConvertAll更好或更差取决于使用情况,这正是我给出两个选项作为我的答案的原因。如果您需要获取大小,然后访问几个索引,它会更好。如果您需要循环两次,那么它就更糟了。如果您需要进行更改,则完全无用。说“一定要”使用ConvertAll而不是Select是微观优化,一旦您习惯以这种方式进行操作并且没有任何副作用,这种优化就变得免费了。 - Jon Hanna
足够正确,但并不是突破性的。因此,请使用您认为更易用的内容。在许多情况下,IEnumerable就已经足够了,所以在使用Select时,ToList也就变得无意义了。如果您只需要使用Count并且命中了一些索引,那么最好直接从原始列表中检索它们。引入一个全新的列表,也意味着需要编写相应的测试用例来支持它。 - Arcturus
@Arcturus,如果仅需要一个简单的“Select”来完成工作,那么它就能胜过这里提到的所有内容。当需要列表时,“ConvertAll”胜过“Select”后跟“List”,而上面的类和“ConvertAll”在不同的用例中相互胜过。通过显式代码完全避免lambda比它们都要好,但这确实是一种微小的优化,因为它需要更多的编码。 - Jon Hanna

4
var usernames = users.Select(u => u.Username).ToList();

1

你做

List<string> userNames = users.ConvertAll(u => u.UserName);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接