Метод Generi c вызывает другой generi c с конкретной перегрузкой с параметром out: конкретная версия никогда не используется - PullRequest
2 голосов
/ 11 июля 2020

Здесь, на SO, есть несколько похожих вопросов о методах generi c с конкретными перегрузками, и большинство из них говорят по существу то же самое: Generi * Разрешение перегрузки 1084 * выполняется во время компиляции, поэтому, если вы хотите использовать конкретную перегрузку, вам может потребоваться использовать Dynami c, чтобы среда выполнения решила, какую перегрузку использовать. ОК, круто - я это понимаю. Ни один из найденных мной вопросов не касается параметра out, и мне интересно, есть ли лучший способ справиться с этим, чем то, что я сделал. Начнем с самого простого случая c (весь мой код был написан и протестирован в LinqPad):

void Main()
{
    string x = "";
    var g = new GenericTest();
    g.RunTest(x);           //Ran from TryFoo<T>
    g.RunTestWithDynamic(x);//Ran from TryFoo(string)
    g.Foo(x);               //Ran from TryFoo(string)
}

public class GenericTest
{
    //public void RunTest(string withInput) => Foo(withInput); <-- This would fix it
    public void RunTest<T>(T withInput) => Foo(withInput);
    public void RunTestWithDynamic<T>(T withInput) => Foo((dynamic)withInput);
    public void Foo<T>(T withInput) => Console.WriteLine("Ran from TryFoo<T>");
    public void Foo(string withInput) => Console.WriteLine("Ran from TryFoo(string)");
}

Вот некоторые моменты, на которые следует обратить внимание:

  1. RunTest - это общий метод c, который вызывает другой общий метод c, Foo. Когда я вызываю RunTest, кажется, что компилятор не следует полностью от сайта вызова, чтобы увидеть, что g.RunTeset проходит в x, что является string, и связывает все это так, чтобы Foo(string) вызывается перегрузка; вместо этого он просто видит, что RunTest<T> вызывает Foo. Он не создает разные «пути» на основе разных входных значений T. Хорошо, это честно и понятно.
  2. Если я вызываю Foo непосредственно из моего Main метода, компилятор достаточно умен, чтобы видеть, что мы вызываем Foo с string напрямую и правильно выбираем конкретная перегрузка.
  3. Как описано в связанных сообщениях SO, я могу вызвать RunTestWithDynamic, который изменит, какая перегрузка используется во время выполнения, в зависимости от значения. Это кажется немного "хакерским", но я хорошо разбираюсь в этом решении.
  4. Я закомментировал строку: конкретная перегрузка RunTest. По сути, это будет то же самое, что и прямой вызов Foo. Если бы это не было прокомментировано, это бы все исправило. Увы, в моем случае это не вариант.

А что, если T соответствует параметру out? Рассмотрим шаблон, используемый, скажем, int.TryParse, где вы возвращаете bool, чтобы указать, удалось это или нет, но на самом деле вам нужно значение out. Теперь вы не можете действительно сделать динамическое c разрешение, потому что вы не можете использовать выходной параметр. Я подумал о том, чтобы сделать что-то, где я сделаю default(T), а затем приведу его к динамическому c, но если это когда-нибудь будет хорошо работать где-либо еще, существует проблема ссылочных типов, которые по умолчанию имеют значение null. Нет, это тоже не работает.

В конце концов, лучшее, что я смог придумать, было следующее:

void Main()
{
    string x;
    var g = new GenericTest();
    g.TryRunTest(out x);            //Ran from TryFoo<T>
    g.TryRunTestWithDynamic(out x); //Ran from TryFoo(string)
    g.TryFoo(out x);                //Ran from TryFoo(string)
}

public class GenericTest
{
    //This would fix it, but in my case, not an option
    //public bool TryRunTest(out string withOutput) => return TryFoo(out withOutput);
    
    public bool TryRunTest<T>(out T withOutput)
    {
        return TryFoo(out withOutput);
    }
    public bool TryRunTestWithDynamic<T>(out T withOutput)
    {
        if(typeof(T) == typeof(string))
        {
            var retval = TryFoo(out string s);
            withOutput = (T)(dynamic)s;
            return retval;
        }
        
        return TryFoo(out withOutput);
    }
    public bool TryFoo<T>(out T withOutput)
    {
        withOutput = default(T);
        Console.WriteLine("Ran from TryFoo<T>");
        return true;
    }
    public bool TryFoo(out string withOutput)
    {
        withOutput = "Strings are special";
        Console.WriteLine("Ran from TryFoo(string)");
        return true;
    }
}

Вы видите, что TryRunTestWithDynamic нужно искать конкретный тип string. Я не могу придумать способ сделать это, чтобы я мог просто добавить перегрузки, а затем использовать разрешение Dynami c, чтобы выбрать перегрузку, которую я хочу, без необходимости все это описывать (и давайте посмотрим правде в глаза - все это изложим в первую очередь убивает весь смысл наличия перегрузок).

В своем сообщении, на которое я ссылался выше (и здесь для хорошей меры), Джон Скит упоминает использование MethodInfo.MakeGenericMethod в качестве альтернатива. Вот он рассказывает о том, как им пользоваться. Мне любопытно, поможет ли это мне здесь, но я не могу понять, как использовать его с параметрами out.

Итак, вот мои конкретные c вопросы:

  1. Хотя да, это ДЕЙСТВИТЕЛЬНО работает, но ОЧЕНЬ неуклюже. Есть лучший способ сделать это? Самое главное, есть ли способ потреблять перегрузки, при которых мне не нужно было бы проверять каждый тип отдельно?
  2. Есть ли способ использовать маршрут MethodInfo.MakeGenericMethod с параметрами out, и поможет ли это мне?
  3. Допустим, мы могли взмахнуть волшебной c палочкой, и внезапно в C# была добавлена ​​языковая функция, которая дала бы нам возможность принудительно устанавливать разрешение c динамического c для общих *1096* методов. (либо для всех случаев, либо только для случая, когда параметр generi c предназначен для out, как моя основная проблема). Взяв из приведенных выше примеров, предположим, что у нас было что-то вроде этого:
//The following are all invalid syntax but illustrate possible
// ways we may express that we want to force dynamic resolution

//For out parameters
public void TryFoo<T>(dynamic out T withOutput){...}

//"dynamic out" could be a special combination of keywords so that this only works
// with output variables.  Not sure if that constraint buys us anything.
// Maybe we would want to allow it for regular (not out) generic parameters as well
public void Foo<T>(dynamic T withInput){...}
public void Foo<dynamic T>(T withInput){...}
public void dynamic Foo<T>(T withInput){...}

Давайте не будем сосредотачиваться на самом синтаксисе; мы могли бы легко использовать другие обозначения - дело не в этом. Идея состоит в том, что мы сообщаем среде выполнения, что когда мы дойдем до TryFoo<T>..., остановимся и проверим, есть ли другая, более подходящая перегрузка, на которую мы должны переключиться. Я также не особо разбираюсь в том, проверяем ли мы лучшее разрешение конкретно для T или применимо ли это к методу в целом.

Есть ли причина, по которой это может быть ПЛОХО? Как он часто это делает, Эри c Липперт делает несколько интересных моментов о generi c и конкретном разрешении перегрузки, когда задействовано наследование (Джон Скит также цитируется в других ответах) и обсуждает, почему выбор самая конкретная c перегрузка - не всегда лучший выбор. Мне интересно, может ли такая языковая конструкция вызвать аналогичные проблемы.

1 Ответ

0 голосов
/ 13 июля 2020

Хорошо, поэтому я предложил идею добавления поддержки для включения динамической c привязки в C# GitHub и получил это альтернативное решение, которое мне НАМНОГО больше нравится, чем то, что я делаю сейчас: ((dynamic)this).TryFoo(out withOutput);

Это будет выглядеть так:

public bool TryRunTestWithDynamic<T>(out T withOutput)
{
    return ((dynamic)this).TryFoo(out withOutput);
}

Я повторю предупреждение, которое мне было дано: это работает только для методов экземпляра. В моем случае это нормально, но если бы у кого-то был метод stati c, это не помогло бы, поэтому вторая часть моего вопроса (относительно MethodInfo.MakeGenericMethod) все еще может быть актуальной.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...