2010-09-30 32 views
8

Tôi đã có một loạt các lớp học có thể Process() đối tượng, và trở về đối tượng của mình:Quấn một IEnumerable và bắt ngoại lệ

public override IEnumerable<T> Process(IEnumerable<T> incoming) { ... } 

Tôi muốn viết một lớp xử lý có thể quấn một trong những nhà chế biến, và đăng nhập bất kỳ ngoại lệ không thể bắt buộc nào mà phương thức được bọc Process() có thể bị ném. Ý tưởng đầu tiên của tôi là một cái gì đó như thế này:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     foreach (var x in this.processor.Process(incoming)) { 
      yield return x; 
     } 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 

nhưng điều này không hoạt động, do CS1626: Cannot yield a value in the body of a try block with a catch clause.

Vì vậy, tôi muốn viết một cái gì đó tương đương về khái niệm nhưng biên dịch. :-) Tôi đã có điều này:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    IEnumerator<T> walker; 
    try { 
     walker = this.processor.Process(incoming).GetEnumerator(); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 

    while (true) { 
     T value; 
     try { 
      if (!walker.MoveNext()) { 
       break; 
      } 
      value = walker.Current; 
     } catch (Exception e) { 
      WriteToLog(e); 
      throw; 
     } 
     yield return value; 
    } 
} 

nhưng điều đó phức tạp hơn tôi mong đợi và tôi không hoàn toàn chắc chắn về tính chính xác của nó hoặc không có cách nào đơn giản hơn nhiều.

Tôi có đi đúng hướng ở đây không? Có cách nào dễ hơn không?

+1

Điều gì về việc triển khai IEnumerable trực tiếp và bắt ngoại lệ bên trong MoveNext và Hiện tại của bạn? –

+0

@Matt, tôi nghĩ đó là câu trả lời đúng. Bạn nên làm như vậy. Không có gì sai với ngắn và ngọt ngào. :) –

+0

Tại sao không chỉ loại bỏ vòng lặp foreach và sản lượng? –

Trả lời

-1

Vòng lặp và năng suất không cần thiết (ít nhất là trong ví dụ của bạn). Đơn giản loại bỏ chúng và không còn là một hạn chế đối với các khối catch.

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     return this.processor.Process(incoming); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 

Tôi giả định this.processor.Process(incoming) trả về một bộ sưu tập thực hiện IEnumerable<T>, vì thế bạn không cần phải tạo ra một iterator mới. Nếu this.processor.Process(incoming) là lười biếng đánh giá sau đó

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    try { 
     return this.processor.Process(incoming).ToList(); 
    } catch (Exception e) { 
     WriteToLog(e); 
     throw; 
    } 
} 
+0

nơi nào nó nói 'this.processor.Process (đến) 'là lười biếng? –

+0

@Bear Monkey, tôi sẽ nói lại. Nếu ngoại lệ xảy ra khi gọi 'GetEnumerator()' thì mã của bạn sẽ không bắt được nó vì nó không bao giờ gọi 'GetEnumerator()'. –

+0

@Kirk bạn không có ý nghĩa. –

0

Nó có thể là đẹp hơn nếu đối tượng process là cái gì mà xử lý chỉ là một mục trong danh sách tại một thời điểm (nếu đó là thậm chí có thể), như vậy bạn có thể thu thập từng ngoại lệ cá nhân và trả về một số AggregateException giống như thư viện Task Parallel.

[Nếu quá trình hoạt động trên danh sách như một toàn thể, hoặc nếu một ngoại lệ duy nhất cần phải hủy bỏ toàn bộ quá trình rõ ràng đề nghị này là không thích hợp.]

+0

Nó có thể đẹp hơn, hoặc nó có thể không, nhưng đó là một (bán bên ngoài) giao diện tôi không thể thay đổi, ít nhất là không trên một khoảng thời gian hợp lý. :-) – Ken

-1

Thay vì giải quyết vấn đề cụ thể của bạn mà thôi, bạn có thể có các chức năng helper trong bộ công cụ của bạn:

static IEnumerable<T> RunEnumerator<T>(Func<IEnumerator<T>> generator, 
     Action<Exception> onException) 
    { 
     using (var enumerator = generator()) 
     { 
      if (enumerator == null) 
       yield break; 
      for (; ;) 
      { 
       //Avoid creating a default value with 
       //unknown type T, as we don't know is it possible to do so 
       T[] value = null; 
       try 
       { 
        if (enumerator.MoveNext()) 
         value = new T[] { enumerator.Current }; 
       } 
       catch (Exception e) 
       { 
        onException(e); 
       } 
       if (value != null) 
        yield return value[0]; 
       else 
        yield break; 
      } 
     } 
    } 

    public static IEnumerable<T> WithExceptionHandler<T>(this IEnumerable<T> orig, 
     Action<Exception> onException) 
    { 
     return RunEnumerator(() => 
     { 
      try 
      { 
       return orig.GetEnumerator(); 
      } 
      catch (Exception e) 
      { 
       onException(e); 
       return null; 
      } 
     }, onException); 
    } 
} 

WithExceptionHandler<T> chuyển đổi một IEnumerable<T> thành khác, và khi ngoại lệ xảy ra, gọi lại onException sẽ được gọi. Bằng cách này bạn có thể có chức năng process bạn thực hiện như thế này:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { 
    return incoming.WithExceptionHandler(e => { 
     WriteToLog(e); 
     throw; 
    } 
} 

Bằng cách này, bạn cũng có thể làm điều gì đó khác như bỏ qua trừ hoàn toàn và để cho các lần lặp chỉ đơn giản dừng lại. Bạn cũng có thể điều chỉnh nó một chút bằng cách sử dụng Func<Exception, bool> thay vì Action<Exception> và cho phép gọi lại ngoại lệ để quyết định liệu việc lặp lại có tiếp tục hay không.

+0

Điều này sẽ không hoạt động như mong đợi, nếu enumerator.MoveNext() ném một ngoại lệ, điều tra viên có hiệu quả đã chết, bạn sẽ không bao giờ nhận được mục tiếp theo từ điều tra theo cách này. Lặp lại của bạn sẽ luôn luôn dừng lại ở trường hợp ngoại lệ đầu tiên, vì vậy trình bao bọc của bạn đã không đạt được bất cứ điều gì nhiều hơn nếu bạn đã quấn vòng lặp xử lý ban đầu trong khối try try. –

+0

Vâng, điều này phụ thuộc vào cách điều tra viên của bạn được thực hiện. Bạn có thể dễ dàng viết 'IEnumerator' của riêng bạn mà bỏ qua giới hạn này. Mặc dù bạn không thể dựa vào bất kỳ hệ thống nào khác, ít nhất bạn có thể sử dụng cho mã của riêng bạn. –

+1

Đừng làm cho tôi sai, tôi thực sự quan tâm đến một giải pháp như thế này. Tôi muốn thấy một ví dụ hoạt động :) Nhưng nếu MoveNext() ném một ngoại lệ nó là bánh mì nướng, nếu MoveNext xử lý ngoại lệ, thì MoveNext sẽ trả về true, vì vậy tất cả sự bao bọc này là hoàn toàn không cần thiết. Đó là những gì tôi đang nhận được, wrapper này sẽ không bao giờ 'bỏ qua' ngoại lệ, nó sẽ xử lý một trong những đầu tiên và thoát ra một cách duyên dáng, nhưng nó sẽ không bao giờ nhận được đến mục tiếp theo sau khi ngoại lệ đầu tiên. –

4

Có thể viết phần mở rộng LINQ để bỏ qua tất cả các yếu tố gây ra ngoại lệ và chuyển nó sang chức năng ghi nhật ký.

public static IEnumerable<T> CatchExceptions<T> (this IEnumerable<T> src, Action<Exception> action = null) { 
     using (var enumerator = src.GetEnumerator()) { 
      bool next = true; 

      while (next) { 
       try { 
        next = enumerator.MoveNext(); 
       } catch (Exception ex) { 
        if (action != null) { 
         action(ex); 
        } 
        continue; 
       } 

       if (next) { 
        yield return enumerator.Current; 
       } 
      } 
     } 
    } 

Ví dụ:

ienumerable.Select(e => e.something).CatchExceptions().ToArray() 

ienumerable.Select(e => e.something).CatchExceptions((ex) => Logger.Log(ex, "something failed")).ToArray() 
+1

Điều này sẽ không hoạt động như mong đợi, tôi có lẽ đã rải rác luồng này với lời giải thích nhưng tôi đã cố gắng kiểm tra kỹ lưỡng từng giải pháp được liệt kê. Nó sẽ không bỏ qua mục đặc biệt trong lần lặp lại, nó sẽ dừng lại ở ngoại lệ đầu tiên. Sau khi điều tra đã thất bại, đối tượng không thể tiếp tục từ lần lặp tiếp theo, không phải theo cách mà nó được mã hóa ở đây. Vì vậy, .ToArray() của bạn sẽ chỉ bao giờ chứa tất cả các mục trước khi ngoại lệ. Nó sẽ thất bại một cách duyên dáng, vì vậy thay vì CatchExceptions(), bạn nên gọi nó là UntilException() :) –

