2010年12月31日金曜日

Feedのアドレス

ちょっとあまりにも配信に時差がでちゃうので、Feedのアドレス変えます。すません。

Blogpost:無聊を託つ

http://feeds.feedburner.com/blogspot/jNRCtw

Tumblr:等閑に付さないように

http://feeds.feedburner.com/tumblr/ijTN

旧FeedのアドレスはTumblrの方に向けます。ホントすません。

sorry009

2010年12月30日木曜日

問題だと思っていることはホントに問題なの?

すっかり忘れてました。新しく開発者を採用するにあたり、会社の方針に合わせてコードを書いてもらいました。で、採用担当としてこんな問題を考えました。

問題内容

2つの文字列を与えられた時、それぞれの文字列の一致率を0~1の間で算出する関数を作成せよ。
※一致率の定義の仕様も含めて考えてください。

いろいろ考え方や思うところはあると思いますが、今回に関しては問題を解く過程をとても重視しました。どういうふうに仕様を決めるのかも含め、です。内容からして勝手に仕様を決めていいように思われるかもしれないですが、そこの解釈も重視です。与えられた情報の範囲内でどこまで飛べるのか、みたいな。

解答例

public static decimal Rate(string value1, string value2)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }
        
        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

最初のほうはエラーチェックだから、今回別に無くてもよかったです。

あれっすよ、こんなの10分やそこらでコード化するもんじゃないかもね。でも、考え方というか解き方というか、問題はどこでどういう回答をビジネスオーナーは望んでいるのか、ですよね。

問題を考えたものとしては、この問題で先頭一致の割合だけでとくのが一番簡単なのは分かってて、それだけじゃない解き方を答えて欲しいな~、と思ってました。

“どのくらい一致しているか”

の定義は様々だけど「全体文字数に占める一致文字数の割合」が手頃な回答ですよね。んじゃ、一致文字数というのはどういう意味だと解釈するのが面白いか。文字列を前半・中央・後半の3つの集合として考えて、それぞれの文字列の比較開始位置と一致文字数を数えていけば、それらしい数値を出すこと方法として面白いんじゃないかと思うんだけど、どうかな。

前半 中央 後半 文字列1 文字列2

AABBCC AABBCC

AABBCC AABBzz
※AABBも同じ

zzBBCC yyBBCC
※BBCCも同じ

AAzzCC AAyyCC
※AACCも同じ

  zzAAyy xxAAww

※前半だけ、後半だけは前半+中央と中央+後半と同じ。

こんな感じのパターン。ちなみにこのパターンを答えた人がひとりだけいましたよ。なんで同じ答えにたどり着いたのかというと、そういう定義をオーナーが望んでると聞き出したからです。そりゃ同じになるわね。

じゃー、このコードがホントにそれっぽく一致率がでるのか確認するテストコード。

[TestMethod]
public void 空文字渡し()
{
	var rate = MatchRate.Rate(null, null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate(null, "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "AA");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", "");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 空白渡し()
{
	var rate = MatchRate.Rate(" ", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "  ");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", " ");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 完全一致()
{
	var rate = MatchRate.Rate("AA", "AA");
	Assert.AreEqual(1m, rate);

	rate = MatchRate.Rate("aa", "aa");
	Assert.AreEqual(1m, rate);
}

[TestMethod]
public void 前方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "AABB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
    public void 後方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BBAA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 中央一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BAAB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "ABBA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で75パーセント()
{
  var rate = MatchRate.Rate("ABCDE", "ADE");
  Assert.AreEqual(6m / 8m, rate);
}

だいたいいい感じなんじゃないっすかね。直感的にそんな感じ。セマンティックとか関係なく、ですから。問題はここまでで終了。

だたひとり答えにたどり着いたクマさんに拍手。

ここからさらに考え方を進めてみましょう。もし文字列が文章だとしたら。文章だと”の”とか”を”とかが入ってくるから、普通に一致させるだけだと、それっぽくならないですね。なので、このアルゴリズムを再帰に改造して、前半を無視したほうが一致率が上がるなら無視するというふうに改造してみます。何でかというと、このアルゴリズムだと一致率は上昇して頂点になったあと下降するっていう山型になるはずだからです。現在の位置と次の位置との比較で一致率が上昇してるなら、現在の位置は無視したほうがそれっぽい数値に近づくってことです。そうするとコピペ文章なんじゃん?っていうものに関しての判定が”それっぽく”できます。あくまで”それっぽく”ですよ。

public static decimal Rate(string value1, string value2)
{
    return Rate(value1, value2, null);
}

public static Func<string, int, string, int, bool> RecursiveCommand = (val1, idx1, val2, idx2) =>
{
    var p1Rate = Rate(val1.Substring(idx1), val2.Substring(idx2), null);
    var p1IncRate = Rate(val1.Substring(idx1 + 1), val2.Substring(idx2), null);
    return p1Rate < p1IncRate;
};

public static decimal Rate(string value1, string value2, Func<string, int, string, int, bool> command)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        // p1とp1+1で比較してp1+1のほうがRateがよければp1はスキップする
        if (command != null)
        {
            p1 += command(val1, p1, val2, p2Index) ? 1 : 0;
        }

        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }

        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

で、最初のテストがパスするのを確認したうえで、以下のテスト。

[TestMethod]
public void 再帰のほうがそれっぽくなる()
{
  var rate = MatchRate.Rate(
      "ABCDEF",
      "AECDEF"
  );
  var rateR = MatchRate.Rate(
      "ABCDEF",
      "AECDEF",
      MatchRate.RecursiveCommand
  );
  Console.WriteLine("Rate={0} < RateR={1}", rate, rateR);
  Assert.IsTrue(rate < rateR);
}

[TestMethod]
public void コピペしてちょっと手直しした文章()
{
  var rate = MatchRate.Rate(
		"衆院は31日の夜、会議を開き、日本郵政3社への再編などを柱とする郵政改革法案を賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議し、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
		"衆院は31日深夜、本会議を開き、日本郵政グループの3社への再編などを柱とする郵政改革法案を民主、国民新党などの賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議を終え、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
            MatchRate.RecursiveCommand
	);
	Console.WriteLine(rate);
}

ソースアップしておきまーす。何かの役に立つコードではないけどね!

2010年12月29日水曜日

Razor Templating Engine

Razor Templating Engine

CodePlexで新しいのが出てたよ~。面白いね~。テンプレートエンジンっていろいろ用途があるけど需要はどーなんだろね。まぁ、いいや。Razor記法でWeb以外にも使えるって楽しいっすよね。だっていろいろ覚えなくて済むし。ちなみにASP.NETの<%...%>とか<%$…%>とか<%#…%>なんかの記法も用途特化したテンプレートエンジンが解釈してクラスファイルを生成してますよね。

んでね、Razorは純粋にテンプレートエンジンって言ってるくらいだからWebのコンテキストや実行Hostに依存しないわけですね。つまりConsoleアプリでも使えるってことです。もちろんHtmlHelperとかWeb関連のアセンブリに依存するものはあるとしても、それ自体はRazorの機能じゃなくてHelperの機能。TemplateBaseクラスが生成されるクラスの親クラスになるって言う仕組みなので、親クラスにいろいろWeb関連の機能を保持するっていう、Razorとは直接関係なっしんぐ。

サンプル見てみるとこんな感じですね。

rte1

string template = "Hello @Model.Name! Welcome to Razor!";
string result = Razor.Parse(template, new { Name = "World" });

Console.WriteLine(result);
Console.ReadLine();

簡単ですね~。お気楽です。

同じくサンプルのHtmlHelperを使うパターン。

rte2

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
string template =
@"<html>
    <head>
    <title>Hello @Model.Name</title>
    </head>
    <body>
    Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };
string result = Razor.Parse(template, model);

Console.WriteLine(result);
Console.ReadLine();

はい、これも簡単ですね。イメージ通りです。

PageModelは自分で作ってね。あと、RazorEngine.Coreだけじゃ足りなくて、RazorEngine.Templatesアセンブリも必要なので、ソースのダウンロードはここからどーぞ。

これらは全て実行時にクラスファイル(特に指定が無い限りc#)をオンメモリで生成してコンパイルします。そのタイミングでAppDomain内からはクラスのインスタンスを生成できるようになるって言う流れはASP.NETのパイプラインそのもの。

試しに最初のサンプル(Hello World!のほう)をステップ実行していくと、どういうクラス名で生成されるのかというのを追いかけていくと...。

rte3

雑ですね。Guid.NewGuid()から数字とハイフンを除外したのがクラス名。それにNamespaceをくっつけてます。で確認。

rte4

ちゃんといます。で、AppDomainの仕様を見てみるとこんなことがかかれてます。

既定のアプリケーション ドメインにロードされたアセンブリは、プロセスの実行中にメモリからアンロードすることはできません。 しかし、アプリケーション ドメインをもう 1 つ開いてアセンブリをロードおよび実行すると、そのアプリケーション ドメインがアンロードされたときに、アセンブリもアンロードされます。 このテクニックを使用すると、大きな DLL を使用する場合のある、長時間実行されるプロセスのワーキング セットを最小限に抑えることができます。

AppDomain クラス (System)

ん~、どーだろ。どうなるんだろね。

rte6

rte5

めちゃめちゃ分かりにくいですね。2個目のサンプル(HtmlHelper使うほう)を使ってRazor.Parseを繰り返してる途中です。

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

for (var i = 0; i < (singleExec ? 1 : Repeat); i++)
{
    var sw = new Stopwatch();
    sw.Start();
    var result = Razor.Parse(Template, model);
    sw.Stop();

    Console.WriteLine(
        "{0} - {1} {2} ms {3:0,###} bytes",
        result.GetHashCode(),
        i,
        sw.ElapsedMilliseconds,
        Process.GetCurrentProcess().PrivateMemorySize64);
}

少しずつメモリ使用量と動作時間が長くなっていきます。最初は200msとかで処理。すごく単純で1種類だけの生成なのにね。ちなみに一度コンパイルしたものをその後何度も使用する場合は超早い。

rte8

rte7

10000回実行してもすぐ終了。

これってつまり、ASP.NETでも同じでaspxを何度も書き換えてると処理時間はどんどん増加。いつか破綻?でもリサイクルも走るし、そもそも運用環境でそんなことしないから問題にはならないか。

AppDomainをアンロードしない限りアセンブリもアンロードされないってことはつまり常時動いてるようなシステムで頻繁にコンパイルを繰り返すのはよろしくないね!ってなりましょう。それをひっくり返すのがAppDomainを別に作ってそっちで動かす方法。

リファレンスにもちゃんとかかれてますね。ボスもタイムリーに(MEFだけど)、そんなことをブログに書いてました。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02) « kazuk は null に触れてしまった

なのでAppDomainを作って実行するように試してみた。

rte9

けど無理!遅すぎ!

var sw = new Stopwatch();
sw.Start();

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
var razor = executeAppDomain.CreateInstanceAndUnwrap(
    Assembly.GetEntryAssembly().FullName,
    typeof(RazorTemplateTest).FullName
) as RazorTemplateTest;

razor.ExecuteParse(i);

AppDomain.Unload(executeAppDomain);

sw.Stop();

Console.WriteLine(" {0} : {1} ms",
    AppDomain.CurrentDomain.FriendlyName,
    sw.ElapsedMilliseconds);

でも、まぁ、思ったような結果が出なかった残念な調査でした。ホントはもっとメモリ開放されないことを想定してたんだけどな~。

オチがない...。

using System;
using System.Diagnostics;
using System.Reflection;
using RazorEngine;
using RazorEngine.Templating;

namespace ConsoleApplication1
{
    public class PageModel
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }

    public class RazorTemplateTest : MarshalByRefObject
    {
        private static int Repeat = 10000;
        static string Template =
@"<html>
    <head>
        <title>Hello @Model.Name</title>
    </head>
    <body>
        Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

        public void ExecuteParse()
        {
            ExecuteParse(-1);
        }

        public void ExecuteParse(int counter)
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            for (var i = 0; i < (counter!=-1 ? 1 : Repeat); i++)
            {
                var sw = new Stopwatch();
                sw.Start();
                var result = Razor.Parse(Template, model);
                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    AppDomain.CurrentDomain.FriendlyName,
                    counter != -1 ? counter : i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteCompile()
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            Razor.Compile(Template, typeof(PageModel), "test");

            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                var result = Razor.Run(model, "test");

                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    result.GetHashCode(),
                    i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteAnotherAppDomain()
        {
            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                AppDomainSetup ads = new AppDomainSetup();
                ads.ApplicationBase = Environment.CurrentDirectory;
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;
                ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

                AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
                var razor = executeAppDomain.CreateInstanceAndUnwrap(
                    Assembly.GetEntryAssembly().FullName,
                    typeof(RazorTemplateTest).FullName
                ) as RazorTemplateTest;

                razor.ExecuteParse(i);
                
                AppDomain.Unload(executeAppDomain);

                sw.Stop();

                Console.WriteLine(" {0} : {1} ms",
                    AppDomain.CurrentDomain.FriendlyName,
                    sw.ElapsedMilliseconds);
            }
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            //new RazorTemplateTest().ExecuteParse();
            //new RazorTemplateTest().ExecuteCompile();
            new RazorTemplateTest().ExecuteAnotherAppDomain();

            Console.ReadLine();
        }
    }
}

こんなコードで~す。

2010年12月23日木曜日

サウザンダー

so

やったよ!長かった。とても長かった。458日。ちょっとづつ。

so2

ジリジリと。やっとサウザンダーの仲間入り。MVCだけでもなんとかなるもんです!

2010年12月21日火曜日

Norwegian Wood

NoSQLの成功は1:10問題にかかっている:Kenn's Clairvoyance - CNET Japan

面白い記事ですね。

NoSQLをRDBの代わりに使うと、どういう恐ろしいことが起こるか。PARTAKEの作者が語る - Publickey

NoSQLでは↑こんな話もありますが、ちゃんとDomain Specific Database(という言葉があってるのかどうかは知らないけど)であることを認識して、使い方を間違えなければいいじゃんよ、ってなもんです。恐ろしいのは何でもRDBMS、いやいや何でもNoSQLという発想やそのメンタリティ。

言いたいことはそこじゃなくてですね、最初のリンクに書かれている

「コンピュータサイエンスに難問は二つしかない。それは概念に名前をつけることと、キャッシュの失効である」

“There are only two hard things in Computer Science: cache invalidation and naming things”

の部分です。「概念に名前をつける」これはコンピュータサイエンスに限らず重要なことじゃないでしょうか。

先日はじめて知った言葉に「絶対領域」というものがありました。ATフィールドの和訳というそれっぽいこともWikipediaには書かれていましたが、そんなことにココロオドルかっちゅう話ですよ。詳細はグーグルさんに教えてもらってくださいね。この胸の高鳴りはなんなのか?それが定義され名前がある、その事実に衝撃を覚えたものです。

そして「森ガール」という謎の単語。これに関しては相変わらず意味がよくわかんないんだけど、「森にいそうな格好」とはようするに森に住む妖精みたいなもんだろーか?

森ガールといえば最近そんなよーな名前の映画が公開されてた気がする。たぶんこんな話なんだろうな。

目が覚めるとそこは不思議の森。なんでこんな場所に寝てたんだろう?確かこたつでみかんを食べながらテレビを見ていはず。う~ん、記憶が一部飛んでいる。とりあえず、自分がいまどこにいるのか把握しておいたほうがいいな。そう思っておもむろに立ち上がり散策を開始。ところで今何時だ?確かテレビでサングラスのおじさんがウキウキウォッチングがどーのこーの言ってたからお昼過ぎのはずなんだけど。

途中で出会った森の中に住む妖精「森ガール」との珍道中。あれはオーロラか!?まさかここは日本じゃない?外国?そして最初の被害者が...。スリルとサスペンスのドタバタ大活劇。じっちゃんの名にかけて!

ノルウェイの森

森ガールの謎を解くべく観に行ったんだけど全然こんな話じゃなくて愕然とした。

2010年12月18日土曜日

Razor Helper的なForEachでテンプレート

今年も終わりですね。タイトル意味分かんないですね。

来週から夏休み&年末年始休暇に入るので、今年の仕事終了です。てへ。ブログサイトがあるのをすっかり忘れるくらい追い込まれる毎日。よくもまぁ、こんな状態が続くもんだと感心するばかりです。え?きれてねーよ。

