2015-12-09 13 views
10

Tôi đã đọc một cuốn sách C# trong đó tác giả (một số anh chàng tên là Jon Skeet) thực hiện một chức năng Where nhưCác chức năng riêng biệt thành xác thực và triển khai? Tại sao?

public static IEnumerable<T> Where<T> (this IEnumerable<T> source, Funct<T,bool> predicate) 
{ 
    if (source == null || predicate == null) 
    { 
     throw new ArgumentNullException(); 
    } 
    return WhereImpl(source, predicate); 
} 

public static IEnumerable<T> WhereImpl<T> (IEnumerable <T> source, Func<T,bool> predicate) 
{ 
    foreach (T item in source) 
    { 
     if (predicate(item)) 
     { 
     yield return item; 
     } 
    } 

} 

Bây giờ, tôi hoàn toàn hiểu cách làm việc này và rằng nó tương đương với

public static IEnumerable<T> Where<T> (this IEnumerable<T> source, Funct<T,bool> predicate) 
{ 
    if (source == null || predicate == null) 
    { 
     throw new ArgumentNullException(); 
    } 
    foreach (T item in source) 
    { 
     if (predicate(item)) 
     { 
     yield return item; 
     } 
    } 
} 

đưa ra câu hỏi về lý do tại sao một người sẽ tách biệt chúng thành 2 chức năng cho rằng sẽ có bộ nhớ/thời gian trên đầu và tất nhiên là mã. Tôi luôn luôn xác nhận các thông số và nếu tôi bắt đầu viết như ví dụ này thì tôi sẽ viết nhiều gấp hai lần mã. Có một số trường học của tư tưởng nắm giữ rằng xác nhận và thực hiện nên được chức năng riêng biệt?

Trả lời

14

Lý do là khối lặp luôn luôn là lười. Trừ khi bạn gọi GetEnumerator() và sau đó MoveNext(), mã trong phương thức sẽ không được thực thi.

Nói cách khác, hãy xem xét cuộc gọi này đến phương pháp "tương đương" của bạn:

var ignored = OtherEnumerable.Where<string>(null, null); 

Không có ngoại lệ được ném, bởi vì bạn không gọi GetEnumerator() và sau đó MoveNext(). So sánh với phiên bản của tôi, nơi ngoại lệ được ném ngay lập tức bất kể giá trị trả lại được sử dụng như thế nào ... bởi vì nó chỉ gọi phương thức với khối lặp sau xác thực háo hức.

Lưu ý rằng async/chờ đợi có vấn đề tương tự - nếu bạn có:

public async Task FooAsync(string x) 
{ 
    if (x == null) 
    { 
     throw new ArgumentNullException(nameof(x)); 
    } 
    // Do some stuff including awaiting 
} 

Nếu bạn gọi này, bạn sẽ kết thúc nhận được một faulted Task - chứ không phải là một NullReferenceException bị ném. Nếu bạn đang chờ trả lại Task, thì ngoại lệ này sẽ bị ném, nhưng đó có thể không phải là nơi bạn gọi phương thức. Điều đó không sao trong hầu hết các trường hợp, nhưng đáng để biết.

+1

BTW sử dụng * Hợp đồng mã * có lẽ sẽ không phải là một lý do thực sự để tách chúng thành hai phương thức, vì xác thực thông số sẽ được thực hiện bằng cách sử dụng điều kiện trước ... Tôi không thể kiểm tra điều này ngay bây giờ, nhưng tôi không chắc chắn nếu * hợp đồng mã * cũng sẽ được đánh giá như một phần của khối lặp –

+1

Vâng, cuối cùng tôi đã thử nghiệm nó bản thân mình, tôi đã tò mò về kịch bản tôi đã nói với bạn trong phần bình luận ở trên. Tôi đã đăng câu trả lời của riêng mình để đóng góp với ngoại lệ này cho quy tắc –

5

Nó có thể phụ thuộc vào kịch bản và kiểu mã hóa của bạn. Jon Skeet là hoàn toàn đúng về lý do tại sao họ nên được tách ra khi bạn đang sử dụng yield để tạo vòng lặp.

BTW, tôi nghĩ rằng nó có thể là thú vị thêm hai xu của tôi ở đây: cùng mã sử dụng Mã Hợp đồng (ví dụ: thiết kế theo hợp đồng) cư xử theo một cách khác.

Pre-điều kiện không nằm trong khối iterator, do đó, các mã sau đây sẽ ném một ngoại lệ hợp đồng ngay lập tức nếu toàn bộ trước điều kiện không được đáp ứng:

public static class Test 
{ 
    public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) 
    { 
     Contract.Requires(source != null); 
     Contract.Requires(predicate != null); 

     foreach (T item in source) 
     { 
      if (predicate(item)) 
      { 
       yield return item; 
      } 
     } 
    } 
} 

// This throws a contract exception directly, no need of 
// enumerating the returned enumerable 
Test.Where<string>(null, null); 
1

Một phương pháp sử dụng yield return vẻ rất đẹp và đơn giản, nhưng nếu bạn kiểm tra mã được biên dịch, bạn sẽ thấy nó khá phức tạp.

Trình biên dịch tạo ra một lớp mới cho bạn với logic máy trạng thái để hỗ trợ việc liệt kê. Đối với phương thức Where thứ hai, đó là khoảng 160 dòng mã sau khi giải mã.Phương pháp thực tế Where được biên dịch thành

[IteratorStateMachine(typeof(IterarorTest.<Where>d__0<>))] 
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) 
{ 
    IterarorTest.<Where>d__0<T> expr_07 = new IterarorTest.<Where>d__0<T>(-2); 
    expr_07.<>3__source = source; 
    expr_07.<>3__predicate = predicate; 
    return expr_07; 
} 

Như bạn có thể thấy, không có đối số nào được chọn trong phương pháp này. Nó chỉ trả về một trình lặp mới.

Đối số được chọn trong phương thức tự động tạo 'MoveNext (mã hơi dài để đăng ở đây).

Mặt khác, nếu bạn di chuyển yield return sang phương pháp khác, đối số sẽ được kiểm tra ngay lập tức khi bạn gọi phương thức Where - đây là hành vi được mong đợi ở đây.

Sửa

Như noticed by Matias Fidemraizer, hợp đồng mã cũng giải quyết vấn đề - kiểm tra hợp đồng được chèn trong Where phương pháp

public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) 
{ 
    __ContractsRuntime.Requires(source != null, null, "source != null"); 
    __ContractsRuntime.Requires(predicate != null, null, "predicate != null"); 
    IterarorTest.<Where>d__0<T> expr_27 = new IterarorTest.<Where>d__0<T>(-2); 
    expr_27.<>3__source = source; 
    expr_27.<>3__predicate = predicate; 
    return expr_27; 
} 
Các vấn đề liên quan