Xử lý cảnh báo cho nhiều phép liệt kê của IEnumerable


358

Trong mã của tôi, tôi cần phải sử dụng IEnumerable<>nhiều lần, do đó nhận được lỗi Chia sẻ lại "Có thể liệt kê nhiều lần IEnumerable".

Mã mẫu:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • Tôi có thể thay đổi objectstham số thành Listvà sau đó tránh nhiều phép liệt kê nhưng sau đó tôi không nhận được đối tượng cao nhất mà tôi có thể xử lý.
  • Một điều khác mà tôi có thể làm là chuyển đổi IEnumerablethành Listở đầu phương thức:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

Nhưng điều này chỉ là khó xử .

Bạn sẽ làm gì trong kịch bản này?

Câu trả lời:


470

Vấn đề với việc lấy IEnumerablelàm tham số là nó báo cho người gọi "Tôi muốn liệt kê điều này". Nó không cho họ biết bạn muốn liệt kê bao nhiêu lần.

Tôi có thể thay đổi tham số đối tượng thành Danh sách và sau đó tránh nhiều phép liệt kê nhưng sau đó tôi không nhận được đối tượng cao nhất mà tôi có thể xử lý .

Mục tiêu của việc lấy đối tượng cao nhất là cao cả, nhưng nó để lại chỗ cho quá nhiều giả định. Bạn có thực sự muốn ai đó chuyển một truy vấn LINQ sang SQL cho phương thức này không, chỉ để bạn liệt kê nó hai lần (nhận được kết quả có khả năng khác nhau mỗi lần?)

Thiếu ngữ nghĩa ở đây là một người gọi, có lẽ không mất thời gian để đọc chi tiết về phương thức, có thể cho rằng bạn chỉ lặp lại một lần - vì vậy họ chuyển cho bạn một đối tượng đắt tiền. Chữ ký phương thức của bạn không chỉ ra một trong hai cách.

Bằng cách thay đổi chữ ký phương thức thành IList/ ICollection, ít nhất bạn sẽ làm cho người gọi rõ ràng hơn những gì bạn mong đợi và họ có thể tránh được những sai lầm tốn kém.

Mặt khác, hầu hết các nhà phát triển nhìn vào phương thức có thể cho rằng bạn chỉ lặp lại một lần. Nếu lấy một IEnumerablelà rất quan trọng, bạn nên xem xét làm.ToList() khi bắt đầu phương pháp.

Thật đáng tiếc. .NET không có giao diện là IEnumerable + Count + Indexer, không có các phương thức Add / Remove, v.v., đó là điều tôi nghi ngờ sẽ giải quyết vấn đề này.


31
ReadOnlyCollection <T> có đáp ứng các yêu cầu giao diện mong muốn của bạn không? msdn.microsoft.com/en-us/l Library / ms132474.aspx
Dan đang loay hoay bởi Firelight

73
@DanNeely Tôi muốn đề xuất IReadOnlyCollection(T)(mới với .net 4.5) là giao diện tốt nhất để truyền đạt ý tưởng rằng đó là một IEnumerable(T)dự định được liệt kê nhiều lần. Như câu trả lời này, IEnumerable(T)bản thân nó quá chung chung đến nỗi nó thậm chí có thể đề cập đến nội dung không thể đặt lại được mà không thể liệt kê lại mà không có tác dụng phụ. Nhưng IReadOnlyCollection(T)ngụ ý tái lặp lại.
binki

1
Nếu bạn thực hiện .ToList()khi bắt đầu phương thức, trước tiên bạn phải kiểm tra xem tham số có phải là null không. Điều đó có nghĩa là bạn sẽ nhận được hai vị trí nơi bạn ném ArgumentException: nếu tham số là null và nếu nó không có bất kỳ mục nào.
comecme

