Home > Enterprise >  Calling non-generic method from generic method in C#
Calling non-generic method from generic method in C#

Time:09-22

This is similar to this question Call overloaded generic method from generic method but it's not the same: in that case, the method returned void, while I need to return the generic type T, so the proposed solution does not work.

I'm implementing a tuple-like class which has generic parameters and accessors for these parameters:

public class MyClass<T1, T2>
{
    private readonly T1 _item1;
    private readonly T2 _item2;

    public MyClass(T1 item1, T2 item2)
    {
        _item1 = item1;
        _item2 = item2;
    }

    public T1 GetItem(T1 _) => _item1;

    public T2 GetItem(T2 _) => _item2;

    private T GetItem<T>(T x)
    {
        throw new ArgumentException();
        // return x;
    }

    public T Get<T>()
    {
        return GetItem(default(T));
    }
}

I would like to use this class like this:

var obj = new MyClass<int, string>(1, "hello");
var a = obj.Get<int>();
var b = obj.Get<string>();

But if I try this, the Generic T GetItem<T>(T x) is always called (the exception is thrown), and not the non-generic implementations.

If I try to access the GetItem method directly, it works as intended:

var obj = new MyClass<int, string>(1, "hello");
var q = obj.GetItem(default(int));
var w = obj.GetItem(default(string));

q and w contain 1 and hello as expected.

Is there a way for me to use this class as I want (using obj.Get<T>)? I want to avoid Item1 Item2 Item3 getters.

CodePudding user response:

But if I try this, the Generic T GetItem<T>(T x) is always called (the exception is thrown), and not the non-generic implementations.

That's because overload resolution happens at compile-time, and at compile-time, the concrete type of T is unknown.

Is there a way for me to use this class as I want (using obj.Get<T>)?

Using plain old dynamic type checking, sure:

public class MyClass<T1, T2>
{
    private readonly T1 _item1;
    private readonly T2 _item2;

    public MyClass(T1 item1, T2 item2)
    {
        _item1 = item1;
        _item2 = item2;
    }

    public T Get<T>()
    {
        if (typeof(T) == typeof(T1))
        {
            return (T)(object)_item1;
        } 
        
        if (typeof(T) == typeof(T2))
        {
            return (T)(object)_item2;
        }
        
        throw new InvalidOperationException();
    }
}

The advantage of this solution is that the behavior of the edge cases (T1 == T2, or T matches neither T1 nor T2) is properly defined and easy to see.


Alternatively, you can use the dynamic "hack" used in the question you linked to, you just need to cast the result to T: return (T)GetItem((dynamic)default(T));, here's a working fiddle.

But this is just plain ugly, please don't do that. Compare the fiddle with the version above and try to judge honestly which one is easier to read, easier to understand, and, thus, easier to maintain.

CodePudding user response:

Tests

var instance = new MyClass<string, int>("hello", 1);
Console.WriteLine(instance.GetItem("hi"));
Console.WriteLine(instance.GetItem(2));
// returns "hello"
Console.WriteLine(instance.Get<string>());
// returns "1"
Console.WriteLine(instance.Get<int>());
// throws with details
Console.WriteLine(instance.Get<object>());

Implementation

public T Get<T>()
{
    var parentType = typeof(MyClass<T1, T2>);
    var method = parentType.GetMethod(nameof(GetItem), new[] { typeof(T) });
    if (method == null)
        throw new NotImplementedException(
            $"No implementation for {nameof(GetItem)} with parameter type {typeof(T).FullName}");

    var result = (T)method.Invoke(this, new object[] { default(T) });
    return result;
}

This version uses reflection. If you need more performance you should go for compiled expressions, but that is harder to maintain

CodePudding user response:

What if you had the same data-type for the two items?

var obj = new MyClass<int, int>(1, 2);

what should GetItem return here? Obviously there's no valid answer to this.

Anyway providing a parameter only to distinguish which method to call is pretty odd. Actually your GetItem<T>-method shouldn't be generic at all. Just use two different methods:

T1 GetItem1() => _item1;
T2 GetItem2() => _item2;

or use auto-properties to get rid of the backing-fields:

public class MyClass<T1, T2>
{
    public T1 Item1 { get; }
    public T2 Item2 { get; }

    public MyClass(T1 item1, T2 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }
  • Related