В C# генерики - Как избежать избыточных метод?

Позвольте'ы предположим, у меня есть два класса, которые выглядят так (первый блок кода и общие проблемы, связанные с C#):

class A 
{
    public int IntProperty { get; set; }
}

class B 
{
    public int IntProperty { get; set; }
}

Эти классы не могут быть изменены в любую сторону (они входят в состав 3-го партийного съезда). Поэтому, я не могу заставить их реализовать тот же интерфейс, или унаследовать один и тот же класс, что бы потом содержать 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#, мне нужно написать два класса с двумя различными, где п. Хотя код для логика точно такая же:

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;
    }
}

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

Кто-нибудь может предложить подход, где это может быть написано в более элегантной моды?

Комментарии к вопросу (7)
Решение

Добавить прокси-интерфейс (иногда называют adapter, иногда с тонкими различиями), реализации LogicToBeApplied с точки зрения прокси-сервер, то добавить способ, чтобы создать экземпляр этого прокси из две лямбды: одна за собственность вам и на.

interface IProxy
{
    int Property { get; set; }
}
class LambdaProxy : IProxy
{
    private Function getFunction;
    private Action setFunction;
    int Property
    {
        get { return getFunction(); }
        set { setFunction(value); }
    }
    public LambdaProxy(Function getter, Action setter)
    {
        getFunction = getter;
        setFunction = setter;
    }
}

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

A a = new A();
B b = new B();
IProxy proxyA = new LambdaProxy(() => a.Property, (val) => a.Property = val);
IProxy proxyB = new LambdaProxy(() => b.Property, (val) => b.Property = val);
proxyA.Property = 12; // mutates the proxied `a` as well

Кроме того, вы можете писать простые помощники для построения LamdaProxy экземпляры из случаев А или Б. Они даже могут быть методы расширения, чтобы дать вам и "свободно" в стиле:

public static class ProxyExtension
{
    public static IProxy Proxied(this A a)
    {
      return new LambdaProxy(() => a.Property, (val) => a.Property = val);
    }

    public static IProxy Proxied(this B b)
    {
      return new LambdaProxy(() => b.Property, (val) => b.Property = val);
    }
}

И сейчас строительство прокси выглядит так:

IProxy proxyA = new A().Proxied();
IProxy proxyB = new B().Proxied();

Как на вашем заводе, я'd см. Если вы можете выполнить рефакторинг его в том, что "Главное" фабричный метод, который принимает IProxy и выполняет всю логику на нем и другие методы, которые как раз проходят в новой().Индикатора()илиновый B().Индикатора()`:

public class LogicToBeApplied
{
    public A CreateA() {
      A a = new A();
      InitializeProxy(a.Proxied());
      return a; // or maybe return the proxy if you'd rather use that
    }

    public B CreateB() {
      B b = new B();
      InitializeProxy(b.Proxied());
      return b;
    }

    private void InitializeProxy(IProxy proxy)
    {
        proxy.IntProperty = 50;
    }
}

Там's никакой способ сделать эквивалент кода C++ в C#, потому что шаблоны C++ полагаться на структурных ввод текста. Пока два класса имеют одинаковое имя метода и подпись, в C++ можно назвать универсальной, что способ на них обоих. В C# номинального набрав - имя класса или интерфейса является частью его типа. Поэтому классы " А " и " Б " нельзя обращаться так же, в любом качестве, если явный "это" Мои отношения определяется путем наследования или реализации интерфейса.

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

public class ReflectiveProxier 
{
    public object proxyReflectively(object proxied)
    {
        PropertyInfo prop = proxied.GetType().GetProperty("Property");
        return new LambdaProxy(
            () => prop.GetValue(proxied),
            (val) => prop.SetValue(proxied, val));
     }
}

Это не плачевно, когда данный объект неправильного типа; отражение неотъемлемо связан с возможностью сбоев с системе# тип не может предотвратить. К счастью, вы можете избежать отражения до тех пор, пока бремя содержания помощников становится слишком большим, потому что вы'повторно не требуется менять интерфейс IProxy или реализации LambdaProxy чтобы добавить светоотражающие сахара.

Часть этой работы заключается в том, что LambdaProxy есть "максимально универсального и"; он может адаптировать любое значение, которое реализуется в "дух" из IProxy договора за выполнение LambdaProxy определяется с учетом функций GET и set. Она работает даже если классы имеют различные имена для свойства, или различных типов, которые толково и спокойно представимыми как `инт, или если там's некоторый способ сопоставить понятие, что "собственность" должна представлять для любой другой функции класса. На косвенность предоставленной функции дает вам максимальную гибкость.

Комментарии (8)

Вот план, как использовать адаптеры без наследования от A и/или B, с возможностью использования их для существующих объектов а и Б:

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : IAdapter
{
    A _a;

    public AAdapter()  // use this if you want to have the "logic" part create new objects
    {
        _a=new A();
    }

    public AAdapter(A a) // if you need an adapter for an existing object afterwards
    {
       _a=a;
    }

    public int Property
    {
        get { return _a.Property; }
        set { _a.Property = value; }
    }

    public A {get{return _a; } } // to provide access for non-generic code
}

class BAdapter 
{
     // analogously
}

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

Комментарии (2)

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

class A
{
    public int Property { get; set; }
}
class B
{
    public int Property { get; set; }
}

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }
Комментарии (13)

Версию C++ работает только потому, что ее использование шаблонов “статическая типизация дак” – все будет компилироваться, если этот тип обеспечивает правильные имена. Это больше похоже на макро системы. Система обобщений в C# и других языках, работает очень по-разному.

devnull's и Док Браун's отвечает показать, как картина адаптер может быть использован, чтобы сохранить общий алгоритм, и по-прежнему работать на произвольных типов ... с парой ограничений. В частности, вы'вновь теперь создать другой тип, чем вы на самом деле хотите.

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

interface IInteractions {
  T Instantiate();
  void AssignProperty(T target, int value);
}

В интерпретации ООП, это будет пример шаблон стратегии, хотя и смешанное с дженериков.

Тогда мы можем переписать свою логику, чтобы использовать эти взаимодействия:

public class LogicBToBeApplied
{
    public T CreateElement(IInteractions interactions)
    {
        T retVal = interactions.Instantiate();
        interactions.AssignProperty(retVal, 50);
        return retVal;
    }
}

Определения взаимодействия будет выглядеть следующим образом:

class Interactions_ClassA : IInteractions {
  public override ClassA Instantiate() { return new ClassA(); }
  public override void AssignProperty(ClassA target, int value) { target.IntProperty = value; }
}

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

По моему опыту, это ближайший вы можете получить к шаблону функции в других языках. Подобные методы используются в Haskell, Scala и идти, и ржавчина для реализации интерфейсов вне определения типа. Однако в этих языках шагов компилятора и выбирает нужный экземпляр взаимодействия неявно, так что вы Дон'т видели дополнительным аргументом. Это также похожие на C#'s расширение методов, но не ограничивается статическими методами.

Комментарии (1)

Если вы действительно хотите бросить предостережение в ветер, вы можете использовать и"динамический", чтобы заставить компилятор позаботиться обо всех гадости размышления для вас. Это приведет к ошибке во время выполнения, если вы передаете объект в SetSomeProperty, что не имеет свойства с именем SomeProperty.

using System;

namespace ConsoleApplication3
{
    class A
    {
        public int SomeProperty { get; set; }
    }

    class B
    {
        public int SomeProperty { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            var b = new B();

            SetSomeProperty(a, 7);
            SetSomeProperty(b, 12);

            Console.WriteLine($"a.SomeProperty = {a.SomeProperty}, b.SomeProperty = {b.SomeProperty}");
        }

        static void SetSomeProperty(dynamic obj, int value)
        {
            obj.SomeProperty = value;
        }
    }
}
Комментарии (0)

Остальные ответы правильно определить проблему и находить действенные решения. C# не (как правило) поддержку "с утиной типизацией, то" ("Если это ходит, как утка...и"), так там's не способ, чтобы заставить ваши класса и то, чтобы быть взаимозаменяемыми, если они не'т так устроена.

Однако, если вы're уже готовы принять риск выполнения неисправностей, то там'ы более простой ответ, чем с помощью отражения.

В C# есть динамический ключевое слово, которое идеально подходит для таких ситуаций. Он сообщает компилятору "Я выиграл'т знаю, что типа это до времени (а может даже и не тогда), так что позвольте мне сделать все, чтобы ее".

Используя это, вы можете создать именно ту функцию, которую вы хотите:

public class LogicToBeApplied where T : new()
{
    public static T CreateElement()
    {
        dynamic retVal = new T(); // This doesn't care what type T is.
        retVal.IntProperty = 50;  // This will fail at runtime if there is no "IntProperty" 
                                  // or it doesn't accept an int.
        return retVal;            // Once again, we don't care what it is.
    }
}

Обратите внимание на использование "статический" ключевое слово, а также. Что позволяет использовать это:

A classAElement = LogicToBeApplied<A>.CreateElement();
B classBElement = LogicToBeApplied.CreateElement();

Нет большой картинке точки зрения производительности, используя "динамический", так есть разовый урон (и сложности) с помощью отражения. Первый раз, когда ваш код попадает в динамический вызов с определенным типом будет небольшая сумма накладных, но повторные вызовы будут так же быстро, как стандартный код. Тем не менее, вы ** получите `RuntimeBinderException если ты попытаешься пройти в то, что не'т имеют это свойство, и там's не хороший способ проверить, что будет впереди. Вы, возможно, захотите, чтобы специально обработать эту ошибку в полезной манере.

Комментарии (3)

Вы можете использовать отражение, чтобы вытащить свойству по имени.

public class logic 
{
    public object getNew() where T : new()
    {
        T ret = new T();
        try
        {
            var property = typeof(T).GetProperty("IntProperty");
            if (property != null && property.PropertyType == typeof(int))
            {
                property.SetValue(ret, 50);
            }
        }
        catch (AmbiguousMatchException)
        {
            //hmm..
        }
        return ret;
    }
}

Очевидно, вы рискуете ошибки во время выполнения этого метода. Что C# - это попытка остановить ты делаешь.

Я где-то читал, что в будущих версиях языка C# позволяют передавать объекты в качестве интерфейса, что они Дон'т наследовать, а не матч. Что бы решить вашу проблему.

(Я'Лл попробовать и выкопать статьи)

Другой способ, хотя я'м не уверен, что это экономит вам любой код будет на подклассы A и B, а также наследовать интерфейс с IntProperty.

public interface IIntProp {
    public int IntProperty {get, set}
}

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}
Комментарии (7)

Я просто хотел использовать неявный оператор преобразования вместе с подходом делегат/лямбда Джека'ы ответ. A и B являются, как предполагалось:

// A and B are mutable reference types

class A
{
  public int IntProperty { get; set; }
}

class B
{
  public int IntProperty { get; set; }
}

Тогда легко получить хороший синтаксис с неявные пользовательские преобразования (методы продления или аналогичная требуется):

// Adapter is an immutable type. However, the delegate instances have a captured reference to an A or a B (closure semantics)
struct Adapter
{
  readonly Func getter;
  readonly Action setter;

  Adapter(Func getter, Action setter)
  {
    this.getter = getter;
    this.setter = setter;
  }

  public int IntProperty
  {
    get { return getter(); }
    set { setter(value); }
  }

  public static implicit operator Adapter(A a) => new Adapter(() => a.IntProperty, x => a.IntProperty = x);
  public static implicit operator Adapter(B b) => new Adapter(() => b.IntProperty, x => b.IntProperty = x);

  public A CloneToA() => new A { IntProperty = getter(), };
  public B CloneToB() => new B { IntProperty = getter(), };
}

Иллюстрация использования:

class LogicToBeApplied
{
  public static A CreateA()
  {
    var a = new A();
    Initialize(a);
    return a;
  }
  public static B CreateB()
  {
    var b = new B();
    Initialize(b);
    return b;
  }

  static void Initialize(Adapter a)
  {
    a.IntProperty = 50;
  }
}

В инициализации метод показывает, как можно работать с "адаптер", не заботясь о том, будет ли это " А " или " Б " или что-то другое. На призывы о инициализировать способ показать, что нам не нужно никаких (видимых) литой или.AsProxy () или аналогичные для лечения бетона A или B в качестве адаптер.

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

Комментарии (0)