Tôi đọc bài viết này về ngữ nghĩa của IReadOnlyCollection<T>vs IEnumerable<T>và sau đó đăng một câu hỏi về việc sử dụng IReadOnlyCollection<T>như một tham số, và trong trường hợp nào. Tôi nghĩ rằng kết luận cuối cùng tôi đã đưa ra là logic để làm theo. Rằng nó nên được sử dụng ở nơi dự kiến ​​liệt kê, không nhất thiết là nơi có thể có nhiều phép liệt kê.
Neo

32

Nếu dữ liệu của bạn luôn luôn có thể lặp lại, có lẽ đừng lo lắng về nó. Tuy nhiên, bạn cũng có thể hủy đăng ký nó - điều này đặc biệt hữu ích nếu dữ liệu đến có thể lớn (ví dụ: đọc từ đĩa / mạng):

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

Lưu ý Tôi đã thay đổi ngữ nghĩa của DoS SomethingElse một chút, nhưng điều này chủ yếu là để hiển thị việc sử dụng không được kiểm soát. Bạn có thể bọc lại iterator, ví dụ. Bạn cũng có thể biến nó thành một khối lặp, điều này có thể tốt; sau đó không có list- và bạn sẽ nhận yield returncác mục khi bạn nhận được chúng, thay vì thêm vào danh sách sẽ được trả lại.


4
+1 Vâng, đó là một mã rất hay, nhưng- Tôi mất đi tính dễ chịu và dễ đọc của mã.
gdoron đang hỗ trợ Monica

5
@gdoron không phải mọi thứ đều đẹp, tuy nhiên tôi không thể đọc được những điều trên. Đặc biệt nếu nó được tạo thành một khối lặp - khá dễ thương, IMO.
Marc Gravell

Tôi chỉ có thể viết: "var objectList = object.ToList ();" và lưu tất cả các xử lý En enator.
gdoron đang hỗ trợ Monica

10
@gdoron trong câu hỏi mà bạn nói rõ ràng là bạn không thực sự muốn làm điều đó; p Ngoài ra, cách tiếp cận ToList () có thể không phù hợp nếu dữ liệu nếu rất lớn (có khả năng, vô hạn - các chuỗi không cần phải hữu hạn, nhưng có thể vẫn được lặp đi lặp lại). Cuối cùng, tôi chỉ trình bày một lựa chọn. Chỉ có bạn biết bối cảnh đầy đủ để xem nếu nó được bảo hành. Nếu dữ liệu của bạn có thể lặp lại, một tùy chọn hợp lệ khác là: không thay đổi bất cứ điều gì! Bỏ qua R # thay vào đó ... tất cả phụ thuộc vào bối cảnh.
Marc Gravell

1
Tôi nghĩ giải pháp của Marc khá hay. 1. Rất rõ ràng 'danh sách' được khởi tạo như thế nào, rất rõ ràng cách danh sách được lặp lại và rất rõ ràng các thành viên của danh sách được vận hành trên đó.
Keith Hoffman

8

Sử dụng IReadOnlyCollection<T>hoặc IReadOnlyList<T>trong chữ ký phương thức thay vì IEnumerable<T>, có lợi thế là làm rõ rằng bạn có thể cần kiểm tra số đếm trước khi lặp hoặc lặp lại nhiều lần vì một số lý do khác.

Tuy nhiên, chúng có một nhược điểm rất lớn sẽ gây ra vấn đề nếu bạn cố gắng cấu trúc lại mã của mình để sử dụng các giao diện, ví dụ để làm cho nó dễ kiểm tra và thân thiện hơn với proxy động. Điểm mấu chốt là IList<T>không kế thừa từIReadOnlyList<T> và tương tự cho các bộ sưu tập khác và giao diện chỉ đọc tương ứng của chúng. (Nói tóm lại, điều này là do .NET 4.5 muốn giữ khả năng tương thích ABI với các phiên bản trước đó. Nhưng họ thậm chí còn không có cơ hội để thay đổi điều đó trong lõi .NET. )

Điều này có nghĩa là nếu bạn nhận được một phần IList<T>từ một phần của chương trình và muốn chuyển nó sang phần khác mong đợi IReadOnlyList<T>, bạn không thể! Tuy nhiên, bạn có thể vượt qua IList<T>như mộtIEnumerable<T> .

