2014年3月28日

C# と VB のちゃんぽん

C#.NET と VB.NET のプロジェクト(csproj/vbproj)を 1 つソリューション(sln)に含めることができる。
同じ IL を生成するので当然といえば当然なんだが、IDE のデメリットが多いため、オススメしない。

Visual Studio 2010 の場合、言語が異なるプロジェクトに対して、下記の不便がある。
  • 参照先をビルドしてないと、インテリセンスが効かない
  • 「定義へ移動」で、ソース本体に移動せず、定義が表示される
  • 「すべての参照を検索」で、検索されない

要は、プロジェクト参照なのに、実質、DLL 参照状態になる。
特別な理由がない限り、ちゃんぽんは避けるべき・・・

※デバッグ実行については、ちゃんぽんでも追ってくれる

2014年3月27日

char.IsDigit() / 正規表現"\d" の注意事項

char.IsDigit()正規表現の"\d" は、半角数字にマッチする。
が、それだけでなく、UNICODEカテゴリ"Nd"(Number, Decimal Digit) にもマッチする。

上記を意識して使わないと、高確率でバグを仕込んでしまう。
例えば、日本語システムだと全角数字("0" ~ "9")の入力が想定できるため、半角数字と区別する必要がある場合に、これらを使用するとバグる。
他に、これらを使ってチェックし、int.Parse() に投げるなども NG。
※普通は、int.TryParse() 推奨

UNICODEカテゴリ"Nd"にマッチすることの検証

[検証コード]

using System;
using System.Globalization;
using System.Text.RegularExpressions;

static class Program
{
    static void Main()
    {
        foreach( var point in DigitZeroPoints ){
            for( var c = point; c < point + 10; c++ ){
                var char_isdigit = char.IsDigit(c);
                var regex_isdigit = Regex.IsMatch(c.ToString(), @"\d");
                if( !char_isdigit || !regex_isdigit ){
                    // どちらかのマッチ失敗時のみコンソール出力
                    Console.WriteLine(
                        "{0:X4} - char={1} regex={2} category={3}",
                        (int)c, char_isdigit, regex_isdigit,
                        CharUnicodeInfo.GetUnicodeCategory(c));
                }
            }
        }
    }

    // UNICODEカテゴリ"Nd"の ZERO のコードポイント一覧
    // ※FileFormat.Info から 2014/03/26 に取得
    // ※4Byte 文字は char で表現できないため、除外
    private static readonly char[] DigitZeroPoints = new[] {
        '\u0030',  // DIGIT ZERO
        '\u0660',  // ARABIC-INDIC DIGIT ZERO
        '\u06F0',  // EXTENDED ARABIC-INDIC DIGIT ZERO
        '\u07C0',  // NKO DIGIT ZERO
        '\u0966',  // DEVANAGARI DIGIT ZERO
        '\u09E6',  // BENGALI DIGIT ZERO
        '\u0A66',  // GURMUKHI DIGIT ZERO
        '\u0AE6',  // GUJARATI DIGIT ZERO
        '\u0B66',  // ORIYA DIGIT ZERO
        '\u0BE6',  // TAMIL DIGIT ZERO
        '\u0C66',  // TELUGU DIGIT ZERO
        '\u0CE6',  // KANNADA DIGIT ZERO
        '\u0D66',  // MALAYALAM DIGIT ZERO
        '\u0E50',  // THAI DIGIT ZERO
        '\u0ED0',  // LAO DIGIT ZERO
        '\u0F20',  // TIBETAN DIGIT ZERO
        '\u1040',  // MYANMAR DIGIT ZERO
        '\u1090',  // MYANMAR SHAN DIGIT ZERO
        '\u17E0',  // KHMER DIGIT ZERO
        '\u1810',  // MONGOLIAN DIGIT ZERO
        '\u1946',  // LIMBU DIGIT ZERO
        '\u19D0',  // NEW TAI LUE DIGIT ZERO
        '\u1A80',  // TAI THAM HORA DIGIT ZERO
        '\u1A90',  // TAI THAM THAM DIGIT ZERO
        '\u1B50',  // BALINESE DIGIT ZERO
        '\u1BB0',  // SUNDANESE DIGIT ZERO
        '\u1C40',  // LEPCHA DIGIT ZERO
        '\u1C50',  // OL CHIKI DIGIT ZERO
        '\uA620',  // VAI DIGIT ZERO
        '\uA8D0',  // SAURASHTRA DIGIT ZERO
        '\uA900',  // KAYAH LI DIGIT ZERO
        '\uA9D0',  // JAVANESE DIGIT ZERO
        '\uAA50',  // CHAM DIGIT ZERO
        '\uABF0',  // MEETEI MAYEK DIGIT ZERO
        '\uFF10',  // FULLWIDTH DIGIT ZERO
    };
}

[結果]
1A80 - char=False regex=False category=OtherNotAssigned
:
1A89 - char=False regex=False category=OtherNotAssigned
1A90 - char=False regex=False category=OtherNotAssigned
:
1A99 - char=False regex=False category=OtherNotAssigned
A9D0 - char=False regex=False category=OtherNotAssigned
:
A9D9 - char=False regex=False category=OtherNotAssigned
ABF0 - char=False regex=False category=OtherNotAssigned
:
ABF9 - char=False regex=False category=OtherNotAssigned

TAI THAM HORA DIGIT (1A80~9)、TAI THAM THAM DIGIT (1A90~9)、JAVANESE DIGIT (A9D0~9)、MEETEI MAYEK DIGIT (ABF0~9) が数字文字として判定されず、UNICODEカテゴリが"Cn"(Other, Not Assigned) となった。

原因は、判定されなかった文字の UNICODE バージョンは 5.2.0 で、Windows7/.NET 4.0 の UNICODE バージョンは 5.1 だからである。(参考:.NET の UNICODE バージョン
環境がないので検証できないが、Windows 8/.NET 4.5 なら数字文字として判定されるはず。

おまけ

半角数字のみを判定したい場合は、下記を使っている。

public static bool IsAsciiDigit(this char c)
{
    return '0' <= c && c <= '9';
}

正規表現の場合

// \d を使わず素直に書く
var regex_isdigit = Regex.IsMatch(c.ToString(), "[0-9]");

// または

// ECMAScript 準拠の正規表現ならば、半角数字のみにマッチする
var regex_isdigit = Regex.IsMatch(c.ToString(), @"\d", RegexOptions.ECMAScript);

検証環境

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

2014年3月26日

.NET の UNICODE バージョン

.NET OS UNICODE ソース
4.5 Windows 8 Unicode 6.0 standard .NET 4.5 - String
Windows 7
Windows Vista
Unicode 5.0 standard
4.0 すべて Unicode 5.1 standard .NET 4.0 - String
3.5 すべて Unicode 5.0 standard .NET 3.5 - UnicodeCategory
3.0 すべて Unicode 3.1 standard .NET 3.0 - UnicodeCategory
2.0 すべて Unicode 3.1 standard .NET 2.0 - UnicodeCategory
1.1 すべて Unicode 3.1 standard .NET 1.1 - UnicodeCategory
※ソースの MSDN を元に作成。
※MSDN が間違っている場合もあるので注意

UNICODE のバージョン違いで何が変わるかというと、MSDN にもある通り、ソート、ケーシング(大文字・小文字)、正規化(Normalize)などに差が出てくる。

そして、CharUnicodeInfo に大きく影響する。
.NET Framework 内でこれを使っている処理も多く(上に挙げた処理も使っているはず)、.NET のバージョンを変更する際は、意識する必要がある。

参考URL

2014年3月20日

インスタンスのキャッシュ

instance クラス(インスタンスを生成して使うクラス)でも、static なものとして欲しい場合がある。
例えば、IComparer<T>IEqualityComparer<T> を実装したクラスのインスタンスは、基本的に 1 個あれば十分である。

このようなクラスについては、自身のインスタンスを保持する static プロパティ(またはメンバ)を公開する方法がある。

public class OreOreComparer : IComparer<OreClass>
{
    public static Default { get { return _default; } }
    private static readonly _default = new OreOreComparer();

    public int Compare(OreClass x, OreClass y) { /* 実装 */ }
}

// または

public class OreOreComparer : IComparer<OreClass>
{
    public static readonly Default = new OreOreComparer();

    public int Compare(OreClass x, OreClass y) { /* 実装 */ }
}

しかし、クラス毎に変数を用意するのは面倒。かつ、重複コードなのであまり美しくない。
そこで、generic type caching を使う方法を考えてみた。
呼び出し側が長くなるデメリットがあるが、クラス毎に変数を用意する必要がなくなってスッキリ?する。

// 値型をキャッシュしたくない場合は class 制約もあり
public static class InstanceCache<T> where T : new()
{
    public static readonly T Default = new T();
}

// 使い方 : OreClass のインスタンス OreA と OreB を比較する
var comparer = InstanceCache<OreOreComparer>.Default;
var result = comparer.Compare(OreA, OreB);

2014年3月19日

型(Type)をキーにした場合の高速なキャッシュ

キャッシュを扱う場合、通常は、Dictionary<TKey, TValue>ConcurrentDictionary<TKey, TValue> などを選択する。

generic 型(Type) を key にする場合のみ、上記クラスを使用するより高速な方法が存在する。
"generic type caching" と呼ばれる方法である。

2015/03/13 追記:
C# 6.0 からは nameof 演算子があるので、EXAMPLE の GetName メソッドは使わないこと。

EXAMPLE

下記メソッドを、キャッシュを使って高速化する場合を考える。

// 最初のプロパティ名を取得する。
// 例のように匿名型と組み合わせることで変数名を文字列として取得可能。
// int value = 0;
// new{value}.GetName() -> "value"
//
public static string GetName<T>(this T source) where T : class
{
    var properties = typeof(T).GetProperties(); //ここが遅い
    return properties[0].Name;
}

  • ConcurrentDictionary の例 ※パフォーマンス測定の結果、キャッシュ無しよりも遅い

public static string GetName<T>(this T source) where T : class
{
    return getNameCache.GetOrAdd(typeof(T), t => t.GetProperties()[0].Name);
}
private static readonly ConcurrentDictionary<Type,string> getNameCache =
    new ConcurrentDictionary<Type,string>();

  • generic type caching の例

public static string GetName<T>(this T source) where T : class
{
    return GetNameCache<T>.Name;
}
private static class GetNameCache<T>
{
    static GetNameCache()
    {
        var properties = typeof(T).GetProperties();
        Name = properties[0].Name;
    }
    public static readonly string Name;
}
generic type caching とは、要は、generic & static なクラスをキャッシュとして使うことである。
型引数が key、static メンバが value に相当する。
静的コンストラクタは、クラスの呼び出し時に1回だけ実行され、スレッドセーフである。
そして、型引数はコンパイル時に解決されるため、Dictionary などの動的な key ルックアップと比べて、圧倒的に早い。

パフォーマンス

[測定コード]

byte ba=0, bb=0;
int ia=0, ib=0;
float fa=0f, fb=0f;
char ca='\0', cb='\0';
string sa=null, sb=null;
var temp = new List<string>(loopCount);

for( var i=0; i<loopCount; i++ ){
    switch( i % 10 ){
    case 0: temp.Add(new{ba}.GetName()); break;
    case 1: temp.Add(new{bb}.GetName()); break;
    case 2: temp.Add(new{ia}.GetName()); break;
    case 3: temp.Add(new{ib}.GetName()); break;
    case 4: temp.Add(new{fa}.GetName()); break;
    case 5: temp.Add(new{fb}.GetName()); break;
    case 6: temp.Add(new{ca}.GetName()); break;
    case 7: temp.Add(new{cb}.GetName()); break;
    case 8: temp.Add(new{sa}.GetName()); break;
    case 9: temp.Add(new{sb}.GetName()); break;
    }
}

[結果]
loopCount キャッシュ無し ConcurrentDictionary generic type caching
10000 0.0054555 0.0190749 0.0039957
100000 0.0270494 0.1461569 0.0086326
1000000 0.2428146 1.4022937 0.0465277
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]

結果としては、generic type caching > キャッシュ無し > ConcurrentDictionary となった。(左の方が高性能)
ConcurrentDictionary のルックアップが想定していたよりも遅く、キャッシュ無しの方が早い結果に…。
※一応、Dictionary でも試してみたが ConcurrentDictionary より 2 倍早くなっただけで、キャッシュ無しより遅かった。

キャッシュ無しと比べて generic type caching は、1 万回でも 1.5 [ms] 程しか差は出なかった。
が、共通メソッドとしてできる限りパフォーマンスを良くしたい場合は、導入の価値ありだと思われる。
また、キャッシュの特性として、value の生成処理のコストがより高い場合は、より大きな効果が見込まれる。

まとめ

generic type caching の導入可能条件は下記になる。
  • key が generic 型 (コンパイル時に決定できる Type 型)
  • 削除不要なキャッシュ (一度作成したキャッシュは削除できない)

使いどころとしては、EXAMPLE のようにリフレクション使用時のパフォーマンス向上がメインとなる。
※Type.Get~ や Enum.Get~ など

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

参考URL