Частичный вывод обобщенного типа возможен в C #? - PullRequest
19 голосов
/ 24 мая 2010

Я работаю над переписыванием моего свободного интерфейса для своей библиотеки классов IoC, и когда я реорганизовал некоторый код, чтобы разделить некоторые общие функции через базовый класс, я наткнулся на загадку.

Примечание : Это то, что я хочу сделать, а не то, что я должен сделать . Если мне придется обойтись с другим синтаксисом, я это сделаю, но если у кого-то есть идея, как заставить мой код компилироваться так, как я хочу, это было бы очень кстати.

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

Лучше с примером кода, чем приведенное выше описание.

Вот простой и полный пример того, что не работает:

using System;

namespace ConsoleApplication16
{
    public class ParameterizedRegistrationBase { }
    public class ConcreteTypeRegistration : ParameterizedRegistrationBase
    {
        public void SomethingConcrete() { }
    }
    public class DelegateRegistration : ParameterizedRegistrationBase
    {
        public void SomethingDelegated() { }
    }

    public static class Extensions
    {
        public static ParameterizedRegistrationBase Parameter<T>(
            this ParameterizedRegistrationBase p, string name, T value)
        {
            return p;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ConcreteTypeRegistration ct = new ConcreteTypeRegistration();
            ct
                .Parameter<int>("age", 20)
                .SomethingConcrete(); // <-- this is not available

            DelegateRegistration del = new DelegateRegistration();
            del
                .Parameter<int>("age", 20)
                .SomethingDelegated(); // <-- neither is this
        }
    }
}

Если вы скомпилируете это, вы получите:

'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingConcrete' and no extension method 'SomethingConcrete'...
'ConsoleApplication16.ParameterizedRegistrationBase' does not contain a definition for 'SomethingDelegated' and no extension method 'SomethingDelegated'...

Я хочу, чтобы метод расширения (Parameter<T>) можно было вызывать как для ConcreteTypeRegistration, так и DelegateRegistration, и в обоих случаях возвращаемый тип должен соответствовать типу, для которого было вызвано расширение.

Проблема заключается в следующем:

Я хотел бы написать:

ct.Parameter<string>("name", "Lasse")
            ^------^
            notice only one generic argument

но также, что Parameter<T> возвращает объект того же типа, к которому он был вызван, что означает:

ct.Parameter<string>("name", "Lasse").SomethingConcrete();
^                                     ^-------+-------^
|                                             |
+---------------------------------------------+
   .SomethingConcrete comes from the object in "ct"
   which in this case is of type ConcreteTypeRegistration

Можно ли каким-то образом обмануть компилятор, чтобы он совершил для меня этот скачок?

Если я добавлю два аргумента универсального типа в метод Parameter, вывод типа заставит меня либо предоставить оба, либо ни одного, что означает следующее:

public static TReg Parameter<TReg, T>(
    this TReg p, string name, T value)
    where TReg : ParameterizedRegistrationBase

дает мне это:

Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments
Using the generic method 'ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)' requires 2 type arguments

Что так же плохо.

Я могу легко реструктурировать классы или даже сделать методы нерасширенными методами, вводя их в иерархию, но мой вопрос заключается в том, можно ли избежать дублирования методов для двух потомков и каким-то образом объявить их только один раз, для базового класса.

Позвольте мне перефразировать это. Есть ли способ изменить классы в первом примере кода выше, чтобы можно было сохранить синтаксис в методе Main без дублирования рассматриваемых методов?

Код должен быть совместим с C # 3.0 и 4.0.


Редактировать : Причина, по которой я не хотел бы оставлять оба аргумента универсального типа для вывода, состоит в том, что для некоторых служб я хочу указать значение параметра для параметра конструктора, который имеет один тип, но передает в значении, которое является потомком. На данный момент сопоставление указанных значений аргумента и правильного конструктора для вызова выполняется с использованием как имени, так и типа аргумента.

Позвольте мне привести пример:

ServiceContainerBuilder.Register<ISomeService>(r => r
    .From(f => f.ConcreteType<FileService>(ct => ct
        .Parameter<Stream>("source", new FileStream(...)))));
                  ^--+---^               ^---+----^
                     |                       |
                     |                       +- has to be a descendant of Stream
                     |
                     +- has to match constructor of FileService

Если я оставлю оба параметра для вывода типа, тип параметра будет FileStream, а не Stream.

Ответы [ 5 ]

13 голосов
/ 05 ноября 2010

Я хотел создать метод расширения, который мог бы перечислять список вещей и возвращать список тех вещей, которые были определенного типа. Это будет выглядеть так:

listOfFruits.ThatAre<Banana>().Where(banana => banana.Peel != Color.Black) ...

К сожалению, это невозможно. Предлагаемая подпись для этого метода расширения выглядела бы так:

public static IEnumerable<TResult> ThatAre<TSource, TResult>
    (this IEnumerable<TSource> source) where TResult : TSource

... и вызов ThatAre <> завершается неудачно, поскольку необходимо указать оба аргумента типа, даже если TSource может быть выведен из использования.

Следуя советам в других ответах, я создал две функции: одну, которая захватывает источник, и другую, которая позволяет вызывающим абонентам выражать результат:

public static ThatAreWrapper<TSource> That<TSource>
    (this IEnumerable<TSource> source)
{
    return new ThatAreWrapper<TSource>(source);
}

public class ThatAreWrapper<TSource>
{
    private readonly IEnumerable<TSource> SourceCollection;
    public ThatAreWrapper(IEnumerable<TSource> source)
    {
        SourceCollection = source;
    }
    public IEnumerable<TResult> Are<TResult>() where TResult : TSource
    {
        foreach (var sourceItem in SourceCollection)
            if (sourceItem is TResult) yield return (TResult)sourceItem;
        }
    }
}

В результате получается следующий код вызова:

listOfFruits.That().Are<Banana>().Where(banana => banana.Peel != Color.Black) ...

... что неплохо.

Обратите внимание, что из-за ограничений общего типа следующий код:

listOfFruits.That().Are<Truck>().Where(truck => truck.Horn.IsBroken) ...

не удастся скомпилировать на этапе Are (), так как грузовики не являются фруктами. Это превосходит предоставленную функцию .OfType <>:

listOfFruits.OfType<Truck>().Where(truck => truck.Horn.IsBroken) ...

Это компилируется, но всегда дает нулевые результаты и действительно не имеет никакого смысла пытаться. Гораздо приятнее позволить компилятору помочь вам определить эти вещи.

12 голосов
/ 24 мая 2010

Если у вас есть только два конкретных типа регистрации (что, по-видимому, относится к вашему вопросу), вы можете просто реализовать два метода расширения:

public static DelegateRegistration Parameter<T>( 
   this DelegateRegistration p, string name, T value); 

public static ConcreteTypeRegistration Parameter<T>( 
   this ConcreteTypeRegistration p, string name, T value); 

Тогда вам не нужно будет указывать аргумент типа, поэтому вывод типа будет работать в упомянутом вами примере. Обратите внимание, что вы можете реализовать оба метода расширения, просто делегировав один общий метод расширения с двумя типами параметров (один в вашем вопросе).


В общем, C # не поддерживает ничего подобного o.Foo<int, ?>(..) для вывода только параметра второго типа (это было бы неплохо - у F # он есть, и это весьма полезно :-)). Вы могли бы, вероятно, реализовать обходной путь, который позволил бы вам написать это (в основном, разделив вызов на два вызова метода, чтобы получить два места, где можно применить определение типа):

FooTrick<int>().Apply(); // where Apply is a generic method

Вот псевдокод для демонстрации структуры:

// in the original object
FooImmediateWrapper<T> FooTrick<T>() { 
  return new FooImmediateWrapper<T> { InvokeOn = this; } 
}
// in the FooImmediateWrapper<T> class
(...) Apply<R>(arguments) { 
  this.InvokeOn.Foo<T, R>(arguments);
}
2 голосов
/ 24 мая 2010

Почему вы не указываете параметры нулевого типа?Оба могут быть выведены в вашем образце.Если это неприемлемое для вас решение, я тоже часто сталкиваюсь с этой проблемой, и нет простого способа решить проблему «вывести только один тип параметра».Так что я пойду с дубликатами методов.

0 голосов
/ 24 мая 2010

Я думаю, вам нужно разделить два параметра типа между двумя различными выражениями;сделайте явный элемент частью типа параметра в методе расширения, чтобы логический вывод мог его забрать.

Предположим, вы объявили класс-оболочку:

public class TypedValue<TValue>
{
    public TypedValue(TValue value)
    {
        Value = value;
    }

    public TValue Value { get; private set; }
}

Тогда ваше расширениеметод как:

public static class Extensions
{
    public static TReg Parameter<TValue, TReg>(
        this TReg p, string name, TypedValue<TValue> value) 
        where TReg : ParameterizedRegistrationBase
    {
        // can get at value.Value
        return p;
    }
}

Плюс более простая перегрузка (выше можно было бы назвать это):

public static class Extensions
{
    public static TReg Parameter<TValue, TReg>(
        this TReg p, string name, TValue value) 
        where TReg : ParameterizedRegistrationBase
    {
        return p;
    }
}

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

ct.Parameter("name", "Lasse")

Но в случае, когда вам нужно явно указать тип, вы можете сделать это:

ct.Parameter("list", new TypedValue<IEnumerable<int>>(new List<int>()))

Выглядит некрасиво, но, надеюсь, реже, чем простое полностью выведенноеkind.

Обратите внимание, что вы могли бы просто иметь перегрузку без оболочки и написать:

ct.Parameter("list", (IEnumerable<int>)(new List<int>()))

Но это, конечно, имеет тот недостаток, что дает сбой во время выполнения, если выполучить что-то не так.К сожалению, сейчас нет моего компилятора C #, поэтому извиняюсь, если это не так.

0 голосов
/ 24 мая 2010

А как насчет следующего:

Используйте определение, которое вы предоставляете: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase

Затем приведите параметр, чтобы механизм логического вывода получил правильный тип:

ServiceContainerBuilder.Register<ISomeService>(r => r
.From(f => f.ConcreteType<FileService>(ct => ct
    .Parameter("source", (Stream)new FileStream(...)))));
...