【问题标题】:Alternative to IEnumerable<T>.Skip(1).Take(1).Single()IEnumerable<T>.Skip(1).Take(1).Single() 的替代方案
【发布时间】:2011-04-05 05:09:30
【问题描述】:

我遇到了一个看似简单而令人尴尬的问题。我想要的是不使用 Skip(1).Take(1).Single() 的 IEnumberable 中的下一个元素。这个例子说明了基本问题。

private char _nextChar;
private IEnumerable<char> getAlphabet()
{
    yield return 'A';
    yield return 'B';
    yield return 'C';
}
public void sortAlphabet()
{
     foreach (char alpha in getAlphabet())
     {
         switch (alpha)
         {
             case 'A':  //When A pops up, I want to get the next element, ie 'B'
                 _nextChar = getAlphabet().Skip(1).Take(1).Single();
                 break;
             case 'B': //When B pops up, I want 'C' etc
                 _nextChar = getAlphabet().Skip(1).Take(1).Single();
                 break;
         }
     }
}

除了丑陋之外,这个例子还有效。但是假设 IEnumerable 包含 200 万个元素,那么 LINQ 语句会使程序执行速度慢得令人难以忍受。我想要的很简单。我只想要 IEnumberable 中的下一个元素。如果有这样的功能,我所有的问题都会得到解决:

_nextChar = getAlphabet().moveNext() //or getNext()

如果解决方案保持与示例相同的结构/布局/功能,则更好,但我很灵活。我的程序是一个文件解析器,在 200 万行文本中有一些键,例如“money=324”,其中“money”和“324”是 IEnumberable 中的相邻元素,当解析器遇到“money”时,我想要“ 324"。 (谁不呢?:D 对不起,双关语不好。)

【问题讨论】:

  • 这段代码将始终检索序列的第二个元素,即'B',我认为这不是你想要的......
  • 票数最高的答案已经回答了您的问题。但是,为了将来参考,您可以使用 .First() 而不是 .Take(1).Single(),它做同样的事情。
  • "假设 IEnumerable 包含 200 万个元素,那么 LINQ 语句会使程序执行速度慢得令人无法忍受。"是什么让你这么认为?
  • 您应该改为解析可枚举的对。这很容易。
  • 如果您正在编写文件解析器,使用 ANTLR、flex/bison 或开源 INI 阅读器等现成系统不是更容易吗?

标签: c# linq foreach ienumerable yield


【解决方案1】:

如果我的所有问题都将得到解决 有一个类似的功能:

_nextChar = getAlphabet().moveNext() //or getNext()

有一个正是这样的功能。它只是属于IEnumerator&lt;T&gt;,而不是IEnumerable&lt;T&gt;

private char _nextChar;
private IEnumerable<char> getAlphabet()
{
    yield return 'A';
    yield return 'B';
    yield return 'C';
}

public void sortAlphabet()
{
    using (var enumerator = getAlphabet().GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            char alpha = enumerator.Current;
            switch (alpha)
            {
                case 'A':
                    if (enumerator.MoveNext())
                    {
                        _nextChar = enumerator.Currrent;
                    }
                    else
                    {
                        // You decide what to do in this case.
                    }
                    break;
                case 'B':
                    // etc.
                    break;
            }
        }
    }
}

不过,这里有一个问题要问你。这段代码是否有必要使用IEnumerable&lt;char&gt;,而不是IList&lt;char&gt;?我问是因为,好像这并不明显,如果您可以通过索引随机访问 getAlphabet 返回的项目,代码会简单得多(如果有人想指出您可以使用 @987654321 来做到这一点@,拜托,现在就把这个想法从你的脑海中抹去)。

我的意思是,考虑一下在这种情况下代码会是什么样子:

private char _nextChar;
private IList<char> getAlphabet()
{
    return Array.AsReadOnly(new[] { 'A', 'B', 'C' });
}

public void sortAlphabet()
{
    IList<char> alphabet = getAlphabet();
    for (int i = 0; i < alphabet.Count - 1; ++i)
    {
        char alpha = alphabet[i];
        switch (alpha)
        {
            case 'A':
                _nextChar = alphabet[i + 1];
                break;
            case 'B':
                // etc.
                break;
        }
    }
}

这不是更容易吗?

【讨论】:

  • 您的第一个答案正是我所寻找的(有时对某事的全新看法正是人们所寻找的)。但是,我对您的建议感到有些困惑。我不想随机访问。在这种情况下,我想要来信。而且,我可能错了,getAlphabet() 不是将全部内容保存在内存中吗?我不希望这样,因为在我的情况下 getAlphabet() 会保存 200 万行。
  • 啊,我刚刚意识到可能会有一些混乱。字母表只是我的问题的一个例子。实际上,我不会对字母表或字符做任何事情。如果希望我可以提供相关代码,但您已经回答了我的问题。
  • @Nick:无需进一步澄清。甚至在发布我的建议后不久,我就记得你说过 200 万条;所以显然在你的情况下将内容存储在内存中是没有意义的。不过,我将把这个想法留在那里,以防其他人发现他/她自己在按索引访问集合成员实际上可能是一种选择的情况下面临类似问题。
