LoginSignup
1
0

More than 3 years have passed since last update.

[C#]struct制約のないジェネリック型の値から、Nullable<T>.Valueにアクセスする

Last updated at Posted at 2020-07-09

ジェネリック型を使用し、struct制約があればこんなコードですが、

void Func<T>(T? val) where T :struct
{
    Console.WriteLine(val.Value.GetHashCode());
}

long? value = long.MaxValue;
Func(value);

これを制約なしでアクセスしたいわけです。

void Func<T>(T val)
{
    Console.WriteLine(val.Value.GetHashCode());
                          ^^^^^ //アクセスできない
}

long? value = long.MaxValue;
Func(value);

実行時にNullable.GetUnderlyingType(typeof(T));System.Typeを取得してリフレクションでも取得できますが、もう少し効率の良さそうな方法を取りたいので、Expression Treeを使います。

Expression Treeを使用したサンプルコード

using System;
using static System.Linq.Expressions.Expression;

static class InnerValueHashCode
{
    public static int Get<T>(T value) => Impl<T>.Delegate(value);
    private static class Impl<T>
    {
        public static Func<T, int> GenerateFunc()
        {
            var nullableValue = Parameter(typeof(T), "nullableValue");

            var underlyingType = Nullable.GetUnderlyingType(typeof(T));

            var callExpression = Call(
                Property(nullableValue, typeof(T), "Value"),
                underlyingType.GetMethod("GetHashCode", Type.EmptyTypes));
            var lambda = Lambda<Func<T, int>>(callExpression, nullableValue);
            return lambda.Compile();
        }

        internal readonly static Func<T, int> Delegate = GenerateFunc();
    }
}
 long? value = long.MaxValue;
InnerValueHashCode.Get(value);

もちろん Nullable<T>以外を渡すと正しく動作しないので、事前にジェネリック型のチェックは必要です。

また、HasValue == trueの場合、Nullable.ToString()Nullable.GetHashCode()Valueの値を返すので、サンプルコードそのままでは実用性は皆無です。

更にいうと、dynamicを使えば瞬殺です。

追記

もう少し実用的なサンプルコードを追記します。

ジェネリック型を受け取るFormatableToStringメソッドが、T.ToString(string)もしくはT.Value.ToString(string)を呼び出すようにします。
ToString(string)が無ければTypeInitializationExceptionを投げます。

呼び出し元
class Program
{
    static void Main(string[] args)
    {
        FormatableToString(99999U, "X");
        FormatableToString((uint?)99999U, "X");
        FormatableToString(IntPtr.Zero, "X");
        FormatableToString(UIntPtr.Zero, "X"); // throws System.TypeInitializationException
    }

    static void FormatableToString<T>(T value, string format)
    {
        string result;
        if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            result = NullableHelper.ToString(value, format);
        }
        else
        {
            result = CallHelper.ToString(value, format);
        }

        Console.WriteLine(result);
    }
}

NullableHelperCallHelperの実装コードは長いので折りたたみ
using System;
using System.Runtime.CompilerServices;
using static System.Linq.Expressions.Expression;

static class CallHelper
{
    public static string ToString<T>(T value) => Impl<T>.ToStringDelegate(value);
    public static string ToString<T>(T value, string format) => Impl<T>.ToStringWithFormatDelegate(value, format);

    private static class Impl<T>
    {
        public static Func<T, TResult> MakeCallFunc<TResult>(string name) => MakeCallFunc<TResult>(name, Type.EmptyTypes);

        public static Func<T, TResult> MakeCallFunc<TResult>(string name, params Type[] argTypes)
        {
            var value = Parameter(typeof(T), "value");

            var targetMethod = typeof(T).GetMethod(name, argTypes);
            if (targetMethod == null)
                throw new MissingMethodException(typeof(T).FullName, name);

            var lambda = Lambda<Func<T, TResult>>(
                Call(value, targetMethod),
                    value);
            return lambda.Compile(true);
        }

        public static Func<T, TArg, TResult> MakeCallFunc<TArg, TResult>(string name)
        {
            var value = Parameter(typeof(T), "value");
            var arg1 = Parameter(typeof(TArg), "arg1");

            var argTypes = new[] { typeof(TArg) };
            var targetMethod = typeof(T).GetMethod(name, argTypes);
            if (targetMethod == null)
                throw new MissingMethodException(typeof(T).FullName, $"{name}({string.Join(",", argTypes.AsEnumerable())})");

            var lambda = Lambda<Func<T, TArg, TResult>>(
                Call(value, targetMethod, arg1),
                    value, arg1);
            return lambda.Compile(true);
        }

        internal readonly static Func<T, string> ToStringDelegate = MakeCallFunc<string>("ToString");

        // T must has ToString(format) method.
        internal readonly static Func<T, string, string> ToStringWithFormatDelegate = MakeCallFunc<string, string>("ToString");
    }
}

static class NullableHelper
{
    public static string ToString<T>(T value) => Impl<T>.ToStringDelegate(value);
    public static string ToString<T>(T value, string format) => Impl<T>.ToStringWithFormatDelegate(value, format);

    private static class Impl<T>
    {
        static Impl()
        {
            System.Diagnostics.Debug.Assert(typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>));
        }

        public static Func<T, TResult> MakeCallFunc<TResult>(string name) => MakeCallFunc<TResult>(name, Type.EmptyTypes);

        public static Func<T, TResult> MakeCallFunc<TResult>(string name, params Type[] argTypes)
        {
            var nullableValue = Parameter(typeof(T), "nullableValue");

            var underlyingType = Nullable.GetUnderlyingType(typeof(T));
            var targetMethod = underlyingType.GetMethod(name, argTypes);
            if (targetMethod == null)
                throw new MissingMethodException(underlyingType.FullName, name);

            var lambda = Lambda<Func<T, TResult>>(
                Call(
                    Property(nullableValue, typeof(T), "Value"), targetMethod),
                    nullableValue);
            return lambda.Compile(true);
        }

        public static Func<T, TArg, TResult> MakeCallFunc<TArg, TResult>(string name)
        {
            var nullableValue = Parameter(typeof(T), "nullableValue");
            var arg1 = Parameter(typeof(TArg), "arg1");

            var underlyingType = Nullable.GetUnderlyingType(typeof(T));
            var argTypes = new[] { typeof(TArg) };
            var targetMethod = underlyingType.GetMethod(name, argTypes);
            if (targetMethod == null)
                throw new MissingMethodException(underlyingType.FullName, $"{name}({string.Join(",", argTypes.AsEnumerable())})");

            var lambda = Lambda<Func<T, TArg, TResult>>(
                Call(
                    Property(nullableValue, typeof(T), "Value"), targetMethod, arg1),
                nullableValue, arg1);
            return lambda.Compile(true);
        }

        internal readonly static Func<T, string> ToStringDelegate = MakeCallFunc<string>("ToString");

        // T must has ToString(format) method.
        internal readonly static Func<T, string, string> ToStringWithFormatDelegate = MakeCallFunc<string, string>("ToString");
    }
}

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0