C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

IEnumerableとForeachについて

[トピック内 3 記事 (1 - 3 表示)]  << 0 >>

■89778 / inTopicNo.1)  IEnumerableとForeachについて
  
□投稿者/ Study (1回)-(2018/12/24(Mon) 23:28:19)

分類:[C#] 

はじめまして

.Net Framework 4.6.1

下記ソースコードを実行すると、
foreachの2週目のタイミングで例外が発生してしまいます。

foreachに入る前にOutlineArticles.Count()を行ったり、
OutlineArticles.ToList()をforeachに渡したりすると例外は発生しないです。

調べていると遅延評価というのが問題っぽい感じがしているのですが、
・.Count()をしたところでforeachで再評価されそうで、結果は変わらなそうなのになぜかうまくいく
・なぜforeach最初の1回目のループだけはうまくいくのか
などまだまだ理解できておりません。

どなたかお詳しい方がいらっしゃいましたら、ご教授いただけませんでしょうか。

using HtmlAgilityPack;
using System.Linq;

namespace IEnumerableTest
{
class Program
{
static void Main(string[] args)
{
string str = "< div class=\"ently_outline\">aaa</div><div class=\"ently_outline\">bbb</div><div class=\"ently_outline\">ccc</div><div class=\"ently_outline\">eee</div>";
HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(str);

var OutlineArticles = from outline in htmlDocument.DocumentNode.SelectNodes("//div[@class='ently_outline']")
select new
{
Html = outline.OuterHtml
};

foreach (var outline in OutlineArticles)
{
htmlDocument.LoadHtml(outline.Html);
}
}
}
}

System.ArgumentOutOfRangeException
HResult=0x80131502
Message=startIndex に文字列の長さより大きい値を指定することはできません。
パラメーター名:startIndex
Source=mscorlib
スタック トレース:
場所 System.String.Substring(Int32 startIndex, Int32 length)
場所 HtmlAgilityPack.HtmlNode.get_OuterHtml()
場所 HtmlAgilityPack.HtmlNode.WriteTo(TextWriter outText, Int32 level)
場所 HtmlAgilityPack.HtmlNode.WriteContentTo(TextWriter outText, Int32 level)
場所 HtmlAgilityPack.HtmlNode.WriteContentTo()
場所 HtmlAgilityPack.HtmlNode.get_OuterHtml()
場所 IEnumerableTest.Program.<>c.<Main>b__0_0(HtmlNode outline)
場所 System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
場所 IEnumerableTest.Program.Main(String[] args)

引用返信 編集キー/
■89779 / inTopicNo.2)  Re[1]: IEnumerableとForeachについて
□投稿者/ Hongliang (733回)-(2018/12/25(Tue) 05:35:39)
SelectNodes()は即時評価なので、HtmlDocumentは直ちにマッチするノードを表すオブジェクトの配列を生成します。スタックトレースから察するに、各ノードは直接は文字列を持たず、HtmlDocumentが保持しているHTML全体文字列の、そのノードが該当する部分の開始位置と長さを保持しているのでしょう。
selectはEnumerable.Selectに展開されますが、これは遅延評価なので、その内部で行っている処理はforeachの中で要素が列挙されるたびに実行されます。今回の場合はOuterHtmlの参照ですね。

さて、foreachの中でLoadHtmlを呼び出しています。これのために、HtmlDocumentの保持しているHTML全体文字列が変更されます。
そうするとループの2周目において、遅延評価されるOuterHtmlをこのとき実行しようとしますが、元々のHTML全体文字列における開始位置と長さにある部分から取得しようとして、しかしHTML全体文字列が変更されているため、想定外の文字列取得処理となってしまいます。

既存のHtmlDocumentにLoadHtmlするのはこういう変な事態を起こしやすいので、新しいHTMLは新しいHtmlDocumentにロードしてやるべきです。
引用返信 編集キー/
■89783 / inTopicNo.3)  Re[2]: IEnumerableとForeachについて
□投稿者/ Study (2回)-(2018/12/25(Tue) 23:07:08)
No89779 (Hongliang さん) に返信
> SelectNodes()は即時評価なので、HtmlDocumentは直ちにマッチするノードを表すオブジェクトの配列を生成します。スタックトレースから察するに、各ノードは直接は文字列を持たず、HtmlDocumentが保持しているHTML全体文字列の、そのノードが該当する部分の開始位置と長さを保持しているのでしょう。
> selectはEnumerable.Selectに展開されますが、これは遅延評価なので、その内部で行っている処理はforeachの中で要素が列挙されるたびに実行されます。今回の場合はOuterHtmlの参照ですね。
>
> さて、foreachの中でLoadHtmlを呼び出しています。これのために、HtmlDocumentの保持しているHTML全体文字列が変更されます。
> そうするとループの2周目において、遅延評価されるOuterHtmlをこのとき実行しようとしますが、元々のHTML全体文字列における開始位置と長さにある部分から取得しようとして、しかしHTML全体文字列が変更されているため、想定外の文字列取得処理となってしまいます。
>
> 既存のHtmlDocumentにLoadHtmlするのはこういう変な事態を起こしやすいので、新しいHTMLは新しいHtmlDocumentにロードしてやるべきです。

Hongliangさん

教えていただいたように試してみるとうまくいきました!
非常にわかりやすいご説明ありがとうございます!
解決済み
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