Частичный вывод универсального типа возможен в C#?
Я работаю над переписыванием своего интерфейса fluent для библиотеки классов 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.
Edit : причина, по которой я предпочел бы не оставлять оба аргумента универсального типа для вывода, заключается в том, что для некоторых служб я хочу указать значение параметра для параметра конструктора это один тип, но передайте значение, которое является потомком. На данный момент сопоставление указанных значений аргумента и правильного конструктора для вызова выполняется с использованием как имени, так и типа аргумента.
Приведу пример:
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 ответов:
Если у вас есть только два конкретных типа регистрации (что, по-видимому, имеет место в вашем вопросе), вы можете просто реализовать два метода расширения:
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#, и это очень полезно :-)). Вы, вероятно, могли бы реализовать обходной путь, который позволил бы вам написать это (в основном, разделив вызов на два вызова метода, чтобы получить два места, где может быть применен тип inferrence):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>()))
Выглядит уродливо, но, надеюсь, реже, чем простой полностью выводимый вид.
Обратите внимание, что Вы могли бы просто иметь перегрузку no-wrapper и написать:
Но у этого, конечно, есть недостаток-сбой во время выполнения, если вы получаете что-то не так. К сожалению, далеко от моего компилятора C# прямо сейчас, так что извиняюсь, если это далеко.ct.Parameter("list", (IEnumerable<int>)(new List<int>()))