2015年12月5日

Firefox のボタン(<button>, <input type=button>)の大きさ(幅・高さ)が違う場合の対処法

width や height を指定せず、padding や内部の文字列で大きさを確保するボタン(<button> や <input type="button">)では、IE/Chrome と比べて Firefox のボタンサイズが大きくなる。
文字の幅は、同じフォントを指定してもブラウザによって異なるのが普通だが(レンダリングエンジンが異なるため)、Firefox のみ明らかに大きくなる。

[問題になる CSS の例]

button {
    padding: 5px;
    border: 1px solid lime;
    letter-spacing: 0px;

    /* ブラウザ間の差が少ないメイリオを使用 */
    font: 15px/22px Meiryo;
    cursor: pointer;
}
[プレビュー]
[開発者ツールの画面キャプチャ]
IE Chrome Firefox (Firebug)
IE button inspection Chrome button inspection Firefox button inspection
※ Firefox のボタンサイズが、縦方向 2px、横方向 6px 大きい。

原因と対処法

困った時の stackoverflow。
Firefox はボタンフォーカス時の点線の幅と padding が確保されているため、他のブラウザより大きくなるのだとか。
これ → Firefox focused button

[ボタンフォーカスの点線を消す CSS]

button::-moz-focus-inner {
    padding: 0;
    border: 0;
}
/* input[type=button] なども同様に指定可能 */
[プレビュー]
[Firebug の画面キャプチャ]
Firefox button inspection

ボタンフォーカスの点線を消したくない場合、margin を工夫することで大きさを合わせることが可能。

button::-moz-focus-inner {
    padding: 0;
    margin: -1px; /* 点線の幅分のマイナスマージン */
}

ただし、最初にも書いたが、ブラウザによる文字の幅の差はどうすることもできないので、ボタンの大きさ完全に一致させたい場合は、button の width を指定するしかない。

検証環境

  • Windows 7 64bit
  • Internet Explorer 11
  • Firefox 42.0
  • Chrome 48.0.2564.8

2015年11月19日

<iframe> を動的生成する際の注意点

javascript で src 無しの <iframe> とその中身を構築した際、はまったことがあったのでメモ。

  • 動的に追加した <iframe> の中身をいじる際は、
    iframe.contentWindow.documentdocument.write() を使う
    または
    <iframe> のロード完了を待つ。(<iframe> 自身に onload イベントがある)
  • <iframe> に onload イベントを設置する際は、DOM への追加前に行う。
    DOM に <iframe> を追加した瞬間、onload イベントが処理されるブラウザがあるため (Chrome)

[サンプルコード]

// 単純化のため、jquery 使用

// 良い例
// IE:OK / Firefox:OK / Chrome:OK
$(function(){
    $('<iframe>').load(function(){
        // ロード完了後、DOM を変更できる。
        var ibody = this.contentWindow.document.body;
        $(ibody).append('<div>ブロック要素</div>');
    }).appendTo('body');
    // イベント設置後に DOM へ追加
});


// 悪い例(1)
// IE:OK / Firefox:NG / Chrome:OK
// Chrome は iframe の DOM 追加後、即時ロード完了する模様
// IE は iframe のロード完了を待たなくても大丈夫っぽい??
$(function(){
    var iframe = $('<iframe>').appendTo('body');
    var ibody = iframe[0].contentWindow.document.body;
    $(ibody).append('<div>ブロック要素</div>');
});


// 悪い例(2)
// IE:OK / Firefox:OK / Chrome:NG
// Chrome は iframe が即時ロード完了するので、DOM 追加後に load イベントを付けても無意味
$(function(){
    var iframe = $('<iframe>').appendTo('body');
    iframe.load(function(){
        var ibody = this.contentWindow.document.body;
        $(ibody).append('<div>ブロック要素</div>');
    });
});
[環境]
  • Windows 7 64bit
  • Internet Explorer 11
  • Firefox 42.0
  • Chrome 48.0.2564.8

2015年10月31日

highlight.js で行番号を表示

ブログ内の SyntaxHighlighterhighlight.js 移行作業がようやく終わったので投稿。

highlight.js には行番号を表示する機能が付いてない。
とりあえず検索してみたところ、ちょうどいいプラグインがあったのでそれの紹介。(当ブログでも使用中)
wcoder/highlightjs-line-numbers.js

使用方法は、highlight.js を適用した要素(普通は <code>)に対して hljs.lineNumbersBlock(要素) を実行。
もしくは、hljs.initLineNumbersOnLoad() を仕掛けておく。
[使用例]

[].forEach.call(document.querySelectorAll('pre > code'), function(elem){
    // highlight.js は <code> 内の先頭と末尾の改行を無視してくれないので、ここで削除
    // ※ HTML を書く際、先頭と末尾に改行を入れない方法もある
    elem.textContent = elem.textContent.replace(/^[\r\n]+|[\r\n]+$/g, '');

    // highlight.js の適用
    hljs.highlightBlock(elem);

    // highlightjs-line-numbers.js の適用
    hljs.lineNumbersBlock(elem);
});

また、行番号ブロックのスタイル(.hljs-line-numbers)を自分で用意する必要がある。
[使用例(当ブログのスタイル・2015/10/31)]

pre > code.hljs.hljs-line-numbers {
    background-color: #f5f5f5;
    border-right: 0 none;
    color: #707070;
    text-align: right;
    min-width: 14px;
    /* 以降は選択できないようにするためのスタイル */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

highlight.js に移行した理由

  • C# の新しいキーワードに対応 (素の SyntaxHighlighter は var 辺りから未対応)
  • 旧 Visual Studio 風ハイライトが選択可能
  • ハイライトする言語を選択してパッキングと Minify が公式サイトで可能
  • 外部ファイルの数が少ない (プラグインを除けば js と css の 2 ファイル)

2015年9月25日

python の加算代入演算子(+=)(プラスイコール)の注意点

python の一部のオブジェクト(list 等)では、「a = a + b」と「a += b」が同一視できない。
他言語経験者が思わぬバグを埋め込んでしまいそうな罠仕様・・・


>>> A = a = [1]
>>> a = a + [2]
>>> a, A, a is A
([1, 2], [1], False)
>>> # list の加算(+)は、元の a を変更しない
... # 加算の結果(新しい領域)を a に代入しているので、a と A は別オブジェクト
...

>>> B = b = [1]
>>> b += [2]
>>> b, B, b is B
([1, 2], [1, 2], True)
>>> # list の加算代入(+=)は、元の b を変更する
... # 加算代入しても新しい領域は割り当てられないので、b と B は同一オブジェクト
...

この理由は、list には 加算代入演算子(+=)用の __iadd__ メソッドが定義してあり、加算演算子(+)用の __add__ メソッド とは異なる動作になっているため。
※ list.__iadd__ の動作は list.extend と同じ。

__iadd__ メソッドが定義されてないオブジェクト(tuple 等)は、加算代入でも __add__ メソッドが使用される。
つまり、「a = a + b」と「a += b」 が同一視できる。

組み込みオブジェクトの大半には __iadd__ メソッドが定義されていないため、基本的に list とそのサブクラスを扱う際に注意すればいい。
加算代入演算子(+=)だけでなく、乗算代入演算子(*=)なども同様に注意。
ちなみに、__i???__ メソッドは、ミュータブルなオブジェクトに実装されるものらしい。

参考URL

2015年9月24日

python の代入は値を返さないけど多重代入は可能

python の代入は値を返さないので、下記のような書き方はエラーになる。

>>> # ファイルから少しずつ読み込んで処理
...
>>> f = open('hoge.txt', 'rb')
>>> while chunk = f.read(4096):
  File "", line 1
    while chunk = f.read(4096):
                ^
SyntaxError: invalid syntax

だけど、多重代入 (マルチターゲット代入 / chained assignment) はできる。

>>> a = b = 'test'
>>> a, b
('test', 'test')

何となく納得いかない感があるけど、多重代入だけは特別らしい。

下記のような書き方でも多重代入は可能。(自分がかつてやっていた方法)
左辺変更時に修正点多いし、あまりリーダブルじゃないと思うので使わない方がいい・・・

>>> a, b = ('test',) * 2
>>> a, b
('test', 'test')

おまけ

最初の例を綺麗に書きたい場合、iterfunctools.partial を組み合わせて使う。

>>> # ファイルから少しずつ読み込んで処理
...
>>> f = open('hoge.txt', 'rb')
>>> for chunk in iter(functools.partial(f.read, 4096), b''):
...    print(chunk)
...

>>> # partial の代わりにラムダ式でも可能
...
>>> for chunk in iter(lambda: f.read(4096), b''):
...    print(chunk)
...

2015年9月7日

python の等価演算子(==)と bool / 数値型

python の == 演算子は割と厳密で、公式ドキュメントにもあるとおり、数値型を除いて異なる型同士は等価にはならない。
javascript みたいに 1 == "1" が成立したりしない。
そのためか、python には === 演算子がない。

ただし、注意する点が 1 つあって、bool 型が int 型のサブクラスで、「True = 1」「False = 0」であること。
つまり、bool 型も数値型に含まれることになる。
そのため、下記のような結果になる。

>>> isinstance(False, int)
True
>>> isinstance(True, int)
True
>>> False == 0
True
>>> True == 1 == float(1) == complex(1)
True
>>> [True, False] == [1, 0]
True
>>> (True, False) == (1, 0)
True

上記を考慮して実装すれば基本的に問題ないのだが、どうしても厳密比較したい場合、対 bool 型であれば is 演算子を使う。

>>> value = True
>>> value is True
True
>>> value = 1
>>> value is True
False
対数値型であれば値の比較に加えて型の比較が必要になる。

>>> def isnum(value):
...   return type(value) in (int, float, complex)
...
>>> value = 1
>>> isnum(value) and value == 1
True
>>> value = True
>>> isnum(value) and value == 1
False

余談だが、python2 系では、True と False に値を代入して書き換えることができる。
割と危ない仕様・・・
python3 からはちゃんとエラーになってくれる。

2015年8月22日

document.querySelector で先頭が数字の ID を指定する方法

document.querySelectordocument.querySelectorAll で、数字から始まる ID をそのまま指定するとエラーになる。
document.getElementById はエラーにならず取得できる。
※ DOM じゃないけど、jquery セレクタもエラーにならず取得可能。

console.log(document.querySelector('#20150821abc'));
// Error: An invalid or illegal string was specified

console.log(document.getElementById('20150821abc'));
// <div id="20150821abc">

// 上記の div 要素は、このページ内に含まれているので、firebug・開発者ツールなどで確認可能

解決手段

querySelector は CSS セレクタを指定するものなので、CSS セレクタのエスケープ方法(コードポイント指定)で、先頭数字の ID を指定することができる。

// エスケープ方法は 2 通り
console.log(document.querySelector('#\\32 0150821abc'));    // '\3' + (digit) + ' '
console.log(document.querySelector('#\\0000320150821abc')); // '\00003' + (digit)

// <div id="20150821abc">

// もちろん、CSS の記述でも使用可能

自分用の GM スクリプトを作った際、先頭数字の ID を使っている web サイトがあったので・・・
普通は、先頭数字にはしないと思うのでレアケース?

参考URL

2015年7月3日

非同期 ASP.NET MVC と HttpContext.Current

非同期な ASP.NET MVC で HttpContext.Current を使うのはやめた方がいい、という話。

[非同期メソッドのサンプル]

public class MyController : Controller
{
    // RouteConfig で URL が割り当てられた非同期メソッド
    public async Task<ActionResult> Test()
    {
        // (A)

        await Task.Run(() => {
            // (B)
        });

        // (C)

        return View();
    }
}

上記で HttpContext.Current が使用できるのは (A) (B) (C) のどこか?
と疑問になったので調査してみた。

[結果]
(A) 使用可能
(B) 使用不可
(C) web.config の設定で変わる
<appSettings><add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /> を設定、または、<system.web><httpRuntime targetFramework="4.5"> (4.5 以上なら OK) を設定すると使用可能
上記を設定してない場合は使用不可
※ Windows 7/IIS 7.5/ASP.NET MVC 5/.NET 4.5.2
※ Visual Studio 付属の IIS Express は異なる結果になる場合あり

ちなみに、真ん中の Task を ConfigureAwait(false) とすると、設定に関わらず (C) でも HttpContext.Current 使用不可。

まとめ

調査結果のとおりなのだが、非同期だと HttpContext.Current は設定やコードに依存して使えたり使えなかったりするので、使わない方がまし・・・使用禁止でもいいと思う。

そもそも ASP.NET MVC の Controller クラスには HttpContext プロパティがあるので、これを使うのが普通。
Controller から呼び出すロジッククラスでは、HttpContext を引数渡しで使うのが無難。コードが増えるけど・・・
当たり前だけど HttpContext クラスにできるだけ依存しないクラス設計が大事。

参考URL

2015年6月30日

XmlSerializer と CDATA セクション

XmlSerializerCDATA セクションを扱うのが面倒だったので、自分なりのまとめ。(ぐぐれば引っかかる内容。)
System.Xml.Serialization 名前空間には、なぜか CDATA セクションのためのカスタム属性が用意されてない・・・

CDATA セクションのみを含む要素

[XML]

<root>
  <text><![CDATA[<b>cdata</b>]]></text>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml1
{
    [XmlElement("text")]
    public XmlCDataSection Text { get; set; }
}

XmlCDataSection のプロパティを用意するだけ。
XmlCDataSection は new XmlDocument().CreateCDataSection("CDATA セクションの中身") で作成できる。
面倒な場合はプロパティで工夫。
※シリアライズ時、CreateCDataSection の引数が null でも <![CDATA[]]> は出力される。

XmlCDataSection の代わりに、IXmlSerializable を継承した CDATA セクション用クラスを自作するのもあり。
[CDATA セクション用クラス]

public class CDataSection : IXmlSerializable
{
    public CDataSection() { }
    public CDataSection(string text) { Text = text; }
    public string Text { get; set; }

    public virtual XmlSchema GetSchema()
    {
        return null;
    }
    public virtual void ReadXml(XmlReader reader)
    {
        Text = reader.ReadElementString();
    }
    public virtual void WriteXml(XmlWriter writer)
    {
        writer.WriteCData(Text);
    }
}

CDATA セクションと属性がある要素

[XML]

<root>
  <node attr="value"><![CDATA[<b>cdata</b>]]></node>
</root>
[対応クラス]

[XmlRoot("root")]
public class CDataXml3
{
    [XmlElement("node")]
    public CDataNode Node { get; set; }
}

public class CDataNode
{
    [XmlAttribute("attr")]
    public string Attr { get; set; }

    // データの設定・取得用
    [XmlIgnore]
    public string Content { get; set; }

    // シリアライズ・デシリアライズ用
    [XmlText]
    public XmlNode[] ContentNode
    {
        get { return new[] { new XmlDocument().CreateCDataSection(Content) }; }
        set
        {
            if( value == null ){
                Content = null;
                return;
            }
            if( value.Length != 1 ) throw new InvalidOperationException();
            Content = value[0].Value;
        }
    }
}

CDATA セクションにあたるプロパティを、XmlCDataSection を 1 つだけ含む XmlNode[] 型にして、XmlTextAttribute を付加。
そうすると、直接 CDATA セクションを出力できる。
XmlNode[] 型だとデータの設定・取得が面倒なので、別のプロパティ(上記だと Content)を用意する。

ただし、この方法はデシリアライズ時にちょっとした問題があり、CDATA セクションの代わりに普通のテキストノードでもデシリアライズできてしまう。
デシリアライズされたインスタンス(上記だと value[0])は、元が CDATA セクションかテキストノードかに関わらず XmlText になるため、型チェックではじくことができない。
OuterXmlInnerText などのプロパティにも <![CDATA[]]> の文字列が含まれないため、デシリアライズインスタンスからはおそらく判断不可能。

CDATA セクションかテキストノードかどちらでもいい場合は問題ないが、厳密にチェックしたい場合、この方法では難しいかもしれない。

参考URL

2015年6月20日

クラスと構造体のインターフェースメソッド呼び出し

IComparer<T>IEqualityComparer<T> などのインターフェースを実装する際、クラスと構造体のどちらがいいのか迷ったことがあったので、インターフェースにキャストしたクラス・構造体からの、メソッド呼び出し速度を測定してみた。

[測定コード]

static class MeasureMethodCall
{
    static void Main(string[] args)
    {
        var size = 10000000;
        var cmpc = new ComparerClass();  // クラスベース
        var cmps = new ComparerStruct(); // 構造体ベース

        for( var i=0; i<11; i++ ){
            Console.WriteLine("[{0}]", i);

            // インターフェースにキャストした状態からのメソッド呼び出し
            InterfaceCall(size, cmpc);
            InterfaceCall(size, cmps);

            // おまけ:直接のメソッド呼び出し
            DirectCall(size, cmpc);
            DirectCall(size, cmps);
        }
    }

    static void InterfaceCall(int size, IComparer<int> comparer)
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[InterfaceCall][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }

    static void DirectCall<T>(int size, T comparer) where T : IComparer<int>
    {
        var sw = Stopwatch.StartNew();
        for( var i=0; i<size; i++ ) comparer.Compare(0, i);
        sw.Stop();
        Console.WriteLine("[DirectCall   ][{0}][{1:struct;0;class }] {2}",
            size, comparer.GetType().IsValueType.GetHashCode(), sw.Elapsed);
    }
}

class ComparerClass : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}

struct ComparerStruct : IComparer<int>
{
    public int Compare(int x, int y){ return x.CompareTo(y); }
}
[結果]
インターフェース(クラス) 0.0758722
インターフェース(構造体) 0.1012821
クラス 0.0674410
構造体 0.0083828
※コマンドプロンプトで実施(csc /optimize)
※2 回目以降は類似の結果だったため、2 回目の結果を採択
※単位は[s]

まとめると、
構造体 ≫ クラス > インターフェース(クラス) > インターフェース(構造体)
※左の方が速い

当たり前だが、インターフェースにキャストした状態からのメソッド呼び出しは、直接呼び出しよりも遅い。
そして、インターフェースにキャストした状態では、クラスより構造体の方が遅い。
理由はたぶん、インターフェース(構造体)のメソッド呼び出しは、自身(構造体インスタンス)のボックス化解除が必要だから?

インターフェースを引数に取るメソッドが、上記のように where 制約で実装されていれば構造体がベストだが、.NET Framework の標準メソッドの多くはインタフェース引数に where 制約を使っていない。
where 制約を使うと、型引数が増えて呼び出し側での指定が必要になり、コードが汚くなるからだと思われる。(参考:メソッドの型推論で型パラメータの制約は使われない

結論としては、一般的な使い方をするなら、インターフェースはクラスで実装した方がいい。
※プロパティも実質メソッドなので、プロパティを実装するインターフェースについても同様

実際に Array.SortEnumerable.OrderBy で測定したところ、インターフェース(クラス)の方がいい結果になった。

検証環境

Windows 7 64bit/.NET 4.5.2
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

2015年6月5日

LINQ OrderByDescending != OrderBy + Reverse

LINQ でシーケンスを降順にソートする場合、素直に OrderByDescending を使う方法と、OrderByReverse を組み合わせる方法がある。
(降順ソート用 IComparer<T> を使う方法もあるが、OrderByDescending と同じなので割愛)

この 2 つの方法は、異なる結果になることがある。
理由は、OrderByDescending と OrderBy が両方とも 安定ソートであるため。
「降順の安定ソートってどっち?」と混乱するかもしれないが、OrderByDescending の実装は MSDN の記載通り同じキーを持つ要素の順序は保持される。
OrderBy も同様なので、同じキーを持つ要素については、OrderByDescending と OrderBy は同じ順序になってしまう。

つまり、両者は正反対の順序にならないため、OrderBy + Reverse は必ずしも OrderByDescending の結果と一致しない。
※並び替えキーに重複がない場合は一致する。
※要素=並び替えキーとなる場合は、順序が違っても分からないため、考慮不要

[不一致ケース]

// インデックス付き配列を並べ替え
var data = new[]{ 'a', 'b', 'a', 'c', 'a' }.Select((V, I) => new{ V, I });

Console.WriteLine("[OrderBy]");
Console.WriteLine(string.Join(" ", data.OrderBy(v => v.V)));

Console.WriteLine("[OrderBy + Reverse]");
Console.WriteLine(string.Join(" ", data.OrderBy(v => v.V).Reverse()));

Console.WriteLine("[OrderByDescending]");
Console.WriteLine(string.Join(" ", data.OrderByDescending(v => v.V)));
[結果]
[OrderBy]
{ V = a, I = 0 } { V = a, I = 2 } { V = a, I = 4 } { V = b, I = 1 } { V = c, I = 3 }
[OrderBy + Reverse]
{ V = c, I = 3 } { V = b, I = 1 } { V = a, I = 4 } { V = a, I = 2 } { V = a, I = 0 }
[OrderByDescending]
{ V = c, I = 3 } { V = b, I = 1 } { V = a, I = 0 } { V = a, I = 2 } { V = a, I = 4 }

2015年5月23日

ASP.NET 出力レスポンスをキャプチャする方法

ブラウザに出力される HTML や API の出力などのレスポンスを、サーバー側で取得したい場合がたまにある。
ASP.NET (not MVC) でこれを行う方法はいくつかある。

Response.Filter を使う方法

Response.Filter にフィルタクラス (Stream 継承) を設定すると、レスポンスを出力前に加工することができる。
加工目的でなくても、Stream.Write に出力内容が入ってくるので、その内容を MemoryStream 等に入れればレスポンスをキャプチャできる。

[キャプチャ用フィルタクラス実装例]

public class CaptureStream : Stream
{
    public CaptureStream(Stream targetStream)
    {
        _targetStream = targetStream;
        Captured = new MemoryStream();
    }
    private readonly Stream _targetStream;
    public MemoryStream Captured { get; private set; }

    public override void Write(byte[] buffer, int offset, int count)
    {
        // 対象のストリームとメモリに書込み
        _targetStream.Write(buffer, offset, count);
        Captured.Write(buffer, offset, count);
    }

    public override void Flush()
    {
        _targetStream.Flush();

        // ここで Captured に対する処理を行う (ログ出力など)
        // OnFlush イベントを用意するとベター
    }

    public override void Close()
    {
        _targetStream.Close();
        Captured.Close();
        base.Close();

        // Captured に対する処理は、ここでも可能
        // MemoryStream は Close してもバッファは残るので ToArray などが可能
    }

    // 残りのオーバーライドは適宜実装
}

//---- 使用例 ----//
// Page クラス内の OnLoad などで
Response.Filter = new CaptureStream(Response.Filter);

デメリットは、キャプチャしたデータにアクセスできる Page イベント(オーバーライド可能メソッドも含む)が無いこと。
つまり、Page ライフサイクル内にキャプチャしたデータを処理するタイミングはなく、上記コードのようにフィルタクラスの Flush ないし Close 時に処理を挟むことしかできない。
そして、Page ライフサイクルから外れるため、セッションに保存できない。

余談だが、Response.Filter は ASP.NET MVC にも存在しているので、同じことができる。

Page.Render を使う方法

Page.Render をオーバーライドすると、出力されるレスポンスデータにアクセスできる。
→ そのままキャプチャ可能。

[Page.Render のキャプチャ実装例]

protected override void Render(HtmlTextWriter writer)
{
    string captured = null;

    using( var sw = new StringWriter() )
    using( var htw = new HtmlTextWriter(sw) ){
        // メモリ内にレスポンス出力
        base.Render(htw);
        captured = sw.ToString();

        // 文字列化したレスポンスをブラウザに出力
        writer.Write(captured);
    }

    // captured に対する処理を行う
}

この方法は、Page ライフサイクル内の処理なので、セッションに保存できる。

デメリットは、オーバーライドなので、フィルタクラスのように機能分離できないこと。

参考URL

2015年5月16日

Assert.AreEqual に NaN が入るとテストが成功するバグ (VS2010~2012)

Visual Studio 2010 (たぶん 2012 も) の MSTest の delta を指定する Assert.AreEqual には下記のようなバグがある。

// delta を指定する場合
Assert.AreEqual(1.0, double.NaN, 1.0); // 成功

// delta を指定しない場合
Assert.AreEqual(1.0, double.NaN); // 失敗

// 失敗するのが正しい動作なので、delta を指定する方はバグっている

バグの原因はソースを見れば明らかで、NaN を考慮していないため。
[Assert.AreEqual のソース (ILSpy)]

public static void AreEqual(double expected, double actual, double delta, string message, params object[] parameters)
{
    if (Math.Abs(expected - actual) > delta)
    {
        string message2 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
        Assert.HandleFail("Assert.AreEqual", message2, parameters);
    }
}

// 比較条件に NaN が含まれると false になるので if の中に入らない

Assert.AreEqual が含まれるアセンブリの配置場所は
%VS のディレクトリ%\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

Visual Studio 2013 では修正されているが・・・

このバグは VS2013 では修正されている。
[修正済み Assert.AreEqual のソース (ILSpy)]

public static void AreEqual(double expected, double actual, double delta, string message, params object[] parameters)
{
    if (double.IsNaN(expected) || double.IsNaN(actual) || double.IsNaN(delta))
    {
        string message2 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
         Assert.HandleFail("Assert.AreEqual", message2, parameters);
    }
    if (Math.Abs(expected - actual) > delta)
    {
        string message3 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
        Assert.HandleFail("Assert.AreEqual", message3, parameters);
    }
}

// 引数のいずれかが NaN の場合、テスト失敗

が、VS2010 をインストールしている場合、修正されたアセンブリ「Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll」(以降、UnitTestFramework.dll)が使われないケースがある。
[確認できた発生条件]
  • VS2010 と VS2013 インストール済 (インストール順序は関係ない)
  • .NET 4.0 以上のテストプロジェクト

原因

VS2010 は GAC に .NET 2.0 と 4.0 の UnitTestFramework.dll (バグ有り) をインストールするが、VS2013 は .NET 2.0 の UnitTestFramework.dll (修正済み) しかインストールしないため。
かつ、VS2010 の UnitTestFramework.dll (.NET 4.0) と VS2013 の UnitTestFramework.dll (.NET 2.0) のバージョン番号「10.0.0.0」が同じであるため。(公開キーは MS 製品なので同じ)
※他にバージョン番号「10.1.0.0」の UnitTestFramework.dll も存在するが、設定で参照されないようになっている

VS2010 と VS2013 をインストールした環境の GAC は下記の状態になる。
CLR 2.0 %windir%\assembly 修正済み
CLR 4 %windir%\Microsoft.NET\assembly バグ有り

そして、CLR 4 の実行アプリは CLR 4 の GAC を優先するため、バグ有りの方の UnitTestFramework.dll が参照されることになる。(参考:GAC 内のアセンブリ検索方法
公開キーが一緒なのは仕方ないとして、バージョン番号さえ変えとけば、こんな事態は回避できた・・・

解決手段

上記リンクでも説明されてるけど、CLR 4 の GAC の実ファイルを移動または削除すればOK。
%windir%\Microsoft.NET\assembly\GAC_MSIL\Microsoft.VisualStudio.QualityTools.UnitTestFramework\v4.0_10.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

副次的な効果として、VS2010 のテストプロジェクトからも修正された UnitTestFramework.dll が使用されるようになる。

2015年5月10日

GAC 内のアセンブリ検索方法

.NET Framework アプリケーションの実行時、参照されている署名付きアセンブリ (DLL) は、まずグローバル アセンブリ キャッシュ (GAC) から検索される。
GAC 内に見つからなかった場合、アプリケーションフォルダや設定ファイルで指定した場所が検索される。

で、GAC 内のアセンブリ検索方法は、少し複雑である。

GAC の実フォルダ(アセンブリの配置場所)は、アセンブリの CLR バージョンによって 2 個所に分かれている。
※ 2015/05/01 現在
CLR 1.0 ~ 2.0
(.NET 3.5 以前)
%windir%\assembly
CLR 4
(.NET 4.0 以降)
%windir%\Microsoft.NET\assembly

アセンブリ検索方法も、実行アプリケーション (exe) の CLR バージョンによって異なる。
(A) CLR 1.0 ~ 2.0 ① を検索
(B) CLR 4 ② → ① の順で検索
※実行ファイルのビルド時に .NET 3.5 以前のアセンブリを参照しても ② から検索
→ アセンブリのランタイムバージョンは考慮されない

(B) ※ の補足。
同じ名前・バージョン番号・公開キーのアセンブリ (TEST.DLL) を .NET 3.5 と 4.5 でビルドして、それぞれGAC にインストールする。
→ ① と ② の両方にアセンブリが置かれる。
TEST.DLL (.NET 3.5) を参照する実行アプリを .NET 4.5 でビルドして実行する。
→ ② が先に検索されるため、実際に参照されるアセンブリは TEST.DLL (.NET 4.5)

このような事態を回避するため、GAC にインストールするアセンブリに複数の CLR バージョンがある場合、それぞれ異なるバージョン番号にしておくのが定石。

ちなみに実際に参照されるアセンブリのパスは、下記で確認できる。

// string クラスがあるアセンブリの配置場所
Console.WriteLine(Assembly.GetAssembly(typeof(string)).Location);

// C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
// ※ mscorlib.dll は GAC から読み込まれない例外

mscorlib.dll が GAC から読み込まれない理由は下記

2015年4月14日

Entity Framework でアトミックインクリメント & 一括更新

Entity Framework (EF) 標準の更新処理では、アトミックインクリメント(デクリメントも)や一括更新(BULK UPDATE/DELETE)ができない。
一括処理については、条件に当てはまるものを select 後に update ないし delete は可能だが、件数が多い場合はかなり効率の悪い処理になる。(select → update/delete × 件数)
そこだけ文字列 SQL 使えば解決・・・なんだけど、せっかく EF 使ってるので文字列 SQL は避けたい、という人は多いと思う。

そういう要望に応えるライブラリがいくつかある。(非 MS 製) ※すべて NuGet からインストール可能

ここでは、一番よく使われているであろう EntityFramework.Extended を触ってみる。
※ EntityFramework.Utilities は EF 6.0 以降がメインだし、ZZZ は有料なので

[環境]
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0
  • EntityFramework.Extended 5.0.0.73

EntityFramework.Extended サンプルコード

[コード]

// テーブル定義
public partial class CountTable
{
    public string Name { get; set; }
    public int Value { get; set; }
    public System.DateTime Updated { get; set; }
}

static class EExfTest
{
    // アトミックインクリメント
    public static void UpdateIncrement()
    {
        using( var context = new TestEntities() ){
            context.CountTables.Update(
                item => item.Name == "aaa",
                item => new CountTable {
                    Value = item.Value + 1,
                    Updated = EntityFunctions.AddDays(item.Updated, 1).Value
                }
            );
        }
        // Update は IQueryable に対する拡張メソッド
        // 第 1 引数は where 条件、第 2 引数は set の中身 (オブジェクト初期化子で指定する)
        // 両引数は式木として解釈される
        // where 条件を指定しないオーバーロードもあり
        //
        // 数値のインクリメントだけでなく、EntityFunctions を使って日付のインクリメントも可能
    }

    // 一括変更
    public static void BulkUpdate()
    {
        using( var context = new TestEntities() ){
            context.CountTables.Update(
                item => new CountTable {
                    Value = 0
                }
            );
        }
    }

    // 一括削除
    public static void BulkDelete()
    {
        using( var context = new TestEntities() ){
            context.CountTables
                .Where(item => item.Value < 1)
                .Delete();
        }
        // Delete は IQueryable に対する拡張メソッド
        // 第 1 引数(where 条件)を指定するオーバーロードもあり
    }
}
[実行される SQL]

-- UpdateIncrement
UPDATE [dbo].[CountTable] SET 
[Value] = [Value] + 1 , 
[Updated] = DATEADD (day, 1, [Updated])  
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
1 AS [C1], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
WHERE 'aaa' = [Extent1].[Name]
) AS j1 ON (j0.[Name] = j1.[Name])

-- BulkUpdate
-- ※実際は sp_executesql で実行
UPDATE [dbo].[CountTable] SET 
[Value] = 0
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
1 AS [C1], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
) AS j1 ON (j0.[Name] = j1.[Name]

-- BulkDelete
DELETE [dbo].[CountTable]
FROM [dbo].[CountTable] AS j0 INNER JOIN (
SELECT 
[Extent1].[Value] AS [Value], 
[Extent1].[Name] AS [Name]
FROM [dbo].[CountTable] AS [Extent1]
WHERE [Extent1].[Value] < 1
) AS j1 ON (j0.[Name] = j1.[Name])

EntityFramework.Extended のトランザクション

EF.Extended の Update と Delete は、ExecuteSqlCommand と同じく即時 SQL が実行される。
つまり、DbContext.SaveChanges と無関係のトランザクションになる。

EF 標準の更新処理と EF.Extended による更新処理を混ぜる場合は、自分でトランザクションを管理する必要がある。

[トランザクション使用例]

using( var context = new TestEntities() )
using( var tx = context.BeginTransaction() ){
    // 更新処理

    context.SaveChanges();
    tx.Commit();
}

// BeginTransaction も EF.Extended にある拡張メソッド
// EF 6.0 からは context.Database.BeginTransaction() が可能になったので、Obsolete に

EntityFramework.Extended の対応 DB

現在(2015/04/13)の EF.Extended のソースを見る限り、SQL Server にしか対応してない。
※ /Source/EntityFramework.Extended/Batch 配下に SqlServerBatchRunner.cs しかないため

ただし、EF の接続オブジェクト ObjectContext.Connection を利用するため、他の DB も一応は使用可能。
(その DB が扱えない SQL が構築されると、DB 側でエラーになる。)

2015年4月4日

Entity Framework のパフォーマンス #2 更新処理

Entity Framework (EF) の更新処理 (insert, update, delete) は、AutoDetectChangesEnabled に false を設定するだけでパフォーマンスを向上できることがある。

[AutoDetectChangesEnabled 設定方法]

using( var context = new SampleEntities() ){
    context.Configuration.AutoDetectChangesEnabled = false;

    // 更新処理
}

パフォーマンスが向上するケース

[環境]
  • Windows 7 64bit
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0 + Database First + DbContext

[測定コード]
※テーブル定義、経過時間測定メソッドはこの記事と同じ

// 測定処理
static class EfTest
{
    // KeyTable1 の追加処理(insert)の時間
    public static void Key1Insert(bool autoDetect, int count)
    {
        using( var context = new TestEntities() ){
            context.Configuration.AutoDetectChangesEnabled = autoDetect;

            // 既存データを削除
            context.Database.ExecuteSqlCommand("delete from KeyTable1");
            // 追加データを作成
            var date = DateTime.Now;
            var data =
                Enumerable.Range(0, count)
                .Select(i => new KeyTable1 {
                    Key = i.ToString(),
                    Name = "No." + i,
                    Register = date
                }).ToArray();

            Measurement.ConsoleElapsedTime(() => {
                foreach( var d in data ) context.KeyTable1.Add(d);
                context.SaveChanges();
            });
        }
    }

    // KeyTable1 の削除処理(delete)の時間
    public static void Key1Delete(bool autoDetect, int count)
    {
        using( var context = new TestEntities() ){
            context.Configuration.AutoDetectChangesEnabled = autoDetect;

            // 既存データを削除
            context.Database.ExecuteSqlCommand("delete from KeyTable1");
            // 削除データを追加
            for( var i = 0; i < count; i++ ){
                context.Database.ExecuteSqlCommand(
                    "insert into KeyTable1 values({0}, '_', getdate())", i);
            }
            // 削除データを取得
            var data = context.KeyTable1.ToArray();

            Measurement.ConsoleElapsedTime(() => {
                foreach( var d in data ) context.KeyTable1.Remove(d);
                context.SaveChanges();
            });
        }
    }
}

ケース1. 複数件追加


bool autoDetect; // true or false
DbContextLayer.Key1Insert(autoDetect, 1); // ウォームアップ用
DbContextLayer.Key1Insert(autoDetect, 100);
DbContextLayer.Key1Insert(autoDetect, 1000);
AutoDetectChangesEnabled = true
[Key1Insert] 00:00:00.1478355
[Key1Insert] 00:00:00.1596723
[Key1Insert] 00:00:01.9620471
AutoDetectChangesEnabled = false
[Key1Insert] 00:00:00.1454600
[Key1Insert] 00:00:00.1597164
[Key1Insert] 00:00:01.5293208
1 回目はウォームアップ用なので無視。
100 件では同じくらいだが、1000 件になると差が出る。
また、「AutoDetectChangesEnabled = true」の場合、件数の増加と実行時間が非線形。

ケース2. 複数件削除


bool autoDetect; // true or false
DbContextLayer.Key1Delete(autoDetect, 1); // ウォームアップ用
DbContextLayer.Key1Delete(autoDetect, 100);
DbContextLayer.Key1Delete(autoDetect, 1000);
AutoDetectChangesEnabled = true
[Key1Delete] 00:00:00.0689035
[Key1Delete] 00:00:00.1432008
[Key1Delete] 00:00:02.3928906
AutoDetectChangesEnabled = false
[Key1Delete] 00:00:00.0662479
[Key1Delete] 00:00:00.1323060
[Key1Delete] 00:00:01.3451472
ケース1. と似たような結果。
違いは、「AutoDetectChangesEnabled = true」の場合の、ケース2. の方が、件数増加による実行時間の増加が大きいこと。

パフォーマンスが向上する理由


くわしくは上記参照だけど、長いのでまとめる。
DbSet<TEntity>.AddDbSet<TEntity>.Remove は、DetectChanges() というコストの高いメソッドが呼ばれているため。
「AutoDetectChangesEnabled = false」とすると、これを呼ばないようにできる。
ケース1. とケース2. でパフォーマンスが向上しているのは、DetectChanges() の呼び出し回数が減っているため。

DetectChanges() の処理を簡単に言うと、DB から取得したデータと、現在のデータを比べて、変更があればデータのステータス EntityState を更新している。
しかし、DbSet<TEntity>.Add や DbSet<TEntity>.Remove は、対象データのステータスを EntityState.Added や EntityState.Deleted に更新するので、DetectChanges() の実行は不要。
(不要なのに呼ばれる実装になっている理由は不明・・・)

AutoDetectChangesEnabled = false の注意点

上記の通り、追加処理 (insert) と削除処理 (delete) は問題ないのだが、変更処理 (update) の場合、通常処理のままだと問題が出てくる。

[更新できない変更処理]

using( var context = new SampleEntities() ){
    context.Configuration.AutoDetectChangesEnabled = false;

    var data = context.KeyTable1.First(); // KeyTable1 は 1 件以上
    data.Name = "First";
    context.SaveChanges();
}
このコードでは DB に対する更新処理は実行されない。
理由は、context.SaveChanges() で DetectChanges() が呼ばれないようになっているため、EF がデータの更新を検知できず、update 文が実行されないため。

[更新できない変更処理の修正例]

using( var context = new SampleEntities() ){
    context.Configuration.AutoDetectChangesEnabled = false;

    var data = context.KeyTable1.First(); // KeyTable1 は 1 件以上
    data.Name = "First";

    // DetectChanges の手動呼び出し
    context.ChangeTracker.DetectChanges();

    // または、ステータスの手動変更
    context.Entry(data).State = EntityState.Modified;

    // いずれかだけでよい

    context.SaveChanges();
}

ただ、SaveChanges を 1 回しか使わない場合(=DetectChanges の呼び出し回数が変わらない)は「AutoDetectChangesEnabled = false」としても効果はほぼないので、デフォルト(true)のままがベストだと思われる。

まとめ

結局、AutoDetectChangesEnabled の設定をどうすべきかは、ケースバイケースになる。

基本的な方針としては、1 つのトランザクションで複数件の追加・削除がある場合のみ、false を設定。
そのトランザクションに変更が含まれる場合、 SaveChanges 前に DetectChanges を呼び出すか、 EntityState を変更。

常に「AutoDetectChangesEnabled = false」でもいいかもしれないが、上のURL にある注意点に、複雑なことをやると SaveChanges 前に DetectChanges を呼び出すだけでは対応できないケースがあるとか・・・
EF の更新処理は「AutoDetectChangesEnabled = true」がデフォルトとして実装されているので、基本的に変更しない方がいいらしい。

また、EF6.0 から複数件の追加・削除を考慮した DbSet<TEntity>.AddRangeDbSet<TEntity>.RemoveRange が導入されているので、常に「AutoDetectChangesEnabled = true」のままで十分かも。

2015年4月3日

Entity Framework のパフォーマンス #1 LINQ to Entities クエリ

Entity Framework (EF) の LINQ to Entities クエリの 1 回目(コールドクエリ)は、かなり遅い。
1 回目なので DB アクセスも当然遅いのだが、それだけでなく、プログラム側でコストの高い初期化処理を行っているため、遅くなっている。

どんな処理を行っているかは下記参照。

パフォーマンス

いくつかの簡単な LINQ to Entities クエリの SQL 構築時間と実行時間を測定してみる。
[環境]
  • Windows 7 64bit
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0 + Database First + DbContext
    ※ EF5.0 の理由は、VS2010 で DbContext Generator が提供されている最終バージョンのため
    ※ ObjectContext はほぼ使わないと思うのでナシ
    (DbContext は ObjectContext のラッパーなので性能は ObjectContext の方が少し上のはず)

[測定コード]

// テーブル定義
public class KeyTable1
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable2
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable3
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}

// 測定処理
static class EfTest
{
    // KeyTable1 の select の SQL 構築時間
    public static void Key1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
/* 構築される SQL :
SELECT
[Extent1].[Key] AS [Key],
[Extent1].[Name] AS [Name],
[Extent1].[Register] AS [Register]
FROM [dbo].[KeyTable1] AS [Extent1] */
        }
    }

    // KeyTable2 の select の SQL 構築時間
    public static void Key2Select()
    {
        using( var context = new TestEntities() ){
            var query =from item in context.KeyTable2 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable3 の select の SQL 構築時間
    public static void Key3Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable3 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where の SQL 構築時間
    public static void Key1SelectWhere()
    {
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where item.Key == "1"
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where-in (自動コンパイル対象外) の SQL 構築時間
    public static void Key1SelectWhereIn()
    {
        var keys = new[] { "1" };
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where keys.Contains(item.Key)
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    //  KeyTable1 の select の SQL 実行時間
    public static void ExecuteKey1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.AsEnumerable().GetEnumerator());
        }
    }
}

// 経過時間測定ユーティリティ
static class Measurement
{
    public static void ConsoleElapsedTime(Action act)
    {
        var caller = new StackFrame(1);
        var watch = Stopwatch.StartNew();
        act();
        watch.Stop();
        Console.WriteLine("[{0}] {1}", caller.GetMethod().Name, watch.Elapsed);
    }
}

実験1. 同じテーブルに対する select

Key1Select × 5
[Key1Select] 00:00:00.1411941
[Key1Select] 00:00:00.0002129
[Key1Select] 00:00:00.0001244
[Key1Select] 00:00:00.0001124
[Key1Select] 00:00:00.0001150
1 回目はとにかく遅い。
2 回目は色々なキャッシュが効いて速くなる。(この結果だと 663 倍)
3 回目も少し速くなり、以降横ばい。

実験2. 複数のテーブルそれぞれに対する select

Key1Select × 3 → Key2Select × 3 → Key3Select × 3
[Key1Select] 00:00:00.1421443
[Key1Select] 00:00:00.0002142
[Key1Select] 00:00:00.0001231
[Key2Select] 00:00:00.0072696
[Key2Select] 00:00:00.0001616
[Key2Select] 00:00:00.0001167
[Key3Select] 00:00:00.0070160
[Key3Select] 00:00:00.0001599
[Key3Select] 00:00:00.0001321
1 回目ほどではないが、各テーブルの 1 回目もそれなりに遅い。
2 回目以降はキャッシュが効いている。

実験3. select 後の select-where

Key1Select → Key1SelectWhere × 3
[Key1Select] 00:00:00.1420541
[Key1SelectWhere] 00:00:00.0013068
[Key1SelectWhere] 00:00:00.0001667
[Key1SelectWhere] 00:00:00.0001364
Key2Select → Key1SelectWhere × 3
[Key2Select] 00:00:00.1429743
[Key1SelectWhere] 00:00:00.0074316
[Key1SelectWhere] 00:00:00.0002014
[Key1SelectWhere] 00:00:00.0001372
QueryKey1Where の 1 回目について、前者(先に同じテーブルの SQL を構築していた場合)の方が速い。
つまり、異なるクエリでも、式木が部分的に一致していればキャッシュが効くようになっている。

実験4. 自動コンパイルされないクエリ

Key1Select → Key1SelectWhereIn × 3
[Key1Select] 00:00:00.1426134
[Key1SelectWhereIn] 00:00:00.0021291
[Key1SelectWhereIn] 00:00:00.0010327
[Key1SelectWhereIn] 00:00:00.0010109
2 回目以降は早くなっているが、自動コンパイルクエリの対象外であるため、実験3. の 2 回目より遅い。

※自動コンパイルクエリについて
EF 5.0 + .NET 4.5 (TargetFramework は 4.0 でも可) の環境から、LINQ to Entities クエリは自動でコンパイルされてキャッシュされるようになっている。(以前は CompiledQuery を手動で実装する必要があった)
しかし、IN 句を構築するための Enumerable.Contains を式木に含めると自動コンパイルの対象外となる。
理由は、LINQ to Entities クエリから SQL が確定できないため。(下記参照)

実験5. SQL 構築後の SQL 実行

ExecuteKey1Select × 3
[ExecuteKey1Select] 00:00:00.1694211
[ExecuteKey1Select] 00:00:00.0020680
[ExecuteKey1Select] 00:00:00.0016365
Key1Select → ExecuteKey1Select × 3
[Key1Select] 00:00:00.1425704
[ExecuteKey1Select] 00:00:00.0296910
[ExecuteKey1Select] 00:00:00.0019183
[ExecuteKey1Select] 00:00:00.0016942
SQL 構築を事前に行っておくと、その分、SQL 実行も速くなっている。
また、1 回目のSQL 構築時間は 1回目の SQL 実行時間と比べてもかなり遅い。
(SQL 実行時間は環境依存なのであまり比較に意味はないかもしれないが・・・)

まとめ

上記から明らかなように、LINQ to Entities クエリの 1 回目は遅い。

対策としては、動作時間の長いプログラム(Web アプリやサービス)だと、起動時に全テーブルの SQL 構築処理を実行して事前キャッシュしておく、などが可能だが、1 回の実行で終わるようなプログラムの場合はどうしようもない。
そういうものに EF は使うべきではない、ということなんだろう・・・
今のところ、SQL 構築のオーバーヘッドは大きすぎると思われるので、パフォーマンスが求められる場合も EF は向いていない。

EF 5.0 以降を使用している場合、.NET 4.5 をインストールしておくことは割と重要。
複雑なクエリほど SQL 構築に時間がかかるため、自動コンパイルクエリが有効だと、2 回目以降のパフォーマンスが向上する。
CompiledQuery でクエリのキャッシュを手動管理することも可能だが、DbContext では使えないことに注意。(CompiledQuery は ObjectContext のみ対応)

クエリの初期化処理に「ビュー生成」があり、これは事前に作成しておくこと可能だが、上記の測定ではあまり効果が無かったため、省略している。
Entity Framework Power Tools を使えば、プリコンパイルビューが簡単に生成可能。

2015年3月25日

IQueryable はタイプセーフではない

Entity Framework (EF) 使いにとっては常識かもしれないが、IQueryable は残念ながらタイプセーフではない。
より正確に言うと、式木の構築はタイプセーフだが、IQueryable.Provider.Execute による式木の実行はタイプセーフでなくなる。

なぜなら、IQueryProvider.Execute の実装が、すべてのメソッドに対応しているわけではないため。

例えば EF の場合、IQueryProvider.Execute は式木から SQL を構築して DB に投げるが、式木内に SQL 変換が考慮されてないメソッドがあった場合、エラーを発生させるしかない。
特に自作メソッドについては対応しようがないため、すべてエラーとなる。

[EF のエラー発生サンプル]

// テーブル定義
public partial class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
}

// 実行処理 (クラス内)
public static void Sample()
{
    using( var context = new SampleEntities() ){
        var query =
            from item in context.Customer
            select item.Name.ToUpperInvariant();
        // ToUpperInvariant() は LINQ to Entities 未対応
        // ToUpper() は対応してる (EF 5.0 で確認)

        foreach( var item in query ) Console.WriteLine(item);
        // System.NotSupportedException 発生
    }
}

例外的に、Queryable.AsQueryable から生成できる EnumerableQuery は、ほぼタイプセーフになっている。
これの IQueryProvider.Execute の実装は、式木内の Queryable クラスのメソッドを Enumerable クラスの同名のメソッドに置換した後、コンパイルしてラムダ式を作成し、そのまま実行している。
つまり、LINQ to Objects に変換して実行するだけなので、自作メソッドでも何でも使用可能。

参考情報

LINQ to Entities で使用可能なメソッドについては、下記参照。
上記にないものについては、EntityFunctionsSqlFunctions (SQL Server 専用) を使用する。

LINQ to Entities で自作メソッドを SQL にマップする方法も一応ある。
LINQ to Entities が実際に実行できるかを確認したい場合は、LINQPad がオススメ。
というか競合してるアプリが他にない・・・

2015年3月21日

IQueryable の仕組みについて

Entity Framework (以下、EF)LINQ to Entities を実現するための IQueryable インターフェイスについて、今まであまり理解してなかったので MSDN とソースからお勉強。
理解してなくても使える EF は凄い。

IQueryable と IQueryProvider


public interface IQueryable : IEnumerable {
    Expression Expression { get; }
    Type ElementType { get; }
    IQueryProvider Provider { get; }
}
public interface IQueryable<out T> : IEnumerable<T>, IQueryable { }

public interface IQueryProvider{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);

    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

ソースの通りなのだが、とりあえず要点をまとめると
  • IQueryable.Expression は式木(式ツリー)を保持 (IQueryable の本体)
  • IQueryable.ProviderIQueryProvider を提供
  • IQueryable の通常版・ジェネリック版の違いは、継承元の IEnumerable がジェネリック版かどうか
  • IQueryProvider.CreateQuery(Expression) は引数の式木から IQueryable インスタンスを作成する
    (主に IQueryable の拡張メソッドで使用される)
  • IQueryProvider.Execute(Expression) は引数の式木を実行する
    (LINQ to Entities なら式木から SQL を構築して実行し、結果を返す)

IQueryable と IQueryProvider の実装方法は下記参照。

Queryable 拡張メソッド(IQueryable の標準クエリ演算子)

System.Linq.Queryable クラスの拡張メソッドの処理についてソースから説明。

[Queryable.Select の実装 (見易さのため一部整形)]

public static IQueryable<TResult> Select<TSource,TResult>(
    this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector
) {
    if (source == null) throw Error.ArgumentNull("source");
    if (selector == null) throw Error.ArgumentNull("selector");

    return source.Provider.CreateQuery<TResult>( 
        Expression.Call(
            null,
            ((MethodInfo)MethodBase.GetCurrentMethod())
                .MakeGenericMethod(typeof(TSource), typeof(TResult)), 
            new Expression[] { source.Expression, Expression.Quote(selector) }
        )
    );
}
[行毎の説明]
4-5 引数の null チェック
7 IQueryable インスタンスを新たに作成して return
※ IQueryable はイミュータブルなので、式木を変更するには新規作成が必要
8 IQueryProvider.CreateQuery の引数 : Expression.Call はメソッド呼び出しの式木を作成
9 Expression.Call の第 1 引数 : static メソッドを呼び出すので null を指定
10-11 Expression.Call の第 2 引数 : 自身 (Queryable.Select) を呼び出しメソッドとして指定
12 Expression.Call の第 3 引数 : 呼び出すメソッドの引数を式木で指定
1 個目の引数は IQueryable.Expression を使用し、2 個目の引数は selector 引数を使用
Expression.Quote は式木型引数を表すために使うものらしい

式木に触ったことが無い場合は意味不明かもしれないが、やってることは単純で、自身のメソッド呼び出しを式木に変換しているだけである。
Queryable クラスの他の拡張メソッドも、一部を除いてほぼ同じ実装。
(こういう実装の場合、CSS のクラスみたくメソッドの定義に対してカンマ区切りが使えれば理想かも)

また、式木 (Expression 型) は、ToString() で簡単に中身を見ることができる。

[Where と Select を使った式木の表示]

var source = new[]{ 0, 1, 2, 3, 4, 5 };
// 純粋な IEnumerable に対する AsQueryable() は EnumerableQuery を生成
var query =
    from n in source.AsQueryable()
    where n % 2 == 1
    select n * n;

Console.WriteLine(query.Expression);
Console.WriteLine(string.Join(",", query));
[結果]
System.Int32[].Where(n => ((n % 2) == 1)).Select(n => (n * n))
1,9,25

まとめ

  • IQueryable は式木を構築するためのインターフェース
  • Queryable 拡張メソッドは式木を簡単に構築するためのユーティリティ

Expression.Quote の存在に疑問を持った場合は下記が参考になる。

2015年3月19日

IE10 table に colspan を指定するとカラムの下線が消えるバグ

検索でもたくさん引っかかる割と有名なバグ。
今更という気もするが、自分も遭遇したため、現象と解決手段をまとめておく。

発生条件(AND)
  • IE10 実機
  • ドキュメントモード: 標準 / Quirks / Internet Explorer 9 標準 / Internet Explorer 8 標準
  • table に { border-collapse: collapse; } を指定
  • td と th に 0 でない border-bottom を指定
  • td または th に 2 以上の colspan を指定

現象が発生するコードと画面キャプチャ。
[CSS]

table { border-collapse: collapse; width: 400px; }
td, th { border: 1px solid black; }
[HTML]

<table>
    <tr>
        <th>col</th>
        <th>col</th>
        <th>col</th>
    </tr>
    <tr>
        <td colspan=3>col3</td>
    </tr>
</table>
[画面キャプチャ]
IE10 table border bug image

常に発生するわけでもなく、画面の再描画で正常に表示されたりする。
開発者ツールを使うと再描画が走るので調査できなかったり・・・

解決手段

発生条件からどれか 1 つを取り除けば、現象は発生しない。
「IE10 を使わない」が理想だけど、実際はドキュメントモードを変更または CSS で何とかすることになると思う。

上のコードに対する CSS による解決例。
※ CSS ハックなどで、IE10 にだけ下記を適用
 (全ブラウザに対して適用してもコードが特殊になるだけで害はないはず・・)

