小ネタを出してゆく。
C#で數値の定數を得たい。指定した型の數値の定數を得たい。即ち次の樣なインターフェイスを想定する。
using System; public class ConstNumber { public static void Main() { int one = GetOne<int>(); Console.WriteLine(one); } private static N GetOne<N>() { // Something } }
実際は或る場所でのデフォルト値として定數が欲しく、且つ外から型を指定できる抽象的なメソッドが欲しく、且つ此のメソッドの外では型安全でありたかった。
意外と此れはできない。
N GetOne<N>() { return (int) 1; } GetOne<int>(); // => error CS0029: Cannot implicitly convert type `int' to `N'
N GetOne<N>() { return (N) 1; } GetOne<int>(); // => error CS0030: Cannot convert type `int' to `N'
N GetOne<N>() { return (N) ((object) 1); } GetOne<int>(); // => OK. GetOne<long>(); // => System.InvalidCastException: Specified cast is not valid.
ジェネリクスの引數として渡された型がどの型判定するコードは直ぐに書ける。typeof(N)
を確かめればよい。
然し指定した型の定數を作るコードが書けない。何故か。其んな函數は用意されてゐないやうだ。
C++では此れは簡単にできる。
#include<iostream> template <typename N> N getOne() { return (N) 1; } int main() { std::cout << getOne<char>() << std::endl << getOne<double>() << std::endl << getOne<float>() << std::endl << getOne<int>() << std::endl << getOne<long>() << std::endl << getOne<short>() << std::endl << getOne<unsigned>() << std::endl << getOne<unsigned char>() << std::endl << getOne<unsigned long>() << std::endl << getOne<unsigned short>() << std::endl; }
何故か。C++ではコンパイル時に其々の型の函數の實體が、型變數Nをchar, double, float, int, ...に置き換へた函數の實體が作られるから、Nへのキャスト(N) 1
はint等へのキャスト(int) 1
に書き換へられてコンパイルされる。此れは普通のコードだ。
C#のジェネリクスは此う云ふものではない。型變數Nの情報は実行時にも取得できる。C++の樣に普通のコードに変換されてコンパイルされるのではなく、ジェネリクスそのものとしてコンパイルされる。
よく思ひ出せばC#にもコードを生成する機能は在った。わたしはUnity3Dを使ってゐたので新しい機能は無いが、式木Expression Treeは在った。此れを使ふ。
using System; using System.Linq.Expressions; public class ConstNumber { public static void Main() { Console.WriteLine(GetOne<int>()); } private static N GetOne<N>() { return Expression.Lambda<Func<N>>( Expression.ConvertChecked( Expression.Constant(1, typeof(int)), typeof(N) ) ).Compile()(); } }
whereガードでNに参照型が渡されるのを禁止できる。
using System; using System.Linq.Expressions; public class ConstNumber { public static void Main() { Console.WriteLine(GetOne<int>()); } private static N GetOne<N>() where N : struct { return Expression.Lambda<Func<N>>( Expression.ConvertChecked( Expression.Constant(1, typeof(int)), typeof(N) ) ).Compile()(); } }
intの1を作り、N型にキャストする。bool等を指定してキャストに失敗したらSystem.InvalidOperationException実行時エラーを吐く。式木は実行時に展開される。コンパイラは、兔に角此のコードがN型を返す事はわかるので、通す。実行時に1をintやfloatにキャストする普通のコードに展開され、実行される。
一部の數値型に限定したい等有れば、型變數Nをチェックするコードを書ける。
using System; using System.Linq; using System.Linq.Expressions; public class ConstNumber { public static void Main() { Console.WriteLine(GetOne<byte>()); Console.WriteLine(GetOne<char>()); Console.WriteLine(GetOne<decimal>()); Console.WriteLine(GetOne<double>()); Console.WriteLine(GetOne<short>()); Console.WriteLine(GetOne<int>()); Console.WriteLine(GetOne<long>()); Console.WriteLine(GetOne<ushort>()); Console.WriteLine(GetOne<uint>()); Console.WriteLine(GetOne<ulong>()); Console.WriteLine(GetOne<sbyte>()); Console.WriteLine(GetOne<float>()); } private static N GetOne<N>() where N : struct { GuardWithNumericType(typeof(N)); return Expression.Lambda<Func<N>>( Expression.ConvertChecked( Expression.Constant(1, typeof(int)), typeof(N) ) ).Compile()(); } private static void GuardWithNumericType(Type t) { var supportedTypes = new TypeCode[] { TypeCode.Byte, TypeCode.Char, TypeCode.Decimal, TypeCode.Double, TypeCode.Int16, TypeCode.Int32, TypeCode.Int64, TypeCode.UInt16, TypeCode.UInt32, TypeCode.UInt64, TypeCode.SByte, TypeCode.Single, }; if (!supportedTypes.Contains(Type.GetTypeCode(t))) { throw new NotImplementedException(); } } }
後は何でも。