Это похоже на этот вопрос Вызов перегруженного универсального метода из универсального метода, но это не то же самое: в этом случае метод вернул void, а мне нужно вернуть общий тип T, поэтому предлагаемое решение не работает.

Я реализую кортежный класс, который имеет общие параметры и методы доступа для этих параметров:

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));
    }
}

Я хотел бы использовать этот класс так:

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

Но если я попробую это сделать, всегда вызывается Generic T GetItem<T>(T x) (генерируется исключение), а не неуниверсальные реализации.

Если я попытаюсь получить доступ к методу GetItem напрямую, он будет работать как положено:

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

q и w содержат 1 и hello, как и ожидалось.

Могу ли я использовать этот класс по своему усмотрению (используя obj.Get<T>)? Я хочу избежать геттеров Item1 Item2 Item3.

1
fnzr 17 Сен 2021 в 15:43

3 ответа

Лучший ответ

Но если я попробую это сделать, всегда вызывается Generic T GetItem<T>(T x) (генерируется исключение), а не неуниверсальные реализации.

Это потому, что разрешение перегрузки происходит во время компиляции, а во время компиляции конкретный тип T неизвестен.

Могу ли я использовать этот класс по своему усмотрению (используя obj.Get<T>)?

Используя простую старую проверку динамического типа, убедитесь:

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();
    }
}

Преимущество этого решения состоит в том, что поведение крайних случаев (T1 == T2 или T не соответствует ни T1, ни T2) правильно определено и легко просматривается.


В качестве альтернативы вы можете использовать «хак» dynamic, использованный в вопросе, на который вы ссылаетесь, вам просто нужно преобразовать результат в T: return (T)GetItem((dynamic)default(T));, вот рабочую скрипку.

Но это просто уродливо, пожалуйста, не делай этого. Сравните скрипку с версией, приведенной выше, и попытайтесь честно судить, какой из них легче читать, легче понимать и, следовательно, легче поддерживать.

2
Heinzi 17 Сен 2021 в 13:15

Тесты

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>());

Реализация

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;
}

В этой версии используется отражение. Если вам нужна более высокая производительность, вам следует использовать скомпилированные выражения, но их сложнее поддерживать.

1
Dbl 17 Сен 2021 в 13:00

Что, если бы у вас был один и тот же тип данных для двух элементов?

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

Что должен здесь вернуть GetItem? Очевидно, на этот вопрос нет правильного ответа.

В любом случае предоставление параметра только для того, чтобы различать, какой метод вызывать, довольно странно. На самом деле ваш GetItem<T> - метод вообще не должен быть универсальным. Просто используйте два разных метода:

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

Или используйте автоматические свойства, чтобы избавиться от полей поддержки:

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

    public MyClass(T1 item1, T2 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }
0
HimBromBeere 17 Сен 2021 в 13:01