/* カラムの下線を 0 に指定 */
td, th { border-bottom: 0 none; }

/* 代わりに table の下線を指定 */
table { border-bottom: 1px solid black; }

/* または、tr でも可能 (tr の border は IE8以降で有効) */
tr { border-bottom: 1px solid black; }

IE お得意の条件付きコメントは IE10 から廃止されているため、IE10 のみ適用する場合は CSS ハックまたは javascript に頼るしかない。
まだバグが残ってるのに・・・

2015年3月12日

設定ファイル <appSettings> の 1 キーに複数値を設定

設定ファイル(app.config/web.config)の <appSettings> セクションで、キー 1 つに対して複数の値を設定したい場合がよくある。
そういう場合、ConfigurationSection を自作するのが一般的だと思われるが、実装が面倒だし、構成が異なるセクションに対して使いまわせるわけでもない。
複雑な設定値に対しては有効だが、簡単な設定値(数列など)に対しては過剰すぎる。

同じキーの <add> を複数記述しても、複数値を設定できない。
<add> なのに複数追加でないのは納得しにくい・・・というかネーミングが悪い。
[設定ファイル]

<configuration>
  <appSettings>
    <add key="numbers" value="1" />
    <add key="numbers" value="2" />
    <add key="numbers" value="3" />
  </appSettings>
</configuration>
[読取処理]

var numbers = ConfigurationManager.AppSettings.GetValues("numbers");

foreach( var number in numbers ) Console.WriteLine(number);
// 3
// ※最後の値しか入っていない
ConfigurationManager.AppSettings の型は NameValueCollection

また、<appSettings> セクションの動作は、.NET 1.1 なら IConfigurationSectionHandler を実装してカスタマイズできたが、.NET 2.0 以降は使用できなくなっている。(<add> の挙動を NameValueCollection.Add に変更するカスタマイズ例

CSV で設定

配列データの場合、CSV(カンマ区切り値)が最もシンプルに設定できる。

[設定ファイル]

<configuration>
  <appSettings>
    <add key="numbers" value="1,2,3" />
  </appSettings>
</configuration>
[読取処理]

// null 未考慮
var numbers = ConfigurationManager.AppSettings["numbers"].Split(',');

foreach( var number in numbers ) Console.WriteLine(number);
// 1
// 2
// 3

JSON で設定

配列よりも複雑なデータを設定したい場合(名前付きのデータなど)、JSON が便利である。
※JSON のデシリアライズには Json.NET がオススメ。(.NET 標準のものより簡単・便利なので)

[設定ファイル]

<configuration>
  <appSettings>
    <add key="ageRange" value="{'Min':10, 'Max':20, 'Name':'AgeRange'}" />
  </appSettings>
</configuration>
[読取処理]

//---- クラス定義 ----//
public class RangeInfo
{
    public int Min { get; set; }
    public int Max { get; set; }
    public string Name { get; set; }
}

//---- 以降はメソッド内で ----//
// null 未考慮
var ageRange = JsonConvert.DeserializeObject<RangeInfo>(
    ConfigurationManager.AppSettings["ageRange"]
);

Console.WriteLine(ageRange.Min);  // 10
Console.WriteLine(ageRange.Max);  // 20
Console.WriteLine(ageRange.Name); // AgeRange

クラスのシリアライズ/デシリアライズなら、XML やバイナリでも可能だが、XML の属性値に入れて運用することを考えると、今のところ JSON がベスト。
Json.NET ならシングルコーテーション「'」が使えることも大きい。

2015年3月11日

LINQ で 2 個ずつ処理

シーケンスを 2 個ないし n 個ずつ処理したい場合がたまにある。
開始時間と終了時間が 1 行毎にあるログから、実行時間を抽出するとか

標準の LINQ のみだとインデックスによるグルーピングで可能だが、コードが長くなるし、パフォーマンスを必要以上に落としている気がする。

[標準 LINQ のみの実装例]

var items = new[]{"a", "b", "c", "d", "e"};

var twoEach = items
    .Select((item, index) => new {item, index})
    .GroupBy(elem => (int)(elem.index / 2), elem => elem.item);

foreach( var each in twoEach ){
    Console.WriteLine(string.Join(",", each));
}
[結果]
a,b
c,d
e

で、拡張メソッドを自作する前にググった所、ドンピシャなものが。
Interactive Extensions (Ix-Main) の Buffer

[Interactive Extensions 使用の実装例]

var items = new[]{"a", "b", "c", "d", "e"};

var twoEach = items.Buffer(2);

foreach( var each in twoEach ){
    Console.WriteLine(string.Join(",", each));
}
※結果は上と同じ
※Buffer の戻り値は IEnumerable<IList<TSource>> なのでインデックスアクセスも可能

Interactive Extensions はどんどん使おう、という話。

2015年3月7日

ProcessStartInfo.Arguments 設定時のエスケープ方法

外部プログラムのコマンドライン引数を指定する ProcessStartInfo.Arguments は、単なる string 型なので、コマンドラインのエスケープを考慮する必要がある。
エスケープを考慮してくれるプロパティがあってもいい気がするが、エスケープ仕様はプログラム(コンパイラ)依存なので、実装してないのだろう・・・たぶん。

C# コンパイラのエスケープ仕様は下記。
たぶん、大抵の Windows のプログラムは、この仕様に沿ってるはず・・・
(VC++ と java は確認済)

エスケープに関する要点。
  • スペース/タブが引数の区切り

    ※コマンドプロンプトでタブを入力したい場合、「cmd /F:OFF」で起動

  • 引数がスペース/タブを含む場合、「"」デリミタで囲む
  • "」リテラルは、「\"」とする
  • "」直前(デリミタ/リテラル問わず)の「\」リテラルは、「\\」とする

    \」リテラルが複数個の場合は、その分エスケープ(「\」の数が2倍になる)

  • "」直前でない「\」は、そのままリテラルとして扱われる(エスケープ不要)

[エスケープ+デリミタ付加処理の実装例]

/// 
/// コマンドライン引数 1 個をエンコード
/// 
public static string EncodeCommandLineValue(this string value)
{
    if( string.IsNullOrEmpty(value) ) return "";
    var containsSpace = value.IndexOfAny(new[]{' ', '\t'}) != -1;

    // 「\…\"」をエスケープ
    // やってることは、「"」直前の「\」の数を 2倍+1
    value = _commandLineEscapePattern.Replace(value, @"$1\$&");

    // スペース/タブが含まれる場合はデリミタで囲み、末尾が「\」だった場合、エスケープ
    if( containsSpace ){
        value = "\"" + _lastBackSlashPattern.Replace(value, "$1$1") + "\"";
    }
    return value;
}
private static readonly Regex _commandLineEscapePattern = new Regex("(\\\\*)\"");
private static readonly Regex _lastBackSlashPattern = new Regex(@"(\\+)$");

/// 
/// コマンドライン引数複数個をエンコードして、スペースで結合
/// 
public static string EncodeCommandLineValues(this IEnumerable<string> values)
{
    if( values == null ) throw new ArgumentNullException("values");
    return string.Join(" ", values.Select(v => EncodeCommandLineValue(v)));
}

[上記 EncodeCommandLineValue の実行例]
value 戻り値
通常 abc abc
デリミタ対象 a bc "a bc"
エスケープ対象 a\b"c\"d a\b\"c\\\"d
両方 a "b" c "a \"b\" c"
両方(末尾「\」) abc \ "abc \\"

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.5.2

参考URL

2015年2月28日

ASP.NET __doPostBack は jQuery.submit(handler) 未対応

jQuery.submit(handler) でイベントを登録しても、__doPostBack によるサブミット(ポストバック)では呼び出してくれない。
原因は、jQuery.submit(handler) は基本的にイベントリスナ(addEventListener, attachEvent)を使用し、__doPostBack はイベントリスナを考慮してないためである。

[一般的な __doPostBack の実装]

var theForm = document.forms['form1'];
if (!theForm) {
    theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}

DOM の onsubmit プロパティは考慮してくれているが、イベントリスナについてはノータッチである。
(ざっと調べた感じ、イベントリスナのみの呼び出しはかなり面倒・・・というかできるか怪しい)

__doPostBack を使わないようにすれば問題ないのだが、asp:LinkButton や各種コントロールのイベント(SelectedIndexChanged など)を使ってしまうと、__doPostBack の使用は回避できない。

解決手段① onsubmit プロパティを使用

__doPostBack の実装に合わせて、素直に onsubmit プロパティを使ってやれば問題ない。
ただし、複数イベントの登録が面倒になる。
(onsubmit は function を 1 つしか登録できない)

解決手段② __doPostBack 上書き

javascript は関数の上書きができるので、__doPostBack を jQuery.submit(handler) を考慮する関数で上書きする。

[__doPostBack 上書き]

if( typeof __doPostBack === 'function' && typeof theForm === 'object' ){
    var __doPostBackOriginal = __doPostBack;
    __doPostBack = function(eventTarget, eventArgument){
        if( $(theForm).triggerHandler('submit') !== false ){
            // イベントが登録されてない場合は undefined が返ってくるので厳密比較
            // イベントで false を返せば、サブミットは実行されない
            __doPostBackOriginal(eventTarget, eventArgument);
        }
    };
}

注意事項として、上記は event.preventDefault には対応してない。
event オブジェクトはクロスブラウザ対応が面倒なので・・・

また、aspx は <form runat=server> をページ内に 1 つしか定義できないため、theForm が複数あるなどの考慮は不要。

解決手段③ form.submit 上書き

上記と似ているが、今度は大元である form.submit を「サブミットボタン押下」の挙動に変更する。
form.submit では登録したイベントは発生しないが、HTMLElement.click を使ってボタン押下をシミュレートすると、イベントを発生させることができる。

[form.submit 上書き]

// 全 form 対象
[].forEach.call(document.forms, function(f){
    f.submitOriginal = f.submit;
    f.submit = function(){
        // 非表示のサブミットボタンを追加 -> 押下 -> 削除
        $('<input type="submit" name="_適当な重複しにくい名前_" style="display:none" />')
        .appendTo(f).click().remove();
    };
});

注意事項として、onsubmit プロパティを使用している場合は、__doPostBack を使うとイベントが 2 回呼ばれてしまう。
jQuery.submit() でサブミットした場合も、jQuery で登録したイベントが 2 回呼ばれてしまう。
なので、これらは使わないようにするしかない。

副作用は大きいが、form.submit でイベントが発生して欲しい場合は、便利かもしれない。
逆に、イベントが発生しないサブミットは、form.submitOriginal を使用する。

2015年2月22日

table カラムの colspan と nth-child の相性問題

この記事のテーブルレイアウトで苦労したのでメモ。

例えば、列数 > 2 のテーブルの先頭 2 列を色指定したい場合、nth-child を使うと簡単にできる。
[CSS]

/* td と th の先頭 2 列を指定 */
td:nth-child(-n+2), th:nth-child(-n+2) {
    background-color: purple;
}

しかし、先頭 2 列に colspan=2 が含まれる場合・・・
[HTML]

<table>
    <tr>
        <td>1</td>
        <td>2</td>
        <td>3</td>
    </tr>
    <tr>
        <td colspan=2>1-2</td>
        <td>3</td>
    </tr>
</table>

nth-child は colspan を考慮してくれないため、破綻する。
[プレビュー]
123
1-23

解決手段

まず、<colgroup> と <col> を使ってスタイルを指定する方法がある。
◇cssでセル幅を指定するとcolspanで破綻する: 万象酔歩 にくわしく説明されているので、ここでは割愛。

基本的には上記でいいのだが、<td> や <th> に既にスタイルを割り当てている場合、<colgroup> が使えない。
<colgroup> と <col> では、 !important を指定しても <td> と <th> のスタイルを上書きできない。

もう 1 つの方法として、非表示の <td> <th> を利用して、nth-child を適用する方法がある。
[HTML]

<table>
    <tr>
        <td>1</td>
        <td>2</td>
        <td>3</td>
    </tr>
    <tr>
        <td colspan=2>1-2</td>
        <td style="display:none"><!-- 非表示カラム --></td>
        <td>3</td>
    </tr>
</table>
[プレビュー]
123
1-23

非表示要素を使うのは微妙だが、対象のカラムすべてに class を書いたりするよりはましである。

その他

Selectors Level 4 の Column combinator が実装されれば、こんな工夫はいらなくなるはず。

2015年2月11日

プロパティのアクセシビリティと PropertyInfo

PropertyInfo を取得する際、アクセシビリティで混乱したので調査。

プロパティ定義 GetProperties の引数 BindingFlags PropertyInfo 情報
get
アクセサ
set
アクセサ
CanRead CanWrite GetMethod
.Attributes
SetMethod
.Attributes
public public Public Public Public
public private Public Public Private
private public Public Private Public
public - Public Public -
- public Public - Public
protected protected NonPublic Family Family
internal internal NonPublic Assembly Assembly
protected
internal
protected
internal
NonPublic FamORAssem FamORAssem
private private NonPublic Private Private
private - NonPublic Private -
- private NonPublic - Private
※GetMethod と SetMethod の MethodBase.Attributes は、Public/Private/Family/Assembly/FamORAssem
のみ抽出

[調査コード]

class Program
{
    static void Main(string[] args)
    {
        ShowPropInfo(new Props());
    }
    static void ShowPropInfo<T>(T source)
    {
        var t = typeof(T);
        Console.WriteLine("[Public]");
        ShowPropDetail(t.GetProperties(BindingFlags.Instance | BindingFlags.Public));

        Console.WriteLine();

        Console.WriteLine("[NonPublic]");
        ShowPropDetail(t.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic));
    }
    static void ShowPropDetail(IEnumerable<PropertyInfo> props)
    {
        foreach( var prop in props ){
            Console.WriteLine("Name:{0} CanRead:{1} CanWrite:{2}",
                prop.Name, prop.CanRead, prop.CanWrite);

            if( prop.GetMethod != null ) Console.WriteLine(
                "  <Get> Name:{0} Attributes:[{1}]",
                prop.GetMethod.Name, prop.GetMethod.Attributes);

            if( prop.SetMethod != null ) Console.WriteLine(
                "  <Set> Name:{0} Attributes:[{1}]",
                prop.SetMethod.Name, prop.SetMethod.Attributes);
        }
    }
}

