Давайте предположим, что у меня есть два класса, которые выглядят так (первый блок кода и общая проблема связаны с C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Эти классы не могут быть изменены каким-либо образом (они являются частью сторонней сборки). Поэтому я не могу заставить их реализовывать один и тот же интерфейс или наследовать тот же класс, который затем будет содержать IntProperty.
Я хочу применить некоторую логику к IntProperty
свойствам обоих классов, и в C ++ я мог бы использовать шаблонный класс, чтобы сделать это довольно легко:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
И тогда я мог бы сделать что-то вроде этого:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
Таким образом, я мог бы создать один общий фабричный класс, который работал бы как для ClassA, так и для ClassB.
Однако в C # мне нужно написать два класса с двумя разными where
предложениями, хотя код для логики точно такой же:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
Я знаю, что если я хочу иметь разные классы в where
предложении, они должны быть связаны, то есть наследовать один и тот же класс, если я хочу применить к ним один и тот же код в том смысле, который я описал выше. Просто очень неприятно иметь два совершенно одинаковых метода. Я также не хочу использовать отражение из-за проблем с производительностью.
Может кто-нибудь предложить какой-нибудь подход, где это можно написать более элегантно?
Ответы:
Добавьте интерфейс прокси (иногда называемый адаптером , иногда с небольшими различиями), реализуйте его
LogicToBeApplied
в терминах прокси, а затем добавьте способ создания экземпляра этого прокси из двух лямбд: один для свойства get и один для набора.Теперь, когда вам нужно передать IProxy, но есть экземпляр сторонних классов, вы можете просто передать несколько лямбд:
Кроме того, вы можете написать простые помощники для создания экземпляров LamdaProxy из экземпляров A или B. Они могут даже быть методами расширения, чтобы дать вам «свободный» стиль:
И теперь конструкция прокси выглядит так:
Что касается вашей фабрики, я бы посмотрел, можете ли вы преобразовать ее в «основной» метод фабрики, который принимает IProxy и выполняет всю логику для него и других методов, которые просто передают
new A().Proxied()
илиnew B().Proxied()
:Там нет никакого способа сделать эквивалент вашего кода C ++ в C #, потому что шаблоны C ++ полагаются на структурную типизацию. Пока два класса имеют одинаковые имя и сигнатуру метода, в C ++ вы можете вызывать этот метод в обоих случаях. C # имеет номинальную типизацию - имя класса или интерфейса является частью его типа. Следовательно, классы
A
иB
не могут обрабатываться одинаково в любой емкости, если явное отношение «не является» определено с помощью наследования или реализации интерфейса.Если шаблон реализации этих методов для каждого класса слишком велик, вы можете написать функцию, которая берет объект и рефлексивно строит его
LambdaProxy
, ища определенное имя свойства:Это терпит неудачу ужасно, когда даны объекты неправильного типа; отражение по своей сути вводит возможность отказов, которые система типов C # не может предотвратить. К счастью, вы можете избежать рефлексии, пока бремя обслуживания помощников не станет слишком большим, потому что вам не нужно изменять интерфейс IProxy или реализацию LambdaProxy для добавления отражающего сахара.
Одна из причин, по которой это работает, заключается в том, что
LambdaProxy
он «максимально универсален»; он может адаптировать любое значение, которое реализует «дух» контракта IProxy, потому что реализация LambdaProxy полностью определяется заданными функциями получения и установки. Это даже работает, если классы имеют разные имена для свойства, или разные типы, которые разумно и безопасно представлены какint
s, или если есть какой-то способ сопоставить концепцию, котораяProperty
должна представлять, с любыми другими функциями класса. Направленность, предоставляемая функциями, обеспечивает максимальную гибкость.источник
ReflectiveProxier
, вы могли бы создать прокси, используяdynamic
ключевое слово? Мне кажется, у вас будут те же фундаментальные проблемы (то есть ошибки, которые обнаруживаются только во время выполнения), но синтаксис и удобство сопровождения будут намного проще.Вот схема использования адаптеров без наследования от A и / или B с возможностью их использования для существующих объектов A и B:
Как правило, я бы предпочел этот тип адаптера объекта над прокси класса, они избегают неприятных проблем, с которыми можно столкнуться при наследовании. Например, это решение будет работать, даже если A и B являются запечатанными классами.
источник
new int Property
? ты ничего не слежу.Вы можете адаптироваться
ClassA
иClassB
через общий интерфейс. Таким образом, ваш кодLogicAToBeApplied
остается прежним. Не сильно отличается от того, что у вас есть, хотя.источник
A
,B
тип в общий интерфейс. Большим преимуществом является то, что нам не нужно дублировать общую логику. Недостатком является то, что логика теперь создает экземпляр оболочки / прокси вместо фактического типа.LogicToBeApplied
имеет определенную сложность и не должно повторяться в двух местах кодовой базы ни при каких обстоятельствах. Тогда дополнительный шаблонный код часто пренебрежимо мал.Версия C ++ работает только потому, что ее шаблоны используют «статическую типизацию» - все компилируется, пока тип предоставляет правильные имена. Это больше похоже на макросистему. Система дженериков C # и других языков работает совсем по-другому.
Ответы devnull и Doc Brown показывают, как можно использовать шаблон адаптера, чтобы сохранить общий алгоритм и работать с произвольными типами… с несколькими ограничениями. В частности, вы сейчас создаете другой тип, чем вы на самом деле хотите.
Приложив немного хитрости, можно использовать точно намеченный тип без каких-либо изменений. Однако теперь нам нужно извлечь все взаимодействия с целевым типом в отдельный интерфейс. Здесь эти взаимодействия являются конструкцией и назначением свойства:
В интерпретации ООП это будет примером модели стратегии , хотя и в сочетании с генериками.
Затем мы можем переписать вашу логику, чтобы использовать эти взаимодействия:
Определения взаимодействия будут выглядеть так:
Большим недостатком этого подхода является то, что при вызове логики программист должен написать и передать экземпляр взаимодействия. Это довольно похоже на решения на основе шаблона адаптера, но немного более общее.
По моему опыту, это самое близкое, что вы можете получить к шаблонным функциям на других языках. Подобные методы используются в Haskell, Scala, Go и Rust для реализации интерфейсов вне определения типа. Однако в этих языках компилятор неявно выбирает правильный экземпляр взаимодействия, поэтому вы не видите лишний аргумент. Это также похоже на методы расширения C #, но не ограничивается статическими методами.
источник
Если вы действительно хотите проявить осторожность, вы можете использовать «динамический», чтобы компилятор позаботился обо всех неприятностях для вас. Это приведет к ошибке времени выполнения, если вы передадите объект в SetSomeProperty, у которого нет свойства с именем SomeProperty.
источник
Другие ответы правильно определяют проблему и дают работоспособные решения. C # (как правило) не поддерживает «типизацию утки» («если она ходит как утка ...»), поэтому нет способа заставить вас
ClassA
иClassB
быть взаимозаменяемыми, если они не были разработаны таким образом.Однако, если вы уже готовы принять риск ошибки во время выполнения, тогда есть более простой ответ, чем использование Reflection.
C # имеет
dynamic
ключевое слово, которое идеально подходит для подобных ситуаций. Он сообщает компилятору: «Я не буду знать, что это за тип, до времени выполнения (и, может быть, даже тогда), поэтому позвольте мне что- нибудь с ним сделать».Используя это, вы можете построить именно ту функцию, которую вы хотите:
Обратите внимание на использование
static
ключевого слова. Это позволяет вам использовать это как:Не существует никаких значительных последствий для производительности, связанных с использованием
dynamic
, так как существует одноразовый хит (и дополнительная сложность) использования Reflection. При первом обращении вашего кода к динамическому вызову с определенным типом накладные расходы будут незначительными , но повторные вызовы будут такими же быстрыми, как и стандартный код. Тем не менее, вы будете получить ,RuntimeBinderException
если вы попытаетесь передать то , что не имеет это имущество, и нет хорошего способа проверить , что загодя. Вы можете специально обработать эту ошибку полезным способом.источник
Вы можете использовать отражение, чтобы вытащить свойство по имени.
Очевидно, что вы рискуете ошибкой во время выполнения с этим методом. Именно это и пытается остановить C #.
Я где-то читал, что будущая версия C # позволит вам передавать объекты в качестве интерфейса, который они не наследуют, но соответствуют. Что также решит вашу проблему.
(Попробую выкопать статью)
Другой метод, хотя я не уверен, что он спасет вас от какого-либо кода, заключался бы в создании подклассов как A, так и B, а также наследовании интерфейса с IntProperty.
источник
Я просто хотел использовать
implicit operator
преобразования вместе с подходом делегата / лямбды в ответе Джека.A
иB
как предполагается:Тогда легко получить хороший синтаксис с неявными пользовательскими преобразованиями (не нужно расширенных методов или подобных):
Иллюстрация использования:
В
Initialize
метод показывает , как вы можете работать с ,Adapter
не заботясь о том, является ли этоA
илиB
или что - то другое. ВызовыInitialize
метода показывают, что нам не нужны какие-либо (видимые) отливки.AsProxy()
или подобные для обработки бетонаA
илиB
какAdapter
.Подумайте, хотите ли вы
ArgumentNullException
добавить пользовательские преобразования, если переданный аргумент является пустой ссылкой или нет.источник