3

Nếu những gì bạn muốn làm là xử lý một ngoại lệ trong việc xử lý các kết quả của một liệt kê, sau đó bạn cố gắng Logic đơn giản chỉ cần đi thẳng bên trong của bạn cho/trong khi vòng lặp.

Nhưng ví dụ của bạn đọc như thể bạn đang cố bắt và bỏ qua các ngoại lệ do nhà cung cấp liệt kê nêu ra.

Theo như tôi có thể xác định, không có cách nào trong C# để lặp qua một điều tra viên và bỏ qua và ngoại lệ xảy ra trong chính điều tra viên. Nếu điều tra tăng ngoại lệ, thì tất cả các cuộc gọi trong tương lai tới MoveNext() sẽ dẫn đến kết quả sai.

Cách đơn giản nhất để giải thích lý do tại sao điều này xảy ra là với điều này đếm được rất đơn giản:

IEnumerable<int> TestCases() 
{ 
    yield return 1; 
    yield return 2; 
    throw new ApplicationException("fail eunmeration"); 
    yield return 3; 
    yield return 4; 
} 

dễ hiểu khi chúng ta nhìn vào ví dụ này rõ ràng là ném ngoại lệ sẽ gây ra toàn bộ khối này để thoát ra, và tuyên bố lợi nhuận thứ 3 và thứ 4 sẽ không bao giờ được xử lý. Trong thực tế, nhận được cảnh báo trình biên dịch 'Unreachable code detect' thông thường trên báo cáo yield thứ 3.

Vì vậy, khi đếm được là một phức tạp hơn, các quy tắc tương tự áp dụng:

IEnumerable<int> TestCases2() 
{ 
    foreach (var item in Enumerable.Range(0,10)) 
    { 
     switch(item) 
     { 
      case 2: 
      case 5: 
       throw new ApplicationException("This bit failed"); 
      default: 
       yield return item; 
       break; 
     } 
    } 
} 

Khi ngoại lệ xảy ra, quá trình xử lý của khối này không còn và họ sẽ trả lại lên các cuộc gọi stack để xử lý ngoại lệ gần nhất.

TẤT CẢ các ví dụ khả thi để khắc phục sự cố này mà tôi đã tìm thấy trên SO không tiến hành mục tiếp theo trong điều tra, tất cả đều ngắt ở ngoại lệ đầu tiên.

Do đó, để bỏ qua vi hợp lệ trong điều tra, bạn sẽ cần nhà cung cấp tạo điều kiện thuận lợi. Này chỉ có thể thực sự nếu mã của bạn nhà cung cấp, hoặc bạn có thể liên hệ với nhà phát triển những người đã làm, sau đây là một ví dụ quá đơn giản về cách bạn có thể đạt được điều này:

IEnumerable<int> TestCases3(Action<int, Exception> exceptionHandler) 
{ 
    foreach (var item in Enumerable.Range(0, 10)) 
    { 
     int value = default(int); 
     try 
     { 
      switch (item) 
      { 
       case 2: 
       case 5: 
        throw new ApplicationException("This bit failed"); 
       default: 
        value = item; 
        break; 
      } 
     } 
     catch(Exception e) 
     { 
      if (exceptionHandler != null) 
      { 
       exceptionHandler(item, e); 
       continue; 
      } 
      else 
       throw; 
     } 
     yield return value; 
    } 
} 

...

foreach (var item in TestCases3(
    (int item, Exception ex) 
    => 
    Console.Out.WriteLine("Error on item: {0}, Exception: {1}", item, ex.Message))) 
{ 
    Console.Out.WriteLine(item); 
} 

này sẽ cho kết quả sau:

0 
1 
Error on item: 2, Exception: This bit failed 
3 
4 
Error on item: 5, Exception: This bit failed 
6 
7 
8 
9 

tôi hy vọng điều này đã xóa bỏ những vấn đề đối với các nhà phát triển khác trong tương lai vì nó là một ý tưởng khá phổ biến mà tất cả chúng ta có được một khi chúng ta bắt đầu getti ng sâu vào Linq và liệt kê. Công cụ mạnh mẽ nhưng có một số hạn chế hợp lý.

Các vấn đề liên quan