Menu

List trong C#: Add, Remove, Contains, Find và Sort

List<T> là mảng có thể co giãn của C#. Tìm hiểu cách tạo list, thêm và chèn phần tử, xóa theo giá trị, chỉ số hoặc điều kiện, tìm kiếm bằng Contains và Find, sắp xếp theo thuộc tính, và tránh lỗi khi thay đổi list bên trong foreach.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

List<T> là một collection có thứ tự, tự co giãn khi bạn thêm và xóa phần tử. Nó nằm trong System.Collections.Generic, truy cập theo chỉ số giống mảng (list[0]), và là lựa chọn mặc định cho "một nhóm thứ gì đó" trong C#.

Tạo list và thêm phần tử

Tham số kiểu trong dấu ngoặc nhọn cho biết list chứa gì: List<string>, List<int>, List<Order>.

Đầu ra:

2
Zoe, Ana, Ben, Chloe, Dev
Ana
Ana B.
3

Count là thuộc tính, không phải phương thức (mảng dùng Length, LINQ có phương thức Count(); với list hãy dùng thuộc tính). Insert đẩy mọi phần tử phía sau lùi một vị trí, nên chèn vào đầu một list lớn tốn thời gian tỷ lệ với kích thước list. Add vào cuối mới là thao tác rẻ.

Xóa phần tử

Có bốn cách xóa, tùy theo điều bạn biết về phần tử:

Đầu ra:

True: 85, 62, 40, 91, 55
False
62, 40, 91, 55
2 removed: 62, 91
Caught ArgumentOutOfRangeException
0

Remove nhận một giá trị và chỉ xóa phần tử khớp đầu tiên. Để xóa mọi bản sao của một giá trị, dùng RemoveAll(s => s == 40). Chỉ số không hợp lệ trên list sẽ ném ArgumentOutOfRangeException (còn mảng ném IndexOutOfRangeException), cả với list[i] lẫn RemoveAt(i). RemoveRange(index, count) xóa một khối liên tiếp.

Count và Capacity

List giữ các phần tử trong một mảng bên trong. Capacity là kích thước của mảng đó; Count là số ô đã dùng. Khi Count chạm Capacity, lần Add tiếp theo sẽ cấp phát một mảng lớn gấp đôi và sao chép mọi thứ sang.

Đầu ra:

Count 0, Capacity 0
Count 1, Capacity 4
Count 2, Capacity 4
Count 3, Capacity 4
Count 4, Capacity 4
Count 5, Capacity 8
Count 6, Capacity 8
Count 7, Capacity 8
Count 8, Capacity 8
Count 9, Capacity 16
Count 0, Capacity 1000

Nhờ nhân đôi, hầu hết lời gọi Add chỉ là ghi vào một ô trống. Nếu bạn biết đại khái sẽ có bao nhiêu phần tử, hãy truyền con số đó cho hàm khởi tạo để bỏ qua các lần sao chép trung gian. Lưu ý new List<int>(1000) vẫn có Count bằng 0: gọi list[0] trên nó sẽ ném ngoại lệ. Capacity là chỗ trống, không phải phần tử.

Tìm kiếm: Contains, IndexOf, Find, Exists

Đầu ra:

True
2
-1
32.00
2 cheap items
False
2
True
False

Find trả về phần tử khớp đầu tiên, hoặc giá trị mặc định của kiểu (null với lớp) khi không có gì khớp, nên hãy kiểm tra trước khi dùng kết quả. Dòng cuối in False vì Contains dùng Equals, và một lớp không ghi đè Equals sẽ so sánh tham chiếu: một Product mới có cùng các trường vẫn là một đối tượng khác. Hãy tìm theo thuộc tính bằng Exists hoặc Find, hoặc ghi đè Equals và GetHashCode trong lớp.

Mọi phương thức này đều duyệt list từ đầu, nên mỗi lần gọi là O(n). Với việc tra cứu lặp lại theo khóa, Dictionary trả lời trong thời gian hằng số.

Sắp xếp list

Sort() sắp xếp tại chỗ theo thứ tự tự nhiên của phần tử. Truyền một lambda so sánh để sắp xếp theo bất cứ tiêu chí nào khác.

Đầu ra:

1, 2, 5, 9
9, 5, 2, 1
Ben 95, Ana 120, Chloe 150
Chloe 150, Ben 95, Ana 120
Ben 95, Ana 120, Chloe 150

Phép so sánh trả về số âm khi a nên đứng trước, số dương khi b nên đứng trước, và 0 khi hai bên bằng nhau; CompareTo cho ra đúng như vậy. Có hai khác biệt giữa Sort và OrderBy của LINQ đáng chú ý trong thực tế:

  • Sort thay đổi list; OrderBy trả về một chuỗi mới và giữ nguyên list.
  • Sort không ổn định: các phần tử so sánh bằng nhau có thể đổi chỗ. OrderBy ổn định, và ThenBy thêm khóa thứ hai: players.OrderBy(p => p.Score).ThenBy(p => p.Name).

Gọi Sort() trên list chứa lớp của bạn mà không truyền phép so sánh sẽ ném InvalidOperationException, vì list không biết cách sắp thứ tự các phần tử. Hãy truyền phép so sánh hoặc cài đặt IComparable<T> cho lớp.

Reverse() đảo ngược tại chỗ. Vì System.Linq cũng định nghĩa extension Reverse, nên list.Reverse() trên một List<T> sẽ chọn phiên bản tại chỗ, phiên bản này không trả về gì.

Duyệt list, và xóa trong lúc duyệt

foreach đọc từng phần tử theo thứ tự. Vòng lặp for cho bạn thêm cả chỉ số. Điều bạn không thể làm là thêm hoặc xóa phần tử giữa chừng một foreach trên chính list đó:

Đầu ra:

Caught InvalidOperationException
120, 80, 60
120, 80, 60

Ngoại lệ là InvalidOperationException với thông báo "Collection was modified; enumeration operation may not execute." Vòng lặp for duyệt xuôi không ném lỗi, nhưng âm thầm bỏ qua phần tử ngay sau mỗi phần tử bạn xóa, vì mọi thứ bị dồn xuống một chỉ số. Duyệt ngược sẽ tránh được điều đó. Cách thứ ba là duyệt trên một bản sao: foreach (var t in orders.ToList()).

Thay đổi thuộc tính của một phần tử bên trong foreach (chẳng hạn order.Status = "sent") thì không sao. Thêm hoặc xóa phần tử làm hỏng enumerator, và thay một phần tử qua indexer (orders[i] = 0) cũng vậy.

Chuyển đổi giữa list và mảng

Đầu ra:

4
3
4, 4, 4, 4
Oslo / Lima / Pune / Kyiv

Cả hai chiều đều sao chép phần tử sang vùng lưu trữ mới, nên thay đổi list về sau không ảnh hưởng đến mảng. ConvertAll là phiên bản riêng của list cho Select(...).ToList() trong LINQ. In một list bằng Console.WriteLine(list) chỉ hiện tên kiểu (System.Collections.Generic.List`1[System.String]); hãy dùng string.Join.

Tra cứu nhanh

Việc cần làmCode
Tạovar l = new List<int>(); hoặc new List<int> { 1, 2 }
Thêm vào cuốil.Add(x), l.AddRange(items)
Chèn vào vị tríl.Insert(i, x)
Xóa phần tử khớp đầu tiênl.Remove(x) (trả về bool)
Xóa theo chỉ sốl.RemoveAt(i)
Xóa theo điều kiệnl.RemoveAll(x => ...) (trả về số lượng)
Số phần tửl.Count
Kiểm tra có chứal.Contains(x), l.Exists(x => ...)
Tìml.Find(...), l.FindAll(...), l.FindIndex(...)
Sắp xếp tại chỗl.Sort(), l.Sort((a, b) => ...)
Bản sao đã sắp xếpl.OrderBy(x => ...).ToList()
Sang mảngl.ToArray()

Lỗi thường gặp

  • Xóa bên trong foreach. Ném InvalidOperationException; hãy dùng RemoveAll hoặc vòng lặp for duyệt ngược.
  • Tưởng Remove(x) xóa mọi bản sao. Nó chỉ xóa bản đầu tiên; hãy dùng RemoveAll.
  • Dùng Contains với đối tượng không có Equals. Nó so sánh tham chiếu; hãy tìm theo thuộc tính bằng Exists.
  • Truy cập theo chỉ số vào list vừa tạo với capacity. new List<int>(10) là list rỗng; hãy thêm phần tử trước.
  • Tưởng Sort giữ thứ tự các phần tử bằng nhau. Không phải vậy; dùng OrderBy khi thứ tự giữa các phần tử bằng nhau quan trọng.
  • Gán list để dùng chung rồi bất ngờ vì nó thay đổi. var copy = list; vẫn là cùng một list; new List<T>(list) mới là bản sao.

Câu hỏi thường gặp

Làm sao thêm phần tử vào List trong C#?

list.Add(item) thêm một phần tử vào cuối, list.AddRange(otherCollection) thêm nhiều phần tử, và list.Insert(index, item) đặt một phần tử vào một vị trí rồi đẩy các phần tử còn lại lùi về sau. Bạn cũng có thể điền list ngay khi tạo: var names = new List<string> { "Ana", "Ben" };.

Làm sao xóa một phần tử khỏi List trong C#?

Remove(value) xóa phần tử khớp đầu tiên và trả về true nếu tìm thấy. RemoveAt(index) xóa theo vị trí. RemoveAll(x => condition) xóa mọi phần tử khớp và trả về số phần tử đã xóa. Clear() làm rỗng list.

Làm sao kiểm tra List có chứa một giá trị trong C#?

list.Contains(value) trả về true hoặc false. Nó so sánh bằng Equals, nên với lớp của bạn nó so sánh tham chiếu, trừ khi lớp đó ghi đè Equals. Để kiểm tra theo thuộc tính, dùng list.Exists(p => p.Name == "Ana") hoặc list.Any(...) của LINQ.

Làm sao sắp xếp List theo một thuộc tính trong C#?

Truyền một phép so sánh cho Sort: people.Sort((a, b) => a.Age.CompareTo(b.Age)) sắp xếp tại chỗ. Đổi chỗ a và b để sắp giảm dần. Hoặc dùng LINQ, cách này trả về một chuỗi mới đã sắp xếp và giữ các phần tử bằng nhau theo thứ tự ban đầu: people.OrderBy(p => p.Age).ToList().

Tại sao gặp lỗi "Collection was modified; enumeration operation may not execute"?

Bạn đã thêm, xóa hoặc thay thế (list[i] = x) phần tử trong khi một foreach đang duyệt chính list đó, và enumerator ném InvalidOperationException ở bước tiếp theo. Hãy dùng list.RemoveAll(condition), duyệt ngược bằng vòng lặp for, hoặc duyệt trên một bản sao như list.ToList().

Count và Capacity khác nhau thế nào?

Count là số phần tử trong list. Capacity là kích thước của mảng bên trong, luôn ít nhất bằng Count. Khi mảng đầy, list cấp phát một mảng mới lớn gấp đôi rồi sao chép các phần tử sang, nên trung bình Add chạy rất nhanh.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu