Trả lại tất cả các liệt kê với lợi nhuận cùng một lúc; không lặp


164

Tôi có chức năng sau đây để nhận lỗi xác thực cho thẻ. Câu hỏi của tôi liên quan đến việc đối phó với GetErrors. Cả hai phương thức có cùng kiểu trả về IEnumerable<ErrorInfo>.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    var errors = GetMoreErrors(card);
    foreach (var e in errors)
        yield return e;

    // further yield returns for more validation errors
}

Có thể trả lại tất cả các lỗi trong GetMoreErrorsmà không cần phải liệt kê thông qua chúng?

Nghĩ về nó đây có lẽ là một câu hỏi ngu ngốc, nhưng tôi muốn chắc chắn rằng tôi sẽ không sai.


Tôi rất vui (và tò mò!) Khi thấy nhiều câu hỏi về lợi nhuận xuất hiện hơn - bản thân tôi không hiểu lắm. Không phải là một câu hỏi ngu ngốc!
JoshJordan

GetCardProductionValidationErrorsFor
Andrew Hare

4
có gì sai khi trả về GetMoreErrors (thẻ); ?
Sam Saffron

10
@Sam: "lợi nhuận cao hơn nữa cho các lỗi xác thực hơn"
Jon Skeet

1
Từ quan điểm của một ngôn ngữ không mơ hồ, một vấn đề là phương pháp không thể biết liệu có bất cứ điều gì thực hiện cả T và IEnumerable <T>. Vì vậy, bạn cần một cấu trúc khác nhau trong sản lượng. Điều đó nói rằng, nó chắc chắn sẽ tốt đẹp khi có một cách để làm điều này. Lợi nhuận mang lại lợi nhuận foo, có lẽ, nơi foo thực hiện IEnumerable <T>?
William Jockusch

Câu trả lời:


140

Đây chắc chắn không phải là một câu hỏi ngu ngốc, và đó là điều mà F # hỗ trợ yield!cho toàn bộ bộ sưu tập so yieldvới một mặt hàng. (Điều đó có thể rất hữu ích về mặt đệ quy đuôi ...)

Thật không may, nó không được hỗ trợ trong C #.

Tuy nhiên, nếu bạn có một vài phương thức mỗi trả về một IEnumerable<ErrorInfo>, bạn có thể sử dụng Enumerable.Concatđể làm cho mã của mình đơn giản hơn:

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetMoreErrors(card).Concat(GetOtherErrors())
                              .Concat(GetValidationErrors())
                              .Concat(AnyMoreErrors())
                              .Concat(ICantBelieveHowManyErrorsYouHave());
}

Có một sự khác biệt rất quan trọng giữa hai triển khai: cách này sẽ gọi tất cả các phương thức ngay lập tức , mặc dù nó sẽ chỉ sử dụng các trình lặp được trả về một lần. Mã hiện tại của bạn sẽ đợi cho đến khi nó lặp qua mọi thứ GetMoreErrors()trước khi nó hỏi về các lỗi tiếp theo.

Thông thường điều này không quan trọng, nhưng nó đáng để hiểu những gì sẽ xảy ra khi nào.


3
Wes Dyer có một bài viết thú vị đề cập đến mô hình này. blogs.msdn.com/wesdyer/archive/2007/03/23/...
JohannesH

1
Sửa lỗi nhỏ cho người qua đường - đó là System.Linq.Enumutions.Concat <> (thứ nhất, thứ hai). Không phải IEnumutions.Concat ().
redcalx

@ the-locster: Tôi không chắc ý của bạn là gì. Đó chắc chắn là Vô số chứ không phải là liệt kê. Bạn có thể làm rõ nhận xét của bạn?
Jon Skeet

@Jon Skeet - Chính xác thì bạn có nghĩa là nó sẽ gọi các phương thức ngay lập tức? Tôi đã chạy thử nghiệm và có vẻ như nó trì hoãn các cuộc gọi phương thức hoàn toàn cho đến khi một cái gì đó thực sự được lặp lại. Mã ở đây: pastebin.com/0kj5QtfD
Steven Oxley

5
@Steven: Không. Nó đang gọi các phương thức - nhưng trong trường hợp của bạn GetOtherErrors()(v.v.) đang trì hoãn kết quả của chúng (khi chúng được thực hiện bằng cách sử dụng các khối lặp). Hãy thử thay đổi chúng để trả về một mảng mới hoặc một cái gì đó tương tự, và bạn sẽ thấy ý tôi là gì.
Jon Skeet

26

Bạn có thể thiết lập tất cả các nguồn lỗi như thế này (tên phương thức mượn từ câu trả lời của Jon Skeet).

private static IEnumerable<IEnumerable<ErrorInfo>> GetErrorSources(Card card)
{
    yield return GetMoreErrors(card);
    yield return GetOtherErrors();
    yield return GetValidationErrors();
    yield return AnyMoreErrors();
    yield return ICantBelieveHowManyErrorsYouHave();
}

Sau đó bạn có thể lặp lại chúng cùng một lúc.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    foreach (var errorSource in GetErrorSources(card))
        foreach (var error in errorSource)
            yield return error;
}

Ngoài ra, bạn có thể làm phẳng các nguồn lỗi với SelectMany.

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return GetErrorSources(card).SelectMany(e => e);
}

Việc thực hiện các phương thức trong GetErrorSourcescũng sẽ bị trì hoãn.


16

Tôi nghĩ ra một yield_đoạn nhanh :

năng suất_ bắn tỉa hoạt hình sử dụng

Đây là đoạn trích XML:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Author>John Gietzen</Author>
      <Description>yield! expansion for C#</Description>
      <Shortcut>yield_</Shortcut>
      <Title>Yield All</Title>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="true">
          <Default>items</Default>
          <ID>items</ID>
        </Literal>
        <Literal Editable="true">
          <Default>i</Default>
          <ID>i</ID>
        </Literal>
      </Declarations>
      <Code Language="CSharp"><![CDATA[foreach (var $i$ in $items$) yield return $i$$end$;]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

2
Làm thế nào đây là một câu trả lời cho câu hỏi?
Ian Kemp

@Ian, đây là cách bạn phải thực hiện lợi nhuận lồng nhau trong C #. Không có yield!, như trong F #.
John Gietzen

đây không phải là một câu trả lời cho câu hỏi
divyang4481

8

Tôi không thấy bất cứ điều gì sai với chức năng của bạn, tôi nói rằng nó đang làm những gì bạn muốn.

Hãy nghĩ về Yield như trả về một phần tử trong Bảng liệt kê cuối cùng mỗi lần nó được gọi, vì vậy khi bạn có nó trong vòng lặp foreach như thế, mỗi lần nó được gọi, nó sẽ trả về 1 phần tử. Bạn có khả năng đưa các câu lệnh có điều kiện vào mục trước để lọc tập kết quả. (chỉ đơn giản bằng cách không đạt được các tiêu chí loại trừ của bạn)

Nếu bạn thêm các sản lượng tiếp theo sau này trong phương thức, nó sẽ tiếp tục thêm 1 phần tử vào bảng liệt kê, cho phép thực hiện những việc như ...

public IEnumerable<string> ConcatLists(params IEnumerable<string>[] lists)
{
  foreach (IEnumerable<string> list in lists)
  {
    foreach (string s in list)
    {
      yield return s;
    }
  }
}

4

Tôi ngạc nhiên không ai nghĩ sẽ đề xuất một phương thức Tiện ích mở rộng đơn giản IEnumerable<IEnumerable<T>>để làm cho mã này tiếp tục thực thi bị trì hoãn. Tôi là một fan hâm mộ của việc thực hiện bị trì hoãn vì nhiều lý do, một trong số đó là dấu chân bộ nhớ là nhỏ ngay cả đối với các liệt kê rất lớn.

public static class EnumearbleExtensions
{
    public static IEnumerable<T> UnWrap<T>(this IEnumerable<IEnumerable<T>> list)
    {
        foreach(var innerList in list)
        {
            foreach(T item in innerList)
            {
                yield return item;
            }
        }
    }
}

Và bạn có thể sử dụng nó trong trường hợp của bạn như thế này

private static IEnumerable<ErrorInfo> GetErrors(Card card)
{
    return DoGetErrors(card).UnWrap();
}

private static IEnumerable<IEnumerable<ErrorInfo>> DoGetErrors(Card card)
{
    yield return GetMoreErrors(card);

    // further yield returns for more validation errors
}

Tương tự như vậy, bạn có thể loại bỏ chức năng bao bọc xung quanh DoGetErrorsvà chỉ cần di chuyển UnWrapđến các cuộc gọi.


2
Có lẽ không ai nghĩ về một phương thức Mở rộng vì DoGetErrors(card).SelectMany(x => x)thực hiện tương tự và duy trì hành vi hoãn lại. Đó chính xác là những gì Adam gợi ý trong câu trả lời của mình .
huysentbeanw

3

Có, có thể trả lại tất cả các lỗi cùng một lúc. Chỉ cần trả lại một List<T>hoặc ReadOnlyCollection<T>.

Bằng cách trả lại một IEnumerable<T>bạn đang trả lại một chuỗi của một cái gì đó. Trên bề mặt có vẻ giống với việc trả lại bộ sưu tập, nhưng có một số khác biệt, bạn nên ghi nhớ.

Bộ sưu tập

  • Người gọi có thể chắc chắn rằng cả bộ sưu tập và tất cả các mục sẽ tồn tại khi bộ sưu tập được trả về. Nếu bộ sưu tập phải được tạo cho mỗi cuộc gọi, trả lại bộ sưu tập là một ý tưởng thực sự tồi tệ.
  • Hầu hết các bộ sưu tập có thể được sửa đổi khi trả lại.
  • Bộ sưu tập có kích thước hữu hạn.

Trình tự

  • Có thể được liệt kê - và đó là khá nhiều tất cả chúng ta có thể nói chắc chắn.
  • Một chuỗi trả về chính nó không thể được sửa đổi.
  • Mỗi phần tử có thể được tạo ra như là một phần của việc chạy qua chuỗi (tức là trả về IEnumerable<T>cho phép đánh giá lười biếng, trả về List<T>thì không).
  • Một chuỗi có thể là vô hạn và do đó để lại cho người gọi để quyết định có bao nhiêu phần tử sẽ được trả về.

Trả lại một bộ sưu tập có thể dẫn đến chi phí không hợp lý nếu tất cả các khách hàng thực sự cần là liệt kê thông qua nó, vì bạn phân bổ cấu trúc dữ liệu cho tất cả các yếu tố trước. Ngoài ra, nếu bạn ủy quyền cho một phương thức khác trả về một chuỗi, thì việc chụp nó thành một bộ sưu tập liên quan đến việc sao chép thêm và bạn không biết có bao nhiêu mục (và do đó có thể có bao nhiêu chi phí). Vì vậy, chỉ nên trả lại bộ sưu tập khi nó đã ở đó và có thể được trả lại trực tiếp mà không cần sao chép (hoặc được gói dưới dạng chỉ đọc). Trong tất cả các trường hợp khác, trình tự là một lựa chọn tốt hơn
Pavel Minaev

Tôi đồng ý, và nếu bạn có ấn tượng rằng tôi nói trả lại một bộ sưu tập luôn là một ý tưởng tốt, bạn đã bỏ lỡ quan điểm của tôi. Tôi đã cố gắng làm nổi bật thực tế là có sự khác biệt giữa trả lại một bộ sưu tập và trả lại một chuỗi. Tôi sẽ cố gắng làm cho nó rõ ràng hơn.
Brian Rasmussen
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.