Tại sao phương thức mở rộng chuỗi này không đưa ra một ngoại lệ?


119

Tôi có một phương thức mở rộng chuỗi C # sẽ trả về một IEnumerable<int>trong tất cả các chỉ mục của một chuỗi con trong một chuỗi. Nó hoạt động hoàn hảo cho mục đích dự định của nó và kết quả mong đợi được trả về (như được chứng minh bởi một trong các bài kiểm tra của tôi, mặc dù không phải là bài kiểm tra bên dưới), nhưng một bài kiểm tra đơn vị khác đã phát hiện ra một vấn đề với nó: nó không thể xử lý các đối số rỗng.

Đây là phương pháp mở rộng mà tôi đang thử nghiệm:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (searchText == null)
    {
        throw new ArgumentNullException("searchText");
    }
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

Đây là bài kiểm tra đã gắn cờ vấn đề:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
    string test = "a.b.c.d.e";
    test.AllIndexesOf(null);
}

Khi thử nghiệm chạy với phương thức mở rộng của tôi, nó không thành công, với thông báo lỗi tiêu chuẩn rằng phương pháp "không đưa ra ngoại lệ".

Điều này thật khó hiểu: tôi đã chuyển rõ ràng nullvào hàm, nhưng vì lý do nào đó mà phép so sánh null == nullđang quay trở lại false. Do đó, không có ngoại lệ nào được ném ra và mã vẫn tiếp tục.

Tôi đã xác nhận rằng đây không phải là lỗi trong bài kiểm tra: khi chạy phương thức trong dự án chính của tôi với lệnh gọi đến Console.WriteLinetrong ifkhối so sánh null , không có gì được hiển thị trên bảng điều khiển và không có ngoại lệ nào bị chặn bởi bất kỳ catchkhối nào tôi thêm. Hơn nữa, sử dụng string.IsNullOrEmptythay vì == nullcó cùng một vấn đề.

Tại sao so sánh được cho là đơn giản này không thành công?


5
Bạn đã thử bước qua mã chưa? Điều đó có thể sẽ được giải quyết khá nhanh chóng.
Matthew Haugen

1
Có gì không xảy ra? (Nó có ném ra một ngoại lệ không; nếu có thì cái nào và dòng nào?)
user2864740

@ user2864740 Tôi đã mô tả mọi thứ xảy ra. Không có ngoại lệ, chỉ là một thử nghiệm thất bại và một phương pháp chạy.
ArtOfCode

7
Các trình lặp lại không được thực thi cho đến khi chúng được lặp lại
BlueRaja - Danny Pflughoeft

2
Không có gì. Điều này cũng lọt vào danh sách "gotcha tệ nhất" của Jon: stackoverflow.com/a/241180/88656 . Đây là một vấn đề khá phổ biến.
Eric Lippert

Câu trả lời:


158

Bạn đang sử dụng yield return. Khi làm như vậy, trình biên dịch sẽ viết lại phương thức của bạn thành một hàm trả về một lớp được tạo để triển khai một máy trạng thái.

Nói chung, nó ghi lại các local vào các trường của lớp đó và mỗi phần trong thuật toán của bạn giữa các yield returnlệnh sẽ trở thành một trạng thái. Bạn có thể kiểm tra bằng trình dịch ngược phương thức này sẽ trở thành gì sau khi biên dịch (đảm bảo tắt tính năng dịch ngược thông minh sẽ tạo ra yield return).

Nhưng điểm mấu chốt là: mã phương thức của bạn sẽ không được thực thi cho đến khi bạn bắt đầu lặp lại.

Cách thông thường để kiểm tra các điều kiện tiên quyết là chia phương pháp của bạn thành hai:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (str == null)
        throw new ArgumentNullException("str");
    if (searchText == null)
        throw new ArgumentNullException("searchText");

    return AllIndexesOfCore(str, searchText);
}

private static IEnumerable<int> AllIndexesOfCore(string str, string searchText)
{
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

Điều này hoạt động vì phương thức đầu tiên sẽ hoạt động giống như bạn mong đợi (thực thi ngay lập tức) và sẽ trả về máy trạng thái được thực hiện bởi phương thức thứ hai.

Lưu ý rằng bạn cũng nên kiểm tra strtham số nullvì các phương thức mở rộng có thể được gọi trên nullcác giá trị, vì chúng chỉ là đường cú pháp.


Nếu bạn tò mò về những gì trình biên dịch làm với mã của bạn, đây là phương pháp của bạn, được dịch ngược với dotPeek bằng cách sử dụng tùy chọn Hiển thị mã do trình biên dịch tạo .

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
  Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2);
  allIndexesOfD0.<>3__str = str;
  allIndexesOfD0.<>3__searchText = searchText;
  return (IEnumerable<int>) allIndexesOfD0;
}