Cuối cùng, IEnumerable<T>là giao diện chỉ đọc được hỗ trợ bởi tất cả các bộ sưu tập .NET bao gồm tất cả các giao diện bộ sưu tập. Bất kỳ sự thay thế nào khác sẽ quay lại cắn bạn khi bạn nhận ra rằng bạn đã tự khóa mình khỏi một số lựa chọn kiến ​​trúc. Vì vậy, tôi nghĩ rằng đó là loại thích hợp để sử dụng trong chữ ký hàm để thể hiện rằng bạn chỉ muốn một bộ sưu tập chỉ đọc.

(Lưu ý rằng bạn luôn có thể viết IReadOnlyList<T> ToReadOnly<T>(this IList<T> list)phương thức tiện ích mở rộng đơn giản nếu loại cơ bản hỗ trợ cả hai giao diện, nhưng bạn phải thêm thủ công ở mọi nơi khi tái cấu trúc, trong đó IEnumerable<T>luôn luôn tương thích.)

Như mọi khi, đây không phải là một điều tuyệt đối, nếu bạn đang viết mã nặng về cơ sở dữ liệu trong đó việc liệt kê nhiều lần vô tình sẽ là một thảm họa, bạn có thể thích một sự đánh đổi khác.


2
+1 Để đến với một bài đăng cũ và thêm tài liệu về các cách mới để giải quyết vấn đề này với các tính năng mới được cung cấp bởi nền tảng .NET (ví dụ IReadOnlyCollection <T>)
Kevin Avignon

4

Nếu mục đích thực sự là ngăn chặn nhiều bảng liệt kê hơn câu trả lời của Marc Gravell là mục đích để đọc, nhưng duy trì cùng một ngữ nghĩa, bạn có thể đơn giản loại bỏ phần thừa AnyFirstgọi và đi theo:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

Lưu ý rằng điều này giả định rằng bạn IEnumerablekhông chung chung hoặc ít nhất bị hạn chế là một loại tham chiếu.


2
objectsvẫn đang được chuyển cho DoS SomethingElse đang trả về một danh sách, vì vậy khả năng bạn vẫn đang liệt kê hai lần vẫn còn đó. Dù bằng cách nào, nếu bộ sưu tập là truy vấn LINQ to SQL, bạn nhấn DB hai lần.
Paul Stovell

1
@PaulStovell, Đọc này: "Nếu mục đích thực sự là ngăn chặn nhiều cách liệt kê hơn câu trả lời của Marc Gravell là người nên đọc" =)
gdoron đang hỗ trợ Monica 23/11/11

Nó đã được đề xuất trên một số bài đăng stackoverflow khác về việc ném ngoại lệ cho danh sách trống và tôi sẽ đề xuất ở đây: tại sao lại ném ngoại lệ vào danh sách trống? Lấy một danh sách trống, đưa ra một danh sách trống. Như một mô hình, điều đó khá đơn giản. Nếu người gọi không biết họ đang chuyển một danh sách trống, thì đó là vấn đề.
Keith Hoffman

@Keith Hoffman, mã mẫu duy trì hành vi giống như mã mà OP đã đăng, trong đó việc sử dụng Firstsẽ đưa ra một ngoại lệ cho một danh sách trống. Tôi không nghĩ có thể nói không bao giờ ném ngoại lệ vào danh sách trống hoặc luôn ném ngoại lệ vào danh sách trống. Nó phụ thuộc ...
João Angelo

Joao đủ công bằng. Nhiều thực phẩm cho suy nghĩ hơn là một lời chỉ trích bài viết của bạn.
Keith Hoffman

3

Tôi thường quá tải phương pháp của mình với IEnumerable và IList trong tình huống này.

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

Tôi cẩn thận để giải thích trong các nhận xét tóm tắt về các phương thức gọi IEnumerable sẽ thực hiện một .ToList ().

Lập trình viên có thể chọn .ToList () ở mức cao hơn nếu nhiều thao tác được nối và sau đó gọi quá tải IList hoặc để cho quá tải IEnumerable của tôi xử lý việc đó.


