Частичный вывод обобщенного типа возможен в C#?
Я работаю над переписыванием моего свободного интерфейса для своей библиотеки классов 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
,
6 ответов
Если у вас есть только два конкретных типа регистрации (что, кажется, имеет место в вашем вопросе), вы можете просто реализовать два метода расширения:
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);
}
Я хотел создать метод расширения, который мог бы перечислять список вещей и возвращать список тех вещей, которые были определенного типа. Это будет выглядеть так:
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) ...
Это компилируется, но всегда дает нулевые результаты и действительно не имеет никакого смысла пытаться. Гораздо приятнее позволить компилятору помочь вам определить эти вещи.
Почему бы вам не указать параметры нулевого типа? Оба могут быть выведены в вашем образце. Если это неприемлемое для вас решение, я тоже часто сталкиваюсь с этой проблемой, и нет простого способа решить проблему "вывести только один параметр типа". Так что я пойду с дубликатами методов.
Как насчет следующего:
Используйте определение, которое вы предоставляете: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(...)))));
Я думаю, вам нужно разделить два параметра типа между двумя различными выражениями; сделайте явный элемент частью типа параметра метода расширения, чтобы логический вывод мог его забрать.
Предположим, вы объявили класс-оболочку:
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>()))
Выглядит некрасиво, но, надеюсь, реже, чем простой полностью выведенный вид.
Обратите внимание, что вы можете просто перегрузить без оболочки и написать:
ct.Parameter("list", (IEnumerable<int>)(new List<int>()))
Но у этого, конечно, есть недостаток: он не работает во время выполнения, если вы что-то не так. К сожалению, далеко от моего компилятора C# прямо сейчас, поэтому извиняюсь, если это далеко.
Я бы использовал решение:
public class JsonDictionary
{
public static readonly Key<int> Foo = new Key<int> { Name = "FOO" };
public static readonly Key<string> Bar = new Key<string> { Name = "BAR" };
IDictionary<string, object> _data;
public JsonDictionary()
{
_data = new Dictionary<string, object>();
}
public void Set<T>(Key<T> key, T obj)
{
_data[key.Name] = obj;
}
public T Get<T>(Key<T> key)
{
return (T)_data[key.Name];
}
public class Key<T>
{
public string Name { get; init; }
}
}
Видеть: