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 で使用可能なメソッドについては、下記参照。
上記にないものについては、EntityFunctions や SqlFunctions (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.Provider は IQueryProvider を提供
  • 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