เหตุใดวิธีการขยายสตริงนี้จึงไม่เกิดข้อยกเว้น


119

ฉันมีวิธีการขยายสตริง C # ที่ควรส่งคืนIEnumerable<int>ดัชนีทั้งหมดของสตริงย่อยภายในสตริง มันทำงานได้อย่างสมบูรณ์ตามวัตถุประสงค์ที่ตั้งใจไว้และผลลัพธ์ที่คาดหวังจะถูกส่งกลับ (ตามที่ได้รับการพิสูจน์โดยหนึ่งในการทดสอบของฉันแม้ว่าจะไม่ใช่แบบทดสอบด้านล่างก็ตาม) แต่การทดสอบหน่วยอื่นพบปัญหา: ไม่สามารถจัดการอาร์กิวเมนต์ว่างได้

นี่คือวิธีการขยายที่ฉันกำลังทดสอบ:

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;
    }
}

นี่คือการทดสอบที่ตั้งค่าสถานะปัญหา:

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

เมื่อการทดสอบทำงานกับเมธอดส่วนขยายของฉันจะล้มเหลวโดยมีข้อความแสดงข้อผิดพลาดมาตรฐานว่าเมธอด "ไม่ได้ส่งข้อยกเว้น"

นี้จะทำให้เกิดความสับสน: ฉันได้ผ่านอย่างชัดเจนnullในการทำงาน แต่ด้วยเหตุผลบางอย่างเปรียบเทียบจะกลับมาnull == null falseดังนั้นจึงไม่มีการโยนข้อยกเว้นและรหัสจะดำเนินต่อไป

ฉันยืนยันว่านี่ไม่ใช่ข้อผิดพลาดจากการทดสอบ: เมื่อเรียกใช้เมธอดในโปรเจ็กต์หลักของฉันด้วยการโทรไปConsole.WriteLineที่ifบล็อกการเปรียบเทียบค่า null ไม่มีสิ่งใดปรากฏบนคอนโซลและcatchบล็อกใด ๆ ที่ฉันเพิ่มก็ไม่มีข้อยกเว้นใด ๆ นอกจากนี้การใช้string.IsNullOrEmptyแทน== nullก็มีปัญหาเช่นเดียวกัน

เหตุใดการเปรียบเทียบแบบธรรมดานี้จึงล้มเหลว


5
คุณลองก้าวผ่านรหัสแล้วหรือยัง? นั่นอาจจะทำให้มันได้รับการแก้ไขอย่างรวดเร็ว
Matthew Haugen

1
สิ่งที่ไม่เกิดขึ้น? (มันมีข้อยกเว้นหรือไม่ถ้าเป็นเช่นนั้นอันไหนและบรรทัดอะไร)
user2864740

@ user2864740 ฉันได้อธิบายทุกอย่างที่เกิดขึ้น ไม่มีข้อยกเว้นเพียงแค่การทดสอบที่ล้มเหลวและวิธีการรัน
ArtOfCode

7
ตัวทำซ้ำจะไม่ดำเนินการจนกว่าจะมีการทำซ้ำ
BlueRaja - Danny Pflughoeft

2
ยินดีต้อนรับ หนึ่งนี้ยังทำให้จอน "gotcha ที่เลวร้ายที่สุด" รายการ: stackoverflow.com/a/241180/88656 นี่เป็นปัญหาที่พบได้บ่อย
Eric Lippert

คำตอบ:


158

คุณกำลังใช้yield return. เมื่อทำเช่นนั้นคอมไพเลอร์จะเขียนเมธอดของคุณใหม่เป็นฟังก์ชันที่ส่งคืนคลาสที่สร้างขึ้นซึ่งใช้เครื่องสถานะ

พูดอย่างกว้าง ๆ มันเขียนภาษาท้องถิ่นไปยังฟิลด์ของคลาสนั้นและแต่ละส่วนของอัลกอริทึมของคุณระหว่างyield returnคำสั่งจะกลายเป็นสถานะ คุณสามารถตรวจสอบกับผู้ถอดรหัสว่าวิธีนี้กลายเป็นอย่างไรหลังจากการคอมไพล์ (ตรวจสอบให้แน่ใจว่าได้ปิดการแยกส่วนอัจฉริยะซึ่งจะก่อให้เกิดyield return)

แต่สิ่งที่สำคัญที่สุดคือโค้ดของวิธีการของคุณจะไม่ถูกเรียกใช้จนกว่าคุณจะเริ่มทำซ้ำ

วิธีปกติในการตรวจสอบเงื่อนไขเบื้องต้นคือการแบ่งวิธีของคุณออกเป็นสองวิธี:

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;
    }
}