[CompilerGenerated]
private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
  private int <>2__current;
  private int <>1__state;
  private int <>l__initialThreadId;
  public string str;
  public string <>3__str;
  public string searchText;
  public string <>3__searchText;
  public int <index>5__1;

  int IEnumerator<int>.Current
  {
    [DebuggerHidden] get
    {
      return this.<>2__current;
    }
  }

  object IEnumerator.Current
  {
    [DebuggerHidden] get
    {
      return (object) this.<>2__current;
    }
  }

  [DebuggerHidden]
  public <AllIndexesOf>d__0(int <>1__state)
  {
    base..ctor();
    this.<>1__state = param0;
    this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
  }

  [DebuggerHidden]
  IEnumerator<int> IEnumerable<int>.GetEnumerator()
  {
    Test.<AllIndexesOf>d__0 allIndexesOfD0;
    if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
    {
      this.<>1__state = 0;
      allIndexesOfD0 = this;
    }
    else
      allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0);
    allIndexesOfD0.str = this.<>3__str;
    allIndexesOfD0.searchText = this.<>3__searchText;
    return (IEnumerator<int>) allIndexesOfD0;
  }

  [DebuggerHidden]
  IEnumerator IEnumerable.GetEnumerator()
  {
    return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
  }

  bool IEnumerator.MoveNext()
  {
    switch (this.<>1__state)
    {
      case 0:
        this.<>1__state = -1;
        if (this.searchText == null)
          throw new ArgumentNullException("searchText");
        this.<index>5__1 = 0;
        break;
      case 1:
        this.<>1__state = -1;
        this.<index>5__1 += this.searchText.Length;
        break;
      default:
        return false;
    }
    this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1);
    if (this.<index>5__1 != -1)
    {
      this.<>2__current = this.<index>5__1;
      this.<>1__state = 1;
      return true;
    }
    goto default;
  }

  [DebuggerHidden]
  void IEnumerator.Reset()
  {
    throw new NotSupportedException();
  }

  void IDisposable.Dispose()
  {
  }
}

Đây là mã C # không hợp lệ, vì trình biên dịch được phép thực hiện những điều mà ngôn ngữ không cho phép, nhưng lại hợp pháp trong IL - ví dụ: đặt tên cho các biến theo cách bạn không thể tránh xung đột tên.

Nhưng như bạn có thể thấy, hàm AllIndexesOfduy nhất tạo và trả về một đối tượng, mà hàm tạo chỉ khởi tạo một số trạng thái. GetEnumeratorchỉ sao chép đối tượng. Công việc thực sự được thực hiện khi bạn bắt đầu liệt kê (bằng cách gọi MoveNextphương thức).


9
BTW, tôi đã thêm điểm quan trọng sau vào câu trả lời: Lưu ý rằng bạn cũng nên kiểm tra strtham số null, vì các phương thức mở rộng có thể được gọi trên nullcác giá trị, vì chúng chỉ là đường cú pháp.
Lucas Trzesniewski

2
yield returnVề nguyên tắc là một ý tưởng hay, nhưng nó có quá nhiều lỗi kỳ lạ. Cảm ơn vì đã đưa cái này ra ánh sáng!
nateirvin

Vì vậy, về cơ bản một lỗi sẽ được ném ra nếu bộ kiểm tra được chạy, như trong phần trước?
MVCDS

1
@MVCDS Chính xác. MoveNextđược gọi dưới mui xe bởi foreachcấu trúc. Tôi đã viết giải thích về những gì foreachcó trong câu trả lời của tôi giải thích ngữ nghĩa bộ sưu tập nếu bạn muốn xem mẫu chính xác.
Lucas Trzesniewski

34

Bạn có một khối trình lặp. Không có mã nào trong phương thức đó được chạy bên ngoài các lệnh gọi đến MoveNexttrên trình lặp được trả về. Việc gọi phương thức không ghi chú nhưng tạo ra máy trạng thái và điều đó sẽ không bao giờ bị lỗi (ngoài các trường hợp cực đoan như lỗi hết bộ nhớ, tràn ngăn xếp hoặc ngoại lệ hủy bỏ luồng).

Khi bạn thực sự cố gắng lặp lại trình tự, bạn sẽ nhận được các ngoại lệ.

Đây là lý do tại sao các phương thức LINQ thực sự cần hai phương thức để có ngữ nghĩa xử lý lỗi mà chúng mong muốn. Chúng có một phương thức riêng là một khối trình vòng lặp và sau đó là một phương thức khối không trình vòng lặp không làm gì khác ngoài việc xác thực đối số (để nó có thể được thực hiện một cách hăng hái, thay vì bị trì hoãn) trong khi vẫn trì hoãn tất cả các chức năng khác.

Vì vậy, đây là mô hình chung:

public static IEnumerable<T> Foo<T>(
    this IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //note, not an iterator block
    if(anotherArgument == null)
    {
        //TODO make a fuss
    }
    return FooImpl(source, anotherArgument);
}

private static IEnumerable<T> FooImpl<T>(
    IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //TODO actual implementation as an iterator block
    yield break;
}

0

Các điều tra viên, như những người khác đã nói, không được đánh giá cho đến khi họ bắt đầu được liệt kê (tức là IEnumerable.GetNextphương thức được gọi). Vì vậy, điều này

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

không được đánh giá cho đến khi bạn bắt đầu liệt kê, tức là

foreach(int index in indexes)
{
    // ArgumentNullException
}
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.