【解决方案2】:

我想你想要这个:

    public void sortAlphabet() {
        using (var enu = getAlphabet().GetEnumerator()) {
            while (enu.MoveNext()) {
                switch (enu.Current) {
                    case 'A':
                        enu.MoveNext();
                        _nextChar = enu.Current;
                        break;
                }
            }
        }
    }

请注意,这会消耗下一个元素,如果我没看错你的问题,这正是你想要的。

【讨论】:

  • 是的,如果你忘记了主板会着火的。
  • 总是。通用 IEnumerable 接口继承自 IDisposable。
  • @Timwi, @SealedSun:理论上是的。在实践中,大多数IEnumerator&lt;T&gt; 实现在他们的Dispose 方法中并没有真正任何事情;它只是在那里,以防万一。显然,当您处理一些任意的IEnumerator&lt;T&gt; 类型时,使用using 是一个好习惯;但我认为 Hans 的原始答案确实没有问题,因为在提供的示例代码中,枚举数的类型实际上是已知的,很明显 Dispose 方法对该类型没有任何作用。
【解决方案3】:

正如在另一个答案中指出的那样,一个MoveNext() 方法,您可以通过调用IEnumerable&lt;T&gt;.GetEnumerator() 返回的IEnumerator&lt;T&gt; 接口访问所有可枚举项。但是,使用MoveNext()Current 会感觉有点“低级”。

如果您希望使用 foreach 循环来处理您的 getAlphabet() 集合,您可以编写一个扩展方法,以两个成对的形式从任何可枚举元素中返回元素:

public static IEnumerable<T[]> InPairsOfTwo<T>(this IEnumerable<T> enumerable)
{
    if (enumerable.Count() < 2) throw new ArgumentException("...");

    T lastItem = default(T);
    bool isNotFirstIteration = false;

    foreach (T item in enumerable)
    {
        if (isNotFirstIteration)
        {
            yield return new T[] { lastItem, item };
        }
        else
        {
            isNotFirstIteration = true;
        }
        lastItem = item;
    }
}

您可以按如下方式使用它:

foreach (char[] letterPair in getAlphabet().InPairsOfTwo())
{
    char currentLetter = letterPair[0],
         nextLetter    = letterPair[1];        

    Console.WriteLine("#  {0}, {1}", currentLetter, nextLetter);
}

你会得到以下输出:

#  A, B
#  B, C

(请注意,虽然上面的扩展方法每个返回两个项目对,但这些对 重叠 一个项目!你基本上得到每个项目以及一个前瞻。如果你想要扩展方法自己返回最后一项,您可以通过调整使用的缓冲方法来调整它。)

【讨论】:

    【解决方案4】:

    如前所述,状态机非常适合这些情况。 它也符合我们思考问题的方式,因此可读性非常好。 我不确定你想对代码做什么,所以下面的例子在遇到下一个字符时返回它。状态机可以很好地适应复杂的任务,并且可以在纸上进行建模和手动检查。

    enum State
    {
        Scan,
        SaveAndExit
    };
    
    public void SortAlphabet()
    {
        State state = State.Scan; // initialize
    
        foreach(char c in getAlphabet())
        {
            switch (state):
            {
                case State.Scan:
                    if (c == 'A' ||
                        c == 'B')
                        state = State.SaveAndExit;
                    break;
                case State.SaveAndExit:
                    return (c);
                    break;
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      您的代码每次都会返回'B',因为您调用getAlphabet(),它每次都会返回一个新的IEnumerable

      根据您要执行的操作,我可能会建议使用基于索引的迭代而不是枚举器。如果你使用MoveNext 来获取下一个元素,你会弄乱你的循环,所以使用基于索引的检索会更干净,开销更少。

      【讨论】:

        【解决方案6】:

        如果您使用的是 .NET 4.0,那么您想要实现的目标非常简单:

        var alphabet = getAlphabet();
        var offByOneAlphabet = alphabet.Skip(1);
        
        foreach (var pair in alphabet.Zip(offByOneAlphabet, (a, b) => Tuple.Create(a, b)))
            Console.WriteLine("Letter: {0}, Next: {1}", pair.Item1, pair.Item2);
        
        // prints:
        //    Letter: A, Next: B
        //    Letter: B, Next: C
        

        如果您使用低于 .NET 4.0 的版本,define your own Zip function 和 Tuple 类仍然非常容易。

        【讨论】:

          猜你喜欢
          • 2010-09-07
          • 2017-07-15
          • 1970-01-01
          • 1970-01-01
          • 2013-08-13
          • 2018-03-05
          • 1970-01-01
          • 2017-05-27
          • 1970-01-01
          相关资源
          最近更新 更多