Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Pattern Matching using Templated Type

How can I rewrite this method using a pattern-matching switch?

internal ResultType Fetch<ResultType>(string field)
{
    var type = typeof(ResultType);
    object xx = type switch
    {
        _ when type == typeof(DateTime) => FetchDateTime(field),
        _ when type == typeof(DateOnly) => FetchDateOnly(field),
        _ when type == typeof(bool) => FetchBool(field),
        _ when type == typeof(decimal) => FetchDecimal(field),
        _ when type == typeof(string) => FetchString(field),
        _ when type == typeof(int) => FetchInt(field),

        _ => throw new NotImplementedException($"this[field: {field}]")
    };

    if (xx is ResultType rt) return rt;
    return default(ResultType)!;
}
like image 461
Ram Avatar asked May 21 '26 16:05

Ram


2 Answers

Posting as an answer because the code is too long for a comment.

You can certainly make this more efficient (ignoring the length aspect). The JIT recognises the pattern if (type == typeof(DateTime)) etc, and will emit code which keeps just the relevant parts of the method for a given ResultType.

For instance, your code results in the following when ResultType is DateTime:

C.Fetch[[System.DateTime, System.Private.CoreLib]](System.String)
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: push esi
    L0004: sub esp, 8
    L0007: mov esi, edx
    L0009: mov ecx, 0x6e099a4
    L000e: call 0x060a300c
    L0013: lea edx, [eax+4]
    L0016: xor ecx, ecx
    L0018: mov [edx], ecx
    L001a: mov [edx+4], ecx
    L001d: mov edx, eax
    L001f: test edx, edx
    L0021: je short L002b
    L0023: cmp dword ptr [edx], 0x6e099a4
    L0029: je short L003a
    L002b: xor eax, eax
    L002d: mov [esi], eax
    L002f: mov [esi+4], eax
    L0032: lea esp, [ebp-4]
    L0035: pop esi
    L0036: pop ebp
    L0037: ret 4
    L003a: add eax, 4
    L003d: mov edx, [eax]
    L003f: mov eax, [eax+4]
    L0042: mov [ebp-0xc], edx
    L0045: mov [ebp-8], eax
    L0048: mov eax, esi
    L004a: mov edx, [ebp-0xc]
    L004d: mov [eax], edx
    L004f: mov edx, [ebp-8]
    L0052: mov [eax+4], edx
    L0055: lea esp, [ebp-4]
    L0058: pop esi
    L0059: pop ebp
    L005a: ret 4

However the code:

internal ResultType Fetch2<ResultType>(string field)
{
    var type = typeof(ResultType);
    if (type == typeof(DateTime))
    {
        return (ResultType)(object)FetchDateTime(field);
    }
    if (type == typeof(DateOnly))
    {
        return (ResultType)(object)FetchDateOnly(field);
    }
    throw new NotImplementedException($"this[field: {field}]");
}

Results in:

C.Fetch2[[System.DateTime, System.Private.CoreLib]](System.String)
    L0000: push ebp
    L0001: mov ebp, esp
    L0003: xor eax, eax
    L0005: mov [edx], eax
    L0007: mov [edx+4], eax
    L000a: pop ebp
    L000b: ret 4

This is the pattern that the runtime uses.

See on SharpLab.

like image 81
canton7 Avatar answered May 24 '26 12:05

canton7


As canton7's answer said, it is more efficient at runtime if you check ResultType against other types using ==, as that is what the JIT looks out for and optimises.

If you don't care about that, you can make your code "look prettier" by using a dictionary:

Dictionary<Type, Func<string, object>> dict = new()
{
    { typeof(DateTime), Wrap(FetchDateTime) },
    { typeof(string), FetchString },
    // and so on
};

Here I am assuming that the FetchXXX methods have the appropriate return type based on their name. Note that since value types don't do covariance, you need an extra Wrap helper to box the return values. Use this for the FetchXXX methods that return value types.

Func<string, object> Wrap<R>(Func<string, R> f) where R: struct 
    => x => f(x);

Then you can use this dictionary like this:

ResultType Fetch<ResultType>(string field)
{
    if (dict.TryGetValue(typeof(ResultType), out var f))
    {
        var result = f(field);
        return result is ResultType r ? r : default;
    } 
    else
    {
        throw new NotImplementedException($"this[field: {field}]");
    }
}

As a side note, if all the types that you are checking against are value types, you can actually use a pattern-matching switch, though this is very hacky. You would switch on default(ResultType).

ResultType Fetch<ResultType>(string field) where ResultType: struct
{
    object xx = default(ResultType) switch
    {
        DateTime => FetchDateTime(field),
        DateOnly => FetchDateOnly(field),
        bool => FetchBool(field),
        decimal => FetchDecimal(field),
        int => FetchInt(field),
        _ => throw new NotImplementedException($"this[field: {field}]")
    };

    if (xx is ResultType rt) return rt;
    return default(ResultType)!;
}
like image 38
Sweeper Avatar answered May 24 '26 13:05

Sweeper



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!