ラベル ジェネリック の投稿を表示しています。 すべての投稿を表示
ラベル ジェネリック の投稿を表示しています。 すべての投稿を表示

2016年2月10日

Kotlin ジェネリクス 型引数の情報を実行時に取得

Kotlin 正式版がそろそろリリースされそうなので・・・
GitHub - temp-impl/spring-mvc-singleton-vs-prototype_kotlin を作った時の調査メモ。)

Kotlin も JVM 言語なので、コンパイル時型消去の呪いからは逃れられない。
が、inline と reified を使うことで、関数・メソッドなら型引数の情報を実行時に使用可能になる。

[型引数使用サンプル]

fun main(args: Array<String>) {
    printType<String>()
}

inline fun <reified T> printType() {
    println(T::class)
}

//[結果]
// class kotlin.String

//[補足]
// 上記結果を得るためには kotlin-refrect.jar も必要
// 無くてもエラー無しで実行できるが、下記結果になる
// class java.lang.String (Kotlin reflection is not available)
[上記のバイトコード(一部)]

   L2
    LINENUMBER 8 L2
    LDC Ljava/lang/String;.class
    INVOKESTATIC kotlin/jvm/internal/Reflection.getOrCreateKotlinClass (Ljava/lang/Class;)Lkotlin/reflect/KClass;
    ASTORE 1
    NOP
   L3
    LINENUMBER 11 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

inline というキーワードから想像が付くと思うが、関数の中身がインライン展開されている。
そのため、型引数の情報も当然使用可能。

ちなみに Java クラスの情報が欲しい場合

inline fun <reified T : Any> printJavaType() {
    println(T::class.java)
}

//[結果] (上と同じ main)
// class java.lang.String

//[補足]
// 型引数のデフォルト上限は Any? で Java に無いクラスなので、Any 上限を指定する必要がある
// .java は KClass<T : Any> の拡張プロパティ

検証環境

  • jdk 1.8.0_74
  • Kotlin 1.0.0-rc-1036

雑記

inline 関数の型引数はデフォルト reified でも良さそうと思ったが、たぶん Kotlin は(型引数使用を)明示していくスタイルなのだろう。
(コンパイラの都合もあるかもしれない・・・)

Scala の implicit + ClassTag みたいな仕組みはおそらく導入されない。
Comparison to Scala - Kotlin Programming Language を読むと implicit は導入する気が無さそうなので。

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年8月25日

メソッドの型推論で型パラメータの制約は使われない

[サンプルコード]

static class TestTypeInference
{
    static void Main()
    {
        // コンパイル NG
        DoSomething(new Dictionary<string, int[]>());

        // コンパイル OK
        DoSomething<int[], int>(new Dictionary<string, int[]>());
    }

    static void DoSomething<TCollection, TItem>(IDictionary<string, TCollection> map)
        where TCollection : ICollection<TItem>
    {
        Console.WriteLine("DoSomething");
    }
}

DoSomething メソッドは、map 引数の型から TCollection は推論できるが、TItem は推論できない。
TCollection の制約から分かりそうなもんだが、推論してくれない。

あきらめて型パラメータを指定してあげましょう、という話。

検証環境

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

参考URL

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