C#泛型-如何避免冗余方法?


28

假设我有两个看起来像这样的类(第一段代码和一般问题与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子句中,则需要关联它们,即,如果我想按照上面所述的方式对它们应用相同的代码,则要继承相同的类。只是拥有两个完全相同的方法非常令人讨厌。由于性能问题,我也不想使用反射。

有人可以建议一些可以更优雅地书写的方法吗?


3
为什么首先要使用泛型?这两个函数没有通用的关系。
a安

1
@Luaan这是抽象工厂模式的简化示例。想象有很多继承ClassA或ClassB的类,而ClassA和ClassB是抽象类。继承的类不包含任何其他信息,需要实例化。我没有选择为每个工厂编写工厂,而是选择使用泛型。
弗拉基米尔·斯托基奇(Fladimir Stokic)'11

6
好吧,如果您确信反射或动力学不会在将来的版本中打破它,则可以使用反射或动力学。
Casey's

实际上,这是我对泛型的最大抱怨是它不能做到这一点。
约书亚

1
@Joshua,我将其更多地视为接口不支持“鸭子键入”的问题。
2016年

Answers:


49

添加一个代理接口(有时称为适配器,有时会有细微的差别),LogicToBeApplied以代理的方式实现,然后添加一种从两个lambda构造此代理实例的方法:一个用于属性get,一个用于集合。

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

现在,只要您需要传递IProxy但拥有第三方类的实例,就可以传递一些lambda:

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

此外,您可以编写简单的帮助程序来从A或B实例构造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();

至于您的工厂,我会看看您是否可以将其重构为一个接受IProxy并对其执行所有逻辑的“主要”工厂方法,以及仅传入new A().Proxied()或的其他方法new B().Proxied()

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

由于C ++模板依赖于结构化类型,因此无法在C#中完成等效的C ++代码。只要两个类具有相同的方法名称和签名,就可以在C ++中对它们两个进行通用调用。C#具有名义上的类型- 类或接口的名称是其类型的一部分。因此,类AB除非通过继承或接口实现定义显式的“是”关系,否则不能以任何能力视为相同。

如果每个类都无法实现这些方法,那么您可以编写一个函数,该函数接受一个对象并通过查找特定的属性名称来反射性地构建一个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));
     }
}

当给定类型不正确的对象时,这会失败。反射从本质上引入了C#类型系统无法防止的故障可能性。幸运的是,您可以避免反射,直到助手的维护负担变得太大为止,因为不需要修改IProxy接口或LambdaProxy实现来添加反射糖。

这项工作有效的部分原因LambdaProxy是“最大程度的通用性”;它可以适应任何实现IProxy合同“精神”的值,因为LambdaProxy的实现完全由给定的getter和setter函数定义。如果这些类具有不同的属性名称,或者可以合理安全地表示为ints的不同类型,或者有某种方法可以将Property应该表示的概念映射到该类的任何其他功能,那么它甚至可以工作。这些函数提供的间接给您最大的灵活性。


这是一种非常有趣的方法,可以肯定地用于调用函数,但是可以将其用于实际需要创建ClassA和ClassB对象的工厂吗?
弗拉基米尔·斯托基奇(Fladimir Stokic)'11

@VladimirStokic查看编辑,我对此进行了扩展
杰克

当然,如果您的映射函数有问题,此方法仍然需要您显式地映射每种类型的属性,并增加运行时错误的可能性
Ewan 2016年

作为的替代方案ReflectiveProxier,您可以使用dynamic关键字构建代理吗?在我看来,您将遇到相同的基本问题(即仅在运行时捕获的错误),但是语法和可维护性将更加简单。
Bobson,

1
@杰克-足够公平。我添加了自己的答案来演示它。在某些罕见的情况下(例如此情况),这是一项非常有用的功能。
Bobson,

12

这是概述如何使用适配器而不继承A和/或B,并可以将它们用于现有的A和B对象:

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

class LogicToBeApplied<T> 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是密封类,此解决方案也将起作用。


为什么new int Property呢 您没有遮挡任何东西。
pinkfloydx33

@ pinkfloydx33:只是一个错字,改了,谢谢。
布朗

9

你能适应ClassAClassB通过一个公共接口。这样,您的代码将LogicAToBeApplied保持不变。与您所拥有的没有太大不同。

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

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

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

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }

1
使用适配器模式 +1 是这里的传统OOP解决方案。它比代理更像是适配器,因为我们将AB类型修改为公共接口。最大的优点是我们不必重复通用逻辑。缺点是现在逻辑实例化了包装器/代理而不是实际的类型。
阿蒙

5
此解决方案的问题在于,您不能简单地获取两个类型为A和B的对象,将它们以某种方式转换为AProxy和BProxy,然后将LogicToBeApplied应用于它们。可以通过使用聚合而不是继承来解决此问题(重新实现代理对象的方法不是从A和B派生,而是通过引用A和B对象)。再次举例说明错误使用继承会导致问题。
布朗

@DocBrown在这种情况下会如何?
弗拉基米尔·斯托基奇(Fladimir Stokic)'11

1
@Jack:这种解决方案在LogicToBeApplied具有一定复杂性时才有意义,并且在任何情况下都不应在代码库的两个位置重复。这样,附加的样板代码通常可以忽略不计。
布朗

1
@Jack冗余在哪里?这两个类没有共同的接口。您创建一个包装具有一个共同的接口。您可以使用该通用接口来实现您的逻辑。这不像C ++代码中不存在相同的冗余-它只是隐藏在一些代码生成的后面。如果您对外观相同的事物有强烈的感觉,即使它们并不相同,也可以始终使用T4或其他模板系统。
a安

8

C ++版本之所以起作用,是因为其模板使用“静态鸭子类型”-只要类型提供正确的名称,任何东西都可以编译。它更像是一个宏系统。C#和其他语言的泛型系统的工作方式非常不同。

devnull和Doc Brown的答案说明了如何使用适配器模式来保持算法的通用性,并仍然可以对任意类型进行操作……有两个限制。值得注意的是,您现在正在创建与实际需要的类型不同的类型。

通过一些技巧,可以完全使用预期的类型而不进行任何更改。但是,我们现在需要将与目标类型的所有交互都提取到一个单独的接口中。在这里,这些交互是构造和属性分配:

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

在OOP解释中,这将是策略模式的一个示例,尽管与泛型混合使用。

然后,我们可以重写您的逻辑以使用这些交互:

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

交互定义如下所示:

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

这种方法的最大缺点是,程序员在调用逻辑时需要编写并传递一个交互实例。这与基于适配器模式的解决方案非常相似,但更为通用。

以我的经验,这是最接近其他语言的模板函数的方法。Haskell,Scala,Go和Rust中使用了类似的技术来实现类型定义之外的接口。但是,在这些语言中,编译器会介入并隐式选择正确的交互实例,因此您实际上看不到额外的参数。这也类似于C#的扩展方法,但不仅限于静态方法。


有趣的方法。这不是我的首选,但是我想它在编写框架之类的东西时可能会有一些优势。
布朗

8

如果您真的想随心所欲,可以使用“动态”功能使编译器为您处理所有反射问题。如果将不具有名为SomeProperty的属性的对象传递给SetSomeProperty,则将导致运行时错误。

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

4

其他答案可以正确识别问题并提供可行的解决方案。C#不(一般)支持“鸭打字”(“如果它走起来像鸭子......”),所以没有办法强迫你ClassAClassB如果他们不这样设计的是可以互换的。

但是,如果您已经愿意接受运行时错误的风险,那么比使用Reflection更容易解决。

C#的dynamic关键字非常适合这种情况。它告诉编译器“直到运行时(甚至可能直到那时),我才知道这是什么类型,所以请允许我对它做任何事情 ”。

使用它,您可以完全构建所需的功能:

public class LogicToBeApplied<T> 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.
    }
}

还要注意static关键字的使用。这使您可以将其用作:

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

使用不会带来大的性能影响,dynamic而是使用Reflection会带来一次性打击(并增加了复杂性)。您的代码第一次匹配具有特定类型的动态调用时,会产生少量开销,但重复调用将与标准代码一样快。但是,如果您尝试传递不具有该属性的内容,则会得到a RuntimeBinderException,并且没有很好的方法可以提前进行检查。您可能想以有用的方式专门处理该错误。


这可能很慢,但是通常慢的代码不是问题。
2013年

@Ian-好点。我增加了一些有关性能的信息。只要您在相同的位置重用相同的类,它实际上并没有您想的那么糟糕。
Bobson

请记住,在C ++模板中甚至没有虚拟方法的开销!
伊恩

2

您可以使用反射按名称拉出属性。

public class logic 
{
    public object getNew<T>() 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 {}

运行时错误和性能问题的可能性是我不想进行反思的原因。但是,我对阅读答案中提到的文章非常感兴趣。期待阅读。
弗拉基米尔·斯托基奇'16

1
您肯定会在使用c ++解决方案时承担同样的风险吗?
伊万

4
@Ewan不,C ++在编译时检查成员
Caleth

反射意味着优化问题,并且(更重要的是)难以调试运行时错误。继承和公共接口意味着提前为这些类中的每个类声明一个子类(无法在现场匿名创建一个子类),并且如果它们每次都不使用相同的属性名称,则该类将不起作用。
杰克

1
@Jack有缺点,但是请考虑在Mappers,序列化器,依赖注入框架等中广泛使用反射,并且目标是用最少的代码重复量来做到这一点
Ewan,2016年

0

我只是想将implicit operator转换与杰克的答案的委托/ lambda方法一起使用。AB假设:

// 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<int> getter;
  readonly Action<int> setter;

  Adapter(Func<int> getter, Action<int> 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;
  }
}

Initialize方法显示了如何使用它Adapter而无需关心它是a A还是a B或其他。该Initialize方法的调用表明,我们不需要任何(可见)浇铸.AsProxy()或类似的处理混凝土A或将其B视为Adapter

请考虑,如果ArgumentNullException传递的参数是空引用,是否要在用户定义的转换中引发。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.