class Props
{
    public int plGetSet { get; set; }
    public int plGetpvSet { get; private set; }
    public int pvGetplSet { private get; set; }
    public int plGet { get { return 0; } }
    public int plSet { set { } }

    protected int ptGetSet { get; set; }
    internal int inGetSet { get; set; }
    protected internal int ptinGetSet { get; set; }

    private int pvGetSet { get; set; }
    private int pvGet { get { return 0;} }
    private int pvSet { set { } }
}

まとめ

  • get アクセサ/set アクセサのいずれかが public → BindingFlags.Public の対象
  • 上記以外 → BindingFlags.NonPublic の対象

  • get アクセサ無し → CanRead : False → GetMethod : null
  • set アクセサ無し → CanWrite : False → SetMethod : null

2015年2月10日

VB.NET Action デリゲート型に Function ラムダ式を代入

タイトル通りなのだが、VB.NET では下記のようなコードがコンパイル&実行可能。

Module TestDelegate
    Sub Main()
        Dim act1 As Action = Function() True
        act1()

        ' 引数があるデリゲートも
        Dim act2 As Action(Of Integer) = Function(i) i + 1
        act2(0)
        ' ラムダ式の引数を省略可能
        act2 = Function() True
        act2(0)
        act2 = Sub() Console.WriteLine("void")
        act2(0)
    End Sub
End Module

'[実行結果]
'> void

「厳密でないデリゲート変換」というもので、ラムダ式が導入された時 (Visual Studio 2008) からあった模様。

「Option Strict On」でも適用されるため、「タイプセーフ?なにそれ?おいしいの?」のような機能・・・
前の投稿とかぶるが、これを無効化する Option はあった方がいい。

ちなみに、自分がやってしまったバグ

Module BugExample
    Sub Main()
        Dim elements As New List(Of Element)
        ' 中略
        elements.Where(適当な条件).ForEach(Function(elem) elem.Enabled = True)
    End Sub
End Module

Class Element
    Public Property Enabled As Boolean
    ' 以下略
End Class
※ ForEach は Interactive Extensions の拡張メソッド。
Sub と Function を間違えたのだが、まったく意味が違ってくる。
代入と比較の記号が同じであるため、コンパイルが通り、テストするまで気付けなかった。

2015年1月30日

VB.NET 匿名デリゲートと 3 項演算子で実行時エラー

下記は、何の問題もなさそうでコンパイル可能なコードだが、実行時エラーが発生する。
[コード]

Module TestDelegateIf
    Sub Main()
        Dim text As String = Nothing
        Dim fn As Func(Of Integer) = If(text Is Nothing, Nothing, Function() text.Length)
    End Sub
End Module
[結果]
ハンドルされていない例外: System.ArgumentException: インスタンス メソッドへのデリゲートに null の 'this' を指定することはできません。
   場所 System.MulticastDelegate.CtorClosed(Object target, IntPtr methodPtr)
   場所 TestDelegateIf.Main()

3 項演算子を使っているのに、Function() text.Length の部分が実行されているっぽく??????
Function ラムダ式だけでなく、Sub ラムダ式でも同じ現象が発生する。

解決手段?

3 項演算子でなく If 文を使えばエラーは起きない。
匿名デリゲートを Func(Of Integer) に代入してもエラーは起きない。
[正常コード1]

Module TestDelegateIf
    Sub Main()
        Dim text As String = Nothing

        ' If を使用
        Dim fn As Func(Of Integer)
        If text Is Nothing Then
            fn = Nothing
        Else
            fn= Function() text.Length
        End If

        ' 匿名デリゲートの代入
        Dim fn_ = Function() text.Length
        fn = fn_
    End Sub
End Module

匿名デリゲートを使わない方法もエラーは起きない。
※ C# だと匿名デリゲートが存在しないため、これと同等のコードになる。
[正常コード2]

Module TestDelegateIf
    Sub Main()
        Dim text As String = Nothing
        Dim fn = If(text Is Nothing, Nothing, New Func(Of Integer)(Function() text.Length))
    End Sub
End Module

まとめ

この現象は、たぶんコンパイラのバグに該当するものだと思われる。
VB の匿名デリゲートは便利だが、同時に無効化する Option が欲しい。というかあった方がいい。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.5.2