Bu dize uzantısı yöntemi neden bir istisna atmıyor?


119

IEnumerable<int>Bir dizedeki bir alt dizenin tüm dizinlerini döndürmesi gereken bir C # dize genişletme yöntemim var . Amaçlanan amaç için mükemmel bir şekilde çalışıyor ve beklenen sonuçlar geri dönüyor (aşağıdaki test olmasa da testlerimden biri tarafından kanıtlandığı gibi), ancak başka bir birim testi bununla ilgili bir sorun keşfetti: boş argümanları işleyemez.

Test ettiğim uzantı yöntemi şudur:

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

Sorunu işaretleyen test şu şekildedir:

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

Test benim uzantı yöntemime karşı çalıştığında, yöntemin "bir istisna oluşturmadığını" belirten standart hata mesajı ile başarısız oluyor.

Bu kafa karıştırıcı: İşleve açıkça geçtim null, ancak bazı nedenlerden dolayı karşılaştırma null == nullgeri dönüyor false. Bu nedenle, hiçbir istisna atılmaz ve kod devam eder.

Bunun testte bir hata olmadığını doğruladım: ana projemde yöntemi Console.WriteLineboş karşılaştırma ifbloğunda bir çağrı ile çalıştırırken, konsolda hiçbir şey gösterilmiyor ve catcheklediğim herhangi bir blok tarafından bir istisna yakalanmıyor . Ayrıca, string.IsNullOrEmptyyerine kullanmak == nullda aynı soruna sahiptir.

Bu sözde basit karşılaştırma neden başarısız oluyor?


5
Kodu aşmayı denediniz mi? Bu muhtemelen oldukça hızlı bir şekilde çözülecektir.
Matthew Haugen

1
Ne mu olur? ( Bir istisna mı
atıyor

@ user2864740 Olan her şeyi anlattım. İstisna yok, sadece başarısız bir test ve bir çalıştırma yöntemi.
ArtOfCode

7
Yineleyiciler, yinelenene kadar yürütülmez
BlueRaja - Danny Pflughoeft

2
Rica ederim. Bu aynı zamanda Jon'un "en kötü yakaladığı" listesini yaptı: stackoverflow.com/a/241180/88656 . Bu oldukça yaygın bir sorundur.
Eric Lippert

Yanıtlar:


158

Kullanıyorsun yield return. Bunu yaparken, derleyici yönteminizi bir durum makinesini uygulayan üretilmiş bir sınıfı döndüren bir işleve yeniden yazar.

Genel olarak konuşursak, yerelleri o sınıfın alanlarına yeniden yazar ve algoritmanızın yield returntalimatlar arasındaki her parçası bir durum haline gelir. Derlemeden sonra bu yöntemin ne hale geldiğini bir derleyici ile kontrol edebilirsiniz (üretecek akıllı derlemeyi kapattığınızdan emin olun yield return).

Ancak sonuç şu: yönteminizin kodu, siz yinelemeye başlayana kadar çalıştırılmayacaktır.

Ön koşulları kontrol etmenin olağan yolu, yönteminizi ikiye bölmektir:

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

Bu işe yarar çünkü ilk yöntem beklediğiniz gibi davranır (anında yürütme) ve ikinci yöntem tarafından uygulanan durum makinesini döndürür.

Ayrıca kontrol etmesi gerektiğini unutmayın striçin parametre nulluzantıları yöntemler nedeniyle, olabilir çağrılacak nulldeğerler onlar sadece sözdizimsel şeker konum olarak.


Derleyicinin kodunuza ne yaptığını merak ediyorsanız, derleyicinin ürettiği Kodu Göster seçeneğini kullanarak dotPeek ile derlenen yönteminiz burada .

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

Bu geçersiz C # kodudur, çünkü derleyicinin dilin izin vermediği, ancak IL'de yasal olan şeyleri yapmasına izin verilir - örneğin değişkenleri ad çakışmalarını önleyemeyeceğiniz şekilde adlandırmak.

Ancak görebileceğiniz gibi, AllIndexesOfyalnızca kurucusu yalnızca bir durumu başlatan bir nesneyi oluşturur ve döndürür. GetEnumeratoryalnızca nesneyi kopyalar. Asıl iş, numaralandırmaya başladığınızda ( MoveNextyöntemi çağırarak ) yapılır.


9
BTW, cevaba şu önemli noktayı ekledim: Parametreyi de kontrol etmelisiniz , çünkü uzantı yöntemleri sadece sözdizimsel şeker oldukları için değerler üzerinde çağrılabilir . strnullnull
Lucas Trzesniewski

2
yield returnprensipte güzel bir fikir, ama pek çok tuhaf aldatmacası var. Bunu gün ışığına çıkardığınız için teşekkürler!
nateirvin

Yani, numaralandırıcı bir foreach'ta olduğu gibi çalıştırılsaydı, temelde bir hata atılırdı?
MVCDS

1
@MVCDS Kesinlikle. yapı MoveNexttarafından kaputun altına denir foreach. Ben ne bir açıklama yazdı foreachyapar toplama anlambilim açıklayan Cevabıma Eğer tam olarak işe görmek istiyorum eğer.
Lucas Trzesniewski

34

Yineleyici bloğunuz var. Bu yöntemdeki kodların hiçbiri MoveNext, döndürülen yineleyicideki çağrıların dışında çalıştırılmaz . Yöntemin çağrılması durum makinesini yaratmaz, ancak yaratmaz ve bu hiçbir zaman başarısız olmaz (bellek yetersizliği hataları, yığın taşmaları veya iş parçacığı iptali istisnaları gibi aşırılıkların dışında).

Diziyi gerçekten yinelemeye çalıştığınızda istisnaları alırsınız.

LINQ yöntemlerinin arzu ettikleri hata işleme anlambilimine sahip olmak için aslında iki yönteme ihtiyaç duymasının nedeni budur. Bir yineleyici bloğu olan özel bir yönteme ve diğer tüm işlevselliği hala ertelemeye devam ederken argüman doğrulamasından başka bir şey yapmayan (ertelenmek yerine hevesle yapılabilmesi için) yineleyici olmayan bir blok yöntemine sahiptirler.

İşte genel model şu:

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

Numaralandırıcılar, diğerlerinin de söylediği gibi, numaralandırılmaya başladıkları zamana kadar değerlendirilmezler (yani IEnumerable.GetNextyöntem çağrılır). Böylece bu

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

siz numaralandırmaya başlayana kadar değerlendirilmez, yani

foreach(int index in indexes)
{
    // ArgumentNullException
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.