วิธีนี้ใช้ได้ผลเนื่องจากวิธีแรกจะทำงานเหมือนกับที่คุณคาดไว้ (การดำเนินการทันที) และจะส่งคืนเครื่องสถานะที่ใช้งานโดยวิธีที่สอง

โปรดทราบว่าคุณควรตรวจสอบstrพารามิเตอร์nullด้วยเนื่องจากเมธอดส่วนขยายสามารถเรียกใช้nullค่าได้เนื่องจากเป็นเพียงแค่น้ำตาลที่เป็นประโยค


หากคุณสงสัยเกี่ยวกับสิ่งที่คอมไพเลอร์ทำกับโค้ดของคุณนี่คือวิธีการของคุณโดยแยกคอมไพล์ด้วย dotPeek โดยใช้ตัวเลือกShow Compiler- created Code

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()
  {
  }
}

รหัส C # ไม่ถูกต้องเนื่องจากคอมไพเลอร์ได้รับอนุญาตให้ทำในสิ่งที่ภาษาไม่อนุญาต แต่ถูกกฎหมายใน IL เช่นการตั้งชื่อตัวแปรในลักษณะที่คุณไม่สามารถหลีกเลี่ยงการชนกันของชื่อได้

แต่อย่างที่คุณเห็นสิ่งAllIndexesOfเดียวที่สร้างและส่งคืนอ็อบเจ็กต์ซึ่งตัวสร้างจะเริ่มต้นบางสถานะเท่านั้น GetEnumeratorคัดลอกวัตถุเท่านั้น งานจริงจะเสร็จสิ้นเมื่อคุณเริ่มแจกแจง (โดยเรียกใช้MoveNextเมธอด)


9
BTW ฉันได้เพิ่มประเด็นสำคัญต่อไปนี้ลงในคำตอบโปรดทราบว่าคุณควรตรวจสอบstrพารามิเตอร์nullด้วยเนื่องจากเมธอดส่วนขยายสามารถเรียกใช้nullค่าได้เนื่องจากเป็นเพียงน้ำตาลวากยสัมพันธ์
Lucas Trzesniewski

2
yield returnโดยหลักการแล้วเป็นความคิดที่ดี แต่มี gotcha แปลก ๆ มากมาย ขอขอบคุณที่นำสิ่งนี้มาสู่แสงสว่าง!
nateirvin

ดังนั้นโดยทั่วไปข้อผิดพลาดจะถูกโยนออกไปหาก enumarator ถูกเรียกใช้เช่นเดียวกับใน foreach?
MVCDS

1
@MVCDS เป๊ะ. MoveNextถูกเรียกว่าภายใต้ประทุนโดยforeachโครงสร้าง ฉันเขียนคำอธิบายว่าอะไรforeachในคำตอบของฉันเพื่ออธิบายความหมายของคอลเลกชันหากคุณต้องการดูรูปแบบที่แน่นอน
Lucas Trzesniewski

34

คุณมีบล็อกตัววนซ้ำ ไม่มีรหัสใดในวิธีการดังกล่าวถูกเรียกใช้นอกการเรียกไปยังMoveNextตัวทำซ้ำที่ส่งคืน การเรียกใช้เมธอด แต่สร้าง state machine ขึ้นมาและจะไม่ล้มเหลวเลย (นอกสุดขั้วเช่นข้อผิดพลาดหน่วยความจำไม่เพียงพอสแต็กล้นหรือข้อยกเว้นการยกเลิกเธรด)

เมื่อคุณพยายามทำซ้ำตามลำดับคุณจะได้รับข้อยกเว้น

นี่คือเหตุผลที่เมธอด LINQ ต้องการสองวิธีเพื่อให้มีข้อผิดพลาดในการจัดการความหมายตามที่ต้องการ พวกเขามีเมธอดส่วนตัวที่เป็นบล็อกตัววนซ้ำและวิธีการบล็อกที่ไม่ใช่ตัววนซ้ำซึ่งไม่ทำอะไรเลยนอกจากทำการตรวจสอบความถูกต้องของอาร์กิวเมนต์ (เพื่อให้สามารถดำเนินการได้อย่างกระตือรือร้นแทนที่จะรอการตัดบัญชี) ในขณะที่ยังคงเลื่อนการทำงานอื่น ๆ ทั้งหมดออกไป

นี่คือรูปแบบทั่วไป:

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

ตัวแจงนับตามที่คนอื่น ๆ กล่าวไว้จะไม่ได้รับการประเมินจนกว่าจะถึงเวลาที่พวกเขาเริ่มแจกแจง (กล่าวIEnumerable.GetNextคือเรียกวิธีการ) ด้วยประการฉะนี้

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

จะไม่ได้รับการประเมินจนกว่าคุณจะเริ่มแจกแจงเช่น

foreach(int index in indexes)
{
    // ArgumentNullException
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.