そんなことはいいんですけどね、ちょっとここ見てみてくださいよ。

Razor, Nested Layouts and Redefined Sections - Marcin On ASP.NET - Site Home - MSDN Blogs

ナイスエントリーにも程があるデスヨ。

Layout用のビューを直接指している、子供ビューのセクションはみえるけど、多段Layoutになってて孫ビューのセクションはスコープの範囲外でエラーになっちゃうっていうエントリー。

実際に試してみたらたしかに動かない。Section not defined。そ

foreach

りゃそうだ。それを克服するために中間LayoutでRedefineSectionで孫のセクションを生成(WebPageBase.DefineSection)してしまおうよっていう強引さには度肝を抜かれますがなによりそこじゃないんです。

その部分より何よりセクション初期値をヘルパーに渡す部分ですよ!

Func<object, HelperResult> defaultContent

これを知っておくと何が出来るかというと...

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@helper ForEach(IEnumerable<int> values, Func<object, HelperResult> template){
    var partialHtml = new System.Text.StringBuilder();
    foreach(var value in values){
        partialHtml.Append( template(value));
    }

    Write(MvcHtmlString.Create(partialHtml.ToString()));
}
<div>
<ul>
@ForEach(Enumerable.Range(1,5),
    @<li>@item</li>
)
</ul>
</div>

↑こんなコード書けるってことですよ。ヘルパーにRazor構文のテンプレートを渡してるんですよ。いいでしょ?ん~、もんどりうつ。

追記:もっと簡単でよかった。

@helper ForEach(IEnumerable<int> items, Func<object, HelperResult> template){
    foreach(var item in items){
        Write(template(item));
    }
}

@helperだとGenericメソッドに出来なさそうなので、もうちょっと調べてみたら、普通に@functionsで実装するサンプルをAndrew君が...。

VibrantCode - Inside Razor - Part 3 – Templates

この場合は↓こうね。

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@functions{
    static IHtmlString ForEachGeneric<T>(IEnumerable<T> items, Func<T, HelperResult> template)
    {
        var sb = new System.Text.StringBuilder();
        foreach(var item in items){
            sb.Append(template(item));
        }
        
        return MvcHtmlString.Create(sb.ToString());
    }
}

<div>
<ul>
@(ForEachGeneric<int>(Enumerable.Range(1,5),
    @<li>@item</li>
))
</ul>
</div>

 

foreach2

小っちゃ!
クリックして拡大してみてね。

Razorのコンパイル結果を見てみると分かるけど、ヘルパーに渡すテンプレートブロックにはobject型のitemが必ずわたりまする。