2014年9月25日

System.Collections.ObjectModel.Collection<T> について

System.Collections.ObjectModel にある Collection<T> について、ソース観点からの補足説明のようなもの。

主な用途は、継承用クラスになる。
実際、System.Collections.ObjectModel にある他クラスの継承元として使われている。
単品で使用できないこともないが、IList<T> ないし List<T> の単なるラッパになるので(Collection<T> の引数無しコンストラクタは内部で List<T> を生成)、ラップするメリットがあまりない。
List<T> の機能を制限したいなら、基本的にインターフェースへのキャストで十分。

状態変更時に挟める protected メソッドとして、ClearItemsInsertItemRemoveItemSetItem が用意されている。
これらと状態変更用の public メソッド・プロパティとの関係は下記
public protected protectedの実装 備考
Item
(インデクサ)
SetItem IList[index] = item protected メソッド実装時、index 引数の境界値チェックは不要。(public 側で実施済)
Add InsertItem IList.Insert(index, item)
Insert
Remove RemoveItem IList.RemoveAt(index)
RemoveAt
Clear ClearItems IList.Clear()

注意すべきは、Add と Insert で同じ InsertItem メソッドが使われる点。
Add(item) は Insert(Length - 1, item) として処理される。

表でも自明なように IList.Add は使用されない。
自前の IList のラッパーとする場合、IList.Add のみの特殊処理を実装していても使われないので注意。
(普通は IList.Insert と差がある実装にしないだろうけど・・・)

2014年9月24日

NuGet のリポジトリパスの変更

NuGet のリポジトリパス(パッケージがダウンロードされる場所)は、デフォルトだと次である。
$(ソリューションディレクトリ)\packages\

ソリューション 1 つだとデフォルトのままでも問題ないのだが、複数だとソリューション毎にリポジトリパスが存在し、それぞれパッケージがダウンロードされることになる。

・・・同じパッケージが違うディレクトリにダウンロードされるのって微妙じゃね?
ということが考慮されてかは分からないが、リポジトリパスは nuget.config を書くことで任意のパスを指定できる。

[リポジトリパス指定用の nuget.config 記述例]

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <config>
    <add key="repositoryPath" value="リポジトリパス(絶対or相対パス)" />
  </config>
</configuration>

ちなみに nuget.config の配置場所。
ソリューション毎 $(ソリューションディレクトリ)\nuget.config
共通(ユーザー毎) %APPDATA%\NuGet\NuGet.Config

nuget.config の他の設定項目。

2014年9月11日

配列のなんちゃって共変性

共変性

共変性とは、要は暗黙の型変換を容易にするための機能である。
参照型の型 A が型 B へ暗黙の型変換が可能な場合に、ジェネリックインターフェースの共変な型パラメータ(out 付き型パラメータ)に型 A を指定した If<A> は同じインタフェースの If<B> へ暗黙の型変換が可能になる。
※out が付いた型パラメータは出力(メソッドの戻り値、読取専用プロパティの型)にしか使えなくなる。
   出力用の out 引数には使えない。→理由

インタフェースの他に、デリゲートの型パラメータも共変にすることができる。

[共変性のサンプル]

class A : B { } // B はクラスまたはインターフェース

static class TestCovariance
{
    static void Main(string[] args)
    {
        // 暗黙の型変換
        A a = new A();
        B b = a;

        // 共変のインターフェース IEnumerable<out T>
        IEnumerable<A> aList = new List<A>();
        IEnumerable<B> bList = aList;

        // 共変のデリゲート Func<out TResult>
        Func<A> aFunc = () => new A();
        Func<B> bFunc = aFunc;
    }
}

配列の共変性

C# 及び VB の配列は共変性がある。

共変性があるのに、不変な型パラメータを持つ ICollection<T>IList<T> を実装している。
※普通のコードでは実装できないため、実行時に提供するという特殊なやり方で。
これはジェネリック(.NET 2.0~)が配列(.NET 1.0~)より後に導入されたためでもあるのだが、そもそもイミュータブルでない配列を共変にしているのがおかしな話なのである。

MSDN にも書いてある話なのだが、配列を暗黙の型変換することでタイプセーフではなくなる。
例えば、次のコードはコンパイルできるが、実行時エラー ArrayTypeMismatchException が発生する。

static class TestCovariance
{
    static void Main(string[] args)
    {
        string[] strings = {"1"};
        object[] objects = strings;
        objects[0] = 1; // ここで例外発生
    }
}

IReadOnlyList<T>

上記の気持ち悪さを解決するためかは分からないが、.NET 4.5 からイミュータブルな配列を扱える IReadOnlyList<T> が導入されている。

static class TestCovariance
{
    static void Main(string[] args)
    {
        // Array.AsReadOnly の戻り値は ReadOnlyCollection<T>
        // このメソッドは .NET 2.0 からあったが IReadOnlyList<T> が存在しなかった
        IReadOnlyList<string> strings = Array.AsReadOnly(new[]{"1"});
        IReadOnlyList<object> objects = strings;
        objects[0] = 1; // コンパイル NG
    }
}

.NET 4.0 までの資産は IList<T> や ICollection<T> 前提で作ってあることが多いので、今更導入されてもという気がするのだが、よりタイプセーフに実装できるということで・・・

参考URL

2014年9月2日

コンパイラをごまかすキャスト

※実質、.net - C# non-boxing conversion of generic enum to int? - Stack Overflow の紹介記事です。

ジェネリック型パラメータと Enum の相性は悪い。
型パラメータの制約として、Enum クラスを指定できない。
Enum に対する制約は、せいぜい下記のようにしかできない。(ゆるい制約だとstructだけの場合も)
[サンプル1 : Enum に対する制約]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
}

問題は、型パラメータで受け取った Enum 値を int などにキャストする場合である。
[サンプル1]で value を int へキャストしようとすると、コンパイラに怒られる。
TEnum の制約では、int へ変換可能かどうかを判断できないためである。
int type conversion error

object キャスト

これを解決する最も簡単な方法は、一度 object へキャストすることである。

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = (int)(object)value;
}

ただし、この方法はコードの通りボックス化(ボクシング)→ボックス化解除(アンボクシング)であるため、あまりパフォーマンス的によろしくない。

動的ラムダ式でキャスト

ボックス化を回避できる方法として、式木で動的ラムダ式を構築してキャストする方法がある。
このコードを少し変更(Get メソッドの処理を静的コンストラクタに移動)。

public static class CastTo<T>
{
    private static class Cache<S>
    {
        static Cache()
        {
            // 次のラムダ式を式木で構築
            // (S s) => (T)s
            var p = Expression.Parameter(typeof(S));
            var c = Expression.ConvertChecked(p, typeof(T));
            Caster = Expression.Lambda<Func<S, T>>(c, p).Compile();
        }
        internal static readonly Func<S, T> Caster;
    }

    public static T From<S>(S source)
    {
        return Cache<S>.Caster(source);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = CastTo<int>.From(value);
}

Expression.ConvertChecked でキャストするだけのラムダ式を構築している。
Expression.ConvertChecked を使うことで、コンパイラのキャスト可能チェックをスルーできる。
ちなみにオーバーフロー例外を発生させたくない場合は Expression.Convert を使う。

動的IL でキャスト

ここにもあるのだが、キャスト処理を動的IL で構築することで、ボックス化を回避できる。

public static class IlCastToInt32<T>
{
    static IlCastToInt32()
    {
        var method = new DynamicMethod(
            "IlCastToInt32Caster", typeof(int), new[] { typeof(T) });
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Conv_Ovf_I4);
        il.Emit(OpCodes.Ret);
        Caster = (Func<T, int>)method.CreateDelegate(typeof(Func<T, int>));
    }
    public static readonly Func<T, int> Caster;
}

// クラスの型パラメータを省略するための拡張メソッド
public static class IlCastExtensions
{
    public static int CastToInt32<T>(this T value)
    {
        return IlCastToInt32<T>.Caster(value);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = value.CastToInt32();
}

戻り値を決め打ちする必要があるため、動的ラムダ式より汎用性が下がる。
その他の型に変換したい場合は、OpCodes を参照。
また、型パラメータとして渡すもの(ここでは Enum)は public である必要がある。
public でないと TypeAccessException が発生する。

パフォーマンス

[測定コード]

static class TestEnumCast
{
    static void Main(string[] args)
    {
        var flag = int.Parse(args[0]); // 1 ~ 5
        var loopCount = int.Parse(args[1]);
        var source = MuseEnumerable().Take(loopCount).ToArray();
        var temp = new List<int>(loopCount);

        if( flag > 3 ){
            // 4,5 はイニシャルコスト無しの設定
            // -> 一度実行してキャッシュを作成しておく
            flag -= 2;
            CastTo<int>.From(Muse.None);
            Muse.None.CastToInt32();
        }

        var sw = new Stopwatch();
        sw.Start();
        switch( flag ){
        case 1:
            foreach( var item in source ) temp.Add((int)(object)item);
            break;
        case 2:
            foreach( var item in source ) temp.Add(CastTo<int>.From(item));
            break;
        case 3:
            // 拡張メソッドだとメソッド 1 個分のコストがかかるため、直呼び
            foreach( var item in source ) temp.Add(IlCastToInt32<Muse>.Caster(item));
            break;
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed);
    }

    static IEnumerable<Muse> MuseEnumerable()
    {
        while( true ){
            yield return Muse.Honoka;
            yield return Muse.Kotori;
            yield return Muse.Umi;
        }
    }
}

[Flags]
public enum Muse
{
    None = 0x0,
    Honoka = 0x1,
    Kotori = 0x2,
    Umi = 0x4
}

[結果]
loopCount object 動的ラムダ式 動的IL
イニシャルコスト有 10000 0.0003435 0.0053873 0.0031249
イニシャルコスト無 0.0001409 0.0001416
イニシャルコスト有 100000 0.0035345 0.0067660 0.0044378
イニシャルコスト無 0.0014422 0.0014731
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]

結果をまとめると下記。(左の方が高性能)
キャスト : 動的ラムダ式 ≒ 動的IL > object
イニシャルコスト : 動的IL > 動的ラムダ式

キャスト処理に関しては、ボックス化を回避することで 2 倍以上の性能になる。
動的ラムダ式と動的IL で差はほぼないため、
汎用性や型パラメータの public 制限を考慮すると動的ラムダ式、イニシャルコストが少ない方がいいなら動的IL となる。
また、動的IL は拡張メソッドから呼び出すと、わずかだがその分のコストがかかる。

ただ、イニシャルコスト >>> object キャスト 1 回 なので、条件によっては無意味な工夫になる。

検証環境

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