使包装类的扩展方法/构造函数成为通用类型

3
在一个我无法更改的服务中,我有两个对象类BarBaz,它们具有大部分相似的属性(但遗憾的是,它们不是从同一基类派生或继承自相同的接口... 是的-愚蠢),以及与它们的相对BarQuxBazQux属性相关的依赖类:
public class Bar                          public class Baz
{                                         {
    public int ID { get; set; }               public int ID { get; set; }
    public bool Active { get; set; }          public bool Active { get; set; }
    public int BarQux { get; set; }           public int BazQux { get; set; }
    ...                                       ...
}                                         }

public class Qux
{
    public int ID { get; set; } // Corresponds to BarQux and BazQux
    public string Name { get; set; }
}

在一个WPF屏幕中,我正在将每种类型(Baz和Bar)的列表绑定到两个单独的ListView上。我需要每个都有一个额外的选中复选框列。为此,我创建了一个包装类,其中包含公共属性、额外的Selected属性和每个的构造函数:
public class Foo
{
    public Foo(Bar bar, Qux qux)
    {
        this.Active = bar.Active;
        this.FooQux = string.Format("{0} - {1}", qux.ID, qux.Name);
        ...
    }

    public Foo(Baz baz, Qux qux)
    {
        this.Active = baz.Active;
        this.FooQux = string.Format("{0} - {1}", qux.ID, qux.Name);
        ...
    }

    public bool Selected { get; set; }
    public int ID { get; set; }
    public bool Active { get; set; }
    public string FooQux { get; set; }
    ...
}

为了将每个类别为 BazBar 的集合转换为 Foo 的集合,我创建了以下扩展方法:
public static List<Foo> ToFoo(this IEnumerable<Bar> bars, IEnumerable<Qux> quxs)
{
    List<Foo> foos = new List<Foo>();

    foreach (Bar bar in bars)
    {
        Foo foo = new Foo(bar, quxs.Single(qux => qux.ID == bar.BarQux));
        foos.Add(foo);
    }

    return foos;
}

public static List<Foo> ToFoo(this IEnumerable<Baz> bazs, IEnumerable<Qux> quxs)
{
    List<Foo> foos = new List<Foo>();

    foreach (Baz baz in bazs)
    {
        Foo foo = new Foo(baz, quxs.Single(qux => qux.ID == baz.BazQux));
        foos.Add(foo);
    }

    return foos;
}

问题:

如何将这个设计泛化?

理论、实现和错误:

  1. Since the constructors are virtually the same besides the Bar and Baz parameters, can I somehow use generic type T to make one constructor and still grab the properties?

    public class Foo<T>
    {
        public Foo(T obj, Qux qux)
        {
            this.Active = obj.Active; // 'T' does not contain a definition for 'Active'...
            this.Qux = string.Format("{0} - {1}", qux.ID, qux.Name);
            ...
        }
        ...
    }
    
  2. Change the constructors to receive the whole collection of Qux objects and do the quxs.Single(qux => qux.ID == object.ObjectQux) logic there. Then make the extension methods into one generic method, something like the following.

    public static List<Foo> ToFoo<T>(this IEnumerable<T> objCollection, IEnumerable<Qux> quxs)
    {
        List<Foo> foos = new List<Foo>();
    
        foreach (T obj in objCollection)
        {
            Foo foo = new Foo(obj, quxs); // The best overloaded method... has some invalid arguments.
            foos.Add(foo);
        }
    
        return foos;
    }
    
  3. Both 1 and 2 combined? Anything I haven't thought of?


你可能想要查看这个 https://zh.wikipedia.org/wiki/适配器模式 - BlackBear
你不能修改文件或类吗?例如,你可以实现一个部分类,但是你必须将原始类更改为部分类。 - Scott Nimrod
@ScottNimrod 我无法修改服务中的任何类或文件,例如 BarBazType 类。如果我可以的话,我想让前两个继承基类或接口——这将使我在泛化事物方面的经验受益更多。 - OhBeWise
1
这似乎是DTO对象与BusinessObject之间的情况。您可以完全控制businessObject的实现,但DTO对象实际上来自难以更改的外部服务。 - cscmh99
3个回答

1
如果您的属性有限,列表中的项目数量也很少,那么可以使用Reflection。由于您将在WPF中使用此功能,因此建议将进程移至单独的后台线程。
通用Foo
public class Foo<T>
{
    public Foo(T obj, Qux qux)
    {
        //this.Active = obj.Active;

        var fooProps = GetType().GetProperties().ToList();
        var tProps = typeof(T).GetProperties()
            .Where(p =>
            {
                var w = fooProps.FirstOrDefault(f => f.Name == p.Name);
                return w != null;
            }).ToList();

        foreach (var propertyInfo in tProps)
        {
            var val = propertyInfo.GetValue(obj);
            fooProps.First(e => e.Name == propertyInfo.Name).SetValue(this, val);
        }
        this.FooQux = string.Format("{0} - {1}", qux.ID, qux.Name);
    }
    public bool Selected { get; set; }
    public int ID { get; set; }
    public bool Active { get; set; }
    public string FooQux { get; set; }
}

您的扩展方法。
    public static IEnumerable<Foo<Bar>> ToFoo(this IEnumerable<Bar> bars, IEnumerable<Qux> quxs)
    {
        return bars.
            Select(bar => new Foo<Bar>(bar, quxs.Single(qux => qux.ID == bar.BarQux))).ToList();
    }

    public static IEnumerable<Foo<Baz>> ToFoo(this IEnumerable<Baz> bazs, IEnumerable<Qux> quxs)
    {
        return bazs.
            Select(baz => new Foo<Baz>(baz, quxs.Single(qux => qux.ID == baz.BazQux))).ToList();
    }
    public static IEnumerable<Qux> ToQuxes(this IEnumerable<BazQux> bazQuxs)
    {
        return bazQuxs.Select(b => new Qux(typeof(BazQux), b)).ToList();
    }

    public static IEnumerable<Qux> ToQuxes(this IEnumerable<BarQux> barQuxes )
    {
        return barQuxes.Select(b => new Qux(typeof(BarQux), b)).ToList();
    }

同样地,您也可以将您的BarQuxBazQux转换为非泛型Qux类。
public class Qux
{
    public int ID { get; set; } // Corresponds to BarQux and BazQux
    public string Name { get; set; }

    public Qux(Type type, object obj)
    {
        var ob = Convert.ChangeType(obj, type);

        var quxProps = GetType().GetProperties();
        var obProps = ob.GetType().GetProperties()
            .Where(p =>
            {
                var w = quxProps.FirstOrDefault(f => f.Name == p.Name);
                return w != null;
            }).ToList();
        foreach (var propertyInfo in obProps)
        {
            var val = propertyInfo.GetValue(obj);
            quxProps.First(e => e.Name == propertyInfo.Name).SetValue(this, val);
        }

    }
}

你可以直接调用 ToFoo 扩展方法,就可以得到一个 Foo 的列表。
你也可以使用 Qux 类的逻辑将 Foo 转换为非泛型。

写得很好。我目前正在测试一个类似的想法,尽管我更喜欢您循环遍历属性的方法。由于不太了解反射,我很好奇这是否会太昂贵,因此不值得花费时间 - 这些类有6个共同属性加上Qux属性,通常为任何给定客户端转换为Foo的对象数量将从仅有几个到可能达到几百个。您有什么想法吗?我需要一些时间来测试您的建议,之后再回复您。 - OhBeWise
1
看着6个属性和几百条记录的数字,上述解决方案是可行的。你还有另一个好处,就是客户端机器将使用反射进行处理。如果它在服务端,你可能会遇到问题,因为托管服务的机器会受到影响。 - Sandesh
感谢您的输入!我无法对Qux类应用任何更改,因为它也是该服务的一部分 - 但我也不需要它们。通用类Foo效果很好。我发布了一个答案,展示了我的确切用法,使扩展方法也具有通用性。 - OhBeWise

0
根据Sandesh的建议,最终结果如下:
public class Foo<T>
{
    public Foo() {}

    public Foo(T obj, IEnumerable<Qux> quxs, string quxPropName)
    {
        var fooProps = GetType().GetProperties().ToList();
        var tProps = typeof(T).GetProperties()
            .Where(p =>
            {
                var w = fooProps.FirstOrDefault(f => f.Name == p.Name);
                return w != null;
            }).ToList();

        foreach (var propertyInfo in tProps)
        {
            var val = propertyInfo.GetValue(obj, null);
            fooProps.First(e => e.Name == propertyInfo.Name).SetValue(this, val, null);
        }

        int id = (int)typeof(T).GetProperty(quxPropName).GetValue(obj, null);
        Qux qux = quxs.Single(q => q.ID == id);
        this.FooQux = string.Format("{0} - {1}", qux.ID, qux.Name);
    }

    public bool Selected { get; set; }
    public int ID { get; set; }
    public bool Active { get; set; }
    public string FooQux { get; set; }
}

还有通用的扩展方法:

public static List<Foo<T>> ToFoo<T>(this IEnumerable<T> list, IEnumerable<Qux> quxs, string quxPropName)
{
    List<Foo<T>> foos = null;

    try
    {
        foos = list.Select(obj => new Foo<T>(obj, quxs, quxPropName)).ToList();
    }
    catch
    {
        foos = new List<Foo<T>>();
    }

    return foos;
}

以及使用方法:

List<Foo<Bar>> bars = barCollection.ToFoo(quxCollection, "BarQux");
List<Foo<Baz>> bazs = bazCollection.ToFoo(quxCollection, "BazQux");

1
添加一些异常处理,因为这种方法也存在被滥用的可能性,即 ToFoo 扩展方法可以用于其他实现了 IEnumerable<T> 的类上,而您可能希望防止这种情况的发生。 - Sandesh

0

我真的看不出这样做有什么好处。

如果你坚持使用泛型,你可以对 T 进行一些类型检查,像这样:

public class Foo<T>
{
    public Foo(T obj, Qux qux)
    {
        var baz = obj as Baz;
        var bar = obj as Bar;

        Active = baz != null && baz.Active || bar != null && bar.Active;

        this.FooQux = string.Format("{0} - {1}", qux.ID, qux.Name);
    }

    public bool Selected { get; set; }
    public int ID { get; set; }
    public bool Active { get; set; }
    public string FooQux { get; set; }
}

或者:

public static List<Foo> ToFoo<T>(this IEnumerable<T> objCollection, IEnumerable<Qux> quxs)
{
    List<Foo> foos = new List<Foo>();

    foreach (T obj in objCollection)
    {
        var baz = obj as Baz;
        var bar = obj as Bar;

        Foo foo = null;

        if(baz != null)
            foo = new Foo(baz, quxs.Single(qux => qux.ID == baz.BazQux));

        if(bar != null)
            foo = new Foo(bar, quxs.Single(qux => qux.ID == bar.BarQux));

        if(foo != null)
            foos.Add(foo);
    }

    return foos;
}

但是这显然比你已经拥有的东西好不了多少,如果T不是Bar或Baz(未显示),你将不得不进行一些异常处理,这会使你面临潜在的运行时故障。你也可以使用反射,但那也会做同样的事情。

不幸的是,你所寻找的在C#中不可能实现。唯一的方法是如果接口像Golang一样被鸭子类型化,但这并不是事实。


1
在实现包装器方面有一个有效的观点。我曾经遇到这样一种情况,应用程序开发已经进行了一半,当时我被告知可能还有一些具有相同属性名称和类型的类也需要处理。不可能再编写更多的构造函数。因此,我使用反射来处理其他类,以节省开发时间。 - Sandesh
我对泛型实现的兴趣在于减少重复代码和减少未来服务类更改所需的工作量。也许我已经拥有的东西已经足够了,泛型可能会更少 - 我愿意接受 - 这是一个学习的过程。 - OhBeWise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接