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


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

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

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

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

```csharp
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を使用したサンプルコード

```csharp
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`を投げます。

```csharp:呼び出し元
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);
    }
}
```

<details><summary>`NullableHelper`と`CallHelper`の実装コードは長いので折りたたみ</summary><div>

```csharp
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");
    }
}
```

</div></details>