20
Điều này thật kinh khủng.
Mike Chamberlain

1
@MikeChamberlain Vâng, nó thực sự là khủng khiếp và hoàn toàn sạch sẽ cùng một lúc. Thật không may một số kịch bản nặng cần cách tiếp cận này.
Mauro Sampietro

1
Nhưng sau đó, bạn sẽ tạo lại mảng mỗi khi bạn chuyển nó sang phương thức tối ưu hóa IEnumerable. Tôi đồng ý với @MikeChamberlain về vấn đề này, đây là một gợi ý khủng khiếp.
Krythic

2
@Krythic Nếu bạn không cần tạo lại danh sách, bạn thực hiện ToList ở một nơi khác và sử dụng quá tải IList. Đó là điểm của giải pháp này.
Mauro Sampietro

0

Nếu bạn chỉ cần kiểm tra phần tử đầu tiên, bạn có thể nhìn vào nó mà không cần lặp lại toàn bộ bộ sưu tập:

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}

-3

Trước hết, cảnh báo đó không phải lúc nào cũng có ý nghĩa rất nhiều. Tôi thường vô hiệu hóa nó sau khi chắc chắn rằng nó không phải là một cổ chai hiệu suất. Nó chỉ có nghĩa IEnumerablelà được đánh giá hai lần, thường không phải là một vấn đề trừ khi evaluationbản thân nó mất một thời gian dài. Ngay cả khi nó mất nhiều thời gian, trong trường hợp này bạn chỉ sử dụng một yếu tố lần đầu tiên.

Trong kịch bản này, bạn cũng có thể khai thác các phương thức mở rộng linq mạnh mẽ hơn nữa.

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

Có thể chỉ đánh giá IEnumerablemột lần trong trường hợp này với một số rắc rối, nhưng hồ sơ đầu tiên và xem nếu nó thực sự là một vấn đề.


15
Tôi làm việc rất nhiều với IQueryable và tắt những cảnh báo đó là rất nguy hiểm.
gdoron đang hỗ trợ Monica

5
Đánh giá hai lần là một điều xấu trong sách của tôi. Nếu phương thức mất IEnumerable, tôi có thể muốn chuyển một truy vấn LINQ sang SQL cho nó, dẫn đến nhiều cuộc gọi DB. Vì chữ ký phương thức không chỉ ra rằng nó có thể liệt kê hai lần, nên nó sẽ thay đổi chữ ký (chấp nhận IList / ICollection) hoặc chỉ lặp lại một lần
Paul Stovell

4
tối ưu hóa sớm và hủy hoại khả năng đọc và do đó khả năng thực hiện tối ưu hóa tạo ra sự khác biệt về sau là điều tồi tệ hơn trong cuốn sách của tôi.
vidstige

Khi bạn có một truy vấn cơ sở dữ liệu, đảm bảo rằng nó chỉ được đánh giá một lần đã tạo ra sự khác biệt. Nó không chỉ là một số lợi ích lý thuyết trong tương lai, mà nó còn là một lợi ích thực sự có thể đo lường được ngay bây giờ. Không chỉ vậy, việc đánh giá chỉ một lần không chỉ mang lại lợi ích cho hiệu suất, nó còn cần thiết cho tính chính xác. Thực hiện một truy vấn cơ sở dữ liệu hai lần có thể tạo ra các kết quả khác nhau, vì ai đó có thể đã ban hành lệnh cập nhật giữa hai đánh giá truy vấn.

1
@hvd Tôi không khuyên bạn như vậy. Tất nhiên bạn không nên liệt kê IEnumerablesnhững kết quả khác nhau mỗi lần bạn lặp lại hoặc mất một thời gian thực sự dài để lặp lại. Xem câu trả lời của Marc Gravells - Ông cũng nói đầu tiên và quan trọng nhất là "có lẽ đừng lo lắng". Vấn đề là - Rất nhiều lần bạn thấy điều này bạn không có gì phải lo lắng.
vidstige
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.