Dlaczego C# nie wywnioskuje moich typów generycznych?

Mam dużo Funcy fun (Zabawa zamierzona) z generycznymi metodami. W większości przypadków wnioskowanie Typu C# jest wystarczająco inteligentne, aby dowiedzieć się, jakie ogólne argumenty musi użyć w moich metodach generycznych, ale teraz mam projekt, w którym kompilator C# nie odnosi sukcesu, podczas gdy wierzę, że mógł odnieść sukces w znalezieniu właściwych typów.

Czy ktoś może mi powiedzieć, czy kompilator jest trochę głupi w tym przypadku, czy jest bardzo jasny powód, dla którego nie może wywnioskować mojego generycznego kłótnie?

Oto kod:

Klasy i definicje interfejsu:

interface IQuery<TResult> { }

interface IQueryProcessor
{
    TResult Process<TQuery, TResult>(TQuery query)
        where TQuery : IQuery<TResult>;
}

class SomeQuery : IQuery<string>
{
}

Jakiś kod, który się nie kompiluje:

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Does not compile :-(
        p.Process(query);

        // Must explicitly write all arguments
        p.Process<SomeQuery, string>(query);
    }
}

Dlaczego tak jest? Co mi umyka?

Oto komunikat o błędzie kompilatora (nie pozostawia wiele naszej wyobraźni):

Argumenty typu dla metody IQueryProcessor.Proces(TQuery) nie może być wywnioskowany z użycia. Spróbuj określić wpisz argumenty jawnie.

The reason I believe C# powinno być w stanie wywnioskować, że jest to spowodowane:

  1. dostarczam obiekt, który implementuje IQuery<TResult>.
  2. , że tylko IQuery<TResult> wersja, którą implementuje Typ, to IQuery<string>, a zatem TResult musi być string.
  3. z tymi informacjami kompilator ma TResult i TQuery.

Rozwiązanie

Dla mnie najlepszym rozwiązaniem była zmiana interfejsu IQueryProcessor i zastosowanie dynamicznego pisania w implementacji:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

// Implementation
sealed class QueryProcessor : IQueryProcessor {
    private readonly Container container;

    public QueryProcessor(Container container) {
        this.container = container;
    }

    public TResult Process<TResult>(IQuery<TResult> query) {
        var handlerType =
            typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
        dynamic handler = container.GetInstance(handlerType);
        return handler.Handle((dynamic)query);
    }
}

Interfejs IQueryProcessor zajmuje teraz w parametrze IQuery<TResult>. W ten sposób może zwrócić TResult i to rozwiąże problemy z perspektywy konsumenta. Musimy użyć refleksji w implementacji, aby uzyskać rzeczywistą implementację, ponieważ konkretne typy zapytań są potrzebne (w moim przypadku). Ale tutaj przychodzi dynamiczne pisanie na ratunek, które zrobi nam odbicie. Możesz przeczytać więcej na ten temat w tym artykuł .

Author: Steven, 2011-12-14

7 answers

Grupa ludzi zauważyła, że C# nie tworzy wniosków opartych na ograniczeniach. To jest poprawne i istotne dla pytania. Wnioskowania dokonuje się poprzez zbadanie argumentów i odpowiadających im formalnych typów parametrów i jest to jedyne źródło informacji wnioskowania.

Kilka osób połączyło się z tym Artykuł:

Https://docs.microsoft.com/en-us/archive/blogs/ericlippert/c-3-0-return-type-inference-does-not-work-on-method-groups

Ten artykuł jest zarówno Nieaktualny, jak i nieistotny dla pytania. Jest nieaktualny, ponieważ opisuje decyzję projektową podjętą w C # 3.0, którą następnie odwróciliśmy w C# 4.0, głównie w oparciu o odpowiedź na ten artykuł. Właśnie dodałem aktualizację w tym celu do artykułu.

Nie ma znaczenia, ponieważ artykuł jest o zwraca wnioskowanie typu z argumentów grupy metod do ogólnych parametrów formalnych delegata . Nie o taką sytuację pyta oryginalny plakat.

Odpowiedni artykuł do przeczytania to raczej ten:

Https://docs.microsoft.com/en-us/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature

UPDATE: słyszałem, że C # 7.3 nieco zmienił zasady stosowania ograniczeń, dzięki czemu powyższy artykuł już nie. Kiedy będę miał czas, przejrzę zmiany, które wprowadzili moi byli koledzy i zobaczę, czy warto zamieszczać poprawki na moim nowym blogu; do tego czasu zachowaj ostrożność i zobacz, co C # 7.3 robi w praktyce.

 62
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-25 15:31:57

C# nie wywnioskuje typów generycznych na podstawie typu zwracanego przez metodę generyczną, tylko argumenty do metody.

Nie używa również ograniczeń jako części wnioskowania typu, co eliminuje ogólne Ograniczenie z dostarczania typu dla Ciebie.

Po szczegóły zobacz Post Erica Lipperta na ten temat .

 15
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-14 20:57:41

Nie używa ograniczeń do wnioskowania typów. Raczej wnioskuje typy (jeśli to możliwe) , a następnie sprawdza ograniczenia.

Dlatego, chociaż jedyny możliwy TResult, który może być użyty z parametrem SomeQuery, nie zobaczy tego.

Zauważ również, że możliwe byłoby również zaimplementowanie SomeQuery IQuery<int>, co jest jednym z powodów, dla których jest to ograniczenie dla kompilatora, może nie być złym pomysłem.

 11
Author: Jon Hanna,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-14 20:25:21

Spec jasno to określa:

Sekcja 7.4.2 Wnioskowanie Typu

Jeżeli podana liczba argumentów jest inna niż liczba parametrów w metodzie, wtedy wnioskowanie natychmiast się nie powiedzie. w przeciwnym razie Załóżmy, że metoda generyczna ma następującą sygnaturę:

Tr M(T1 x1 ... Tm xm)

Z wywołaniem metody w postaci m(E1 ...Em) zadaniem wnioskowania typu jest znalezienie unikalnych argumentów typu S1...Sn dla każdego z wpisz parametry X1 ... Xn tak, aby wywołanie M(E1...Em) stało się poprawne.

Jak widać, Typ powrotu nie jest używany do wnioskowania typu. Jeśli wywołanie metody nie mapuje bezpośrednio do argumentów typu wnioskowanie natychmiast się nie powiedzie.

Kompilator nie tylko zakłada, że chcesz string jako argument TResult, ani nie może. Wyobraź sobie TResult pochodzące od string. Oba byłyby ważne, więc który wybrać? Lepiej być wyraźnym.

 5
Author: Ed S.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-14 20:25:22

Dlaczego zostało dobrze wyjaśnione, ale istnieje alternatywne rozwiązanie. Często jednak mam do czynienia z tymi samymi problemami dynamic lub jakiekolwiek rozwiązanie wykorzystujące odbicie lub alokację danych nie wchodzi w grę w moim przypadku (radość z gier wideo...)

Więc zamiast tego przekazuję return jako out parametry, które są następnie poprawnie wywnioskowane.

interface IQueryProcessor
{
     void Process<TQuery, TResult>(TQuery query, out TResult result)
         where TQuery : IQuery<TResult>;
}

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Instead of
        // string result = p.Process<SomeQuery, string>(query);

        // You write
        string result;
        p.Process(query, out result);
    }
}

Jedyną wadą, o której myślę, jest to, że zakazuje używania 'var'.

 3
Author: Baptiste Dupy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-05 11:44:14

Nie będę znowu wdawał się w "Dlaczego", nie mam złudzeń, że jestem w stanie zrobić lepsze wyjaśnienie niż Eric Lippert.

Istnieje jednak rozwiązanie, które nie wymaga późnego wiązania lub dodatkowych parametrów do wywołania metody. Nie jest to jednak zbyt intuicyjne, więc pozostawiam czytelnikowi decyzję, czy jest to poprawa.

Po pierwsze, zmodyfikuj IQuery, aby było samodzielne:

public interface IQuery<TQuery, TResult> where TQuery: IQuery<TQuery, TResult>
{
}

Twoje IQueryProcessor wyglądałoby tak:

public interface IQueryProcessor
{
    Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
        where TQuery: IQuery<TQuery, TResult>;
}

Rzeczywiste zapytanie "type": "content"]}

public class MyQuery: IQuery<MyQuery, MyResult>
{
    // Neccessary query parameters
}

Implementacja procesora może wyglądać następująco:

public Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
    where TQuery: IQuery<TQuery, TResult>
{
    var handler = serviceProvider.Resolve<QueryHandler<TQuery, TResult>>();
    // etc.
}
 3
Author: Thorarin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-02-21 13:28:59

Innym obejściem tego problemu jest dodanie dodatkowego parametru dla typu resolution.To unikaj zmian w istniejącej bazie kodowej taki parametr można dodać do metody rozszerzenia. Na przykład można dodać następującą metodę rozszerzenia:

static class QueryProcessorExtension
{
    public static TResult Process<TQuery, TResult>(
        this IQueryProcessor processor, TQuery query,
        //Additional parameter for TQuery -> IQuery<TResult> type resolution:
        Func<TQuery, IQuery<TResult>> typeResolver)
        where TQuery : IQuery<TResult>
    {
        return processor.Process<TQuery, TResult>(query);
    }
}

Teraz możemy użyć tego rozszerzenia w następujący sposób:

void Test(IQueryProcessor p)
{
    var query = new SomeQuery();

    //You can now call it like this:
    p.Process(query, x => x);
    //Instead of
    p.Process<SomeQuery, string>(query);
}

Co jest dalekie od ideału, ale znacznie lepsze niż jawne podawanie typów.

P. S. Odnośniki do tego numeru w dotnet repozytorium:

Https://github.com/dotnet/csharplang/issues/997

Https://github.com/dotnet/roslyn/pull/7850

 2
Author: Roman Artiukhin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-